diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..c4e4683 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +BCSD_Android_2025-1 \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..c224ad5 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f7608ed --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2025-06-21__8_40__Changes_.xml b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2025-06-21__8_40__Changes_.xml new file mode 100644 index 0000000..bdc777e --- /dev/null +++ b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2025-06-21__8_40__Changes_.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git "a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2025-06-21_\354\230\244\355\233\204_8_40_[Changes]/shelved.patch" "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2025-06-21_\354\230\244\355\233\204_8_40_[Changes]/shelved.patch" new file mode 100644 index 0000000..6ba94fa --- /dev/null +++ "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2025-06-21_\354\230\244\355\233\204_8_40_[Changes]/shelved.patch" @@ -0,0 +1,54 @@ +Index: app/src/main/java/com/example/bcsd_android_2025_1/NotificationReceiver.kt +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>package com.example.bcsd_android_2025_1\r\n\r\nimport android.content.BroadcastReceiver\r\nimport android.content.Context\r\nimport android.content.Intent\r\nimport android.app.NotificationManager\r\n\r\nclass NotificationReceiver : BroadcastReceiver() {\r\n override fun onReceive(context: Context, intent: Intent) {\r\n if (intent.action == \"ACTION_CLOSE_NOTIFICATION\") {\r\n val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\r\n manager.cancel(1001)\r\n }\r\n }\r\n} +=================================================================== +diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/NotificationReceiver.kt b/app/src/main/java/com/example/bcsd_android_2025_1/NotificationReceiver.kt +--- a/app/src/main/java/com/example/bcsd_android_2025_1/NotificationReceiver.kt (revision 4f6ece16be6f02f2dbafa328f24458f0f7c007d0) ++++ b/app/src/main/java/com/example/bcsd_android_2025_1/NotificationReceiver.kt (date 1748276211662) +@@ -12,4 +12,5 @@ + manager.cancel(1001) + } + } +-} +\ No newline at end of file ++} ++ +Index: .idea/deploymentTargetSelector.xml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n +=================================================================== +diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml +--- a/.idea/deploymentTargetSelector.xml (revision 4f6ece16be6f02f2dbafa328f24458f0f7c007d0) ++++ b/.idea/deploymentTargetSelector.xml (date 1748343144801) +@@ -2,10 +2,10 @@ + + + +- ++ + +- ++ + + +Index: .idea/vcs.xml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>\r\n\r\n \r\n \r\n \r\n +=================================================================== +diff --git a/.idea/vcs.xml b/.idea/vcs.xml +--- a/.idea/vcs.xml (revision 4f6ece16be6f02f2dbafa328f24458f0f7c007d0) ++++ b/.idea/vcs.xml (date 1748343115810) +@@ -1,6 +1,6 @@ + + + +- ++ + + +\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5791375..ff0bc0a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,7 +36,7 @@ android { } dependencies { - + implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c80941..ac9be35 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,15 @@ + + + + + + + + + + @@ -23,4 +38,5 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/AudioAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/AudioAdapter.kt new file mode 100644 index 0000000..7a3deca --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/AudioAdapter.kt @@ -0,0 +1,64 @@ +package com.example.bcsd_android_2025_1 + +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView + +class AudioAdapter : RecyclerView.Adapter(){ + + + private val items = mutableListOf() + + fun submitList(newList: List) { + items.clear() + items.addAll(newList) + notifyDataSetChanged() + } + + inner class AudioViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val titleTextView: TextView = itemView.findViewById(R.id.tv_item_title) + val artistTextView: TextView = itemView.findViewById(R.id.tv_item_artist) + val timeTextView: TextView = itemView.findViewById(R.id.tv_item_time) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudioViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item, parent, false) + return AudioViewHolder(view) + } + + override fun onBindViewHolder(holder: AudioViewHolder, position: Int) { + val item = items[position] + holder.titleTextView.text = item.name + holder.artistTextView.text = item.artist + + val totalTime = item.duration/1000 + val hours = totalTime/3600 + val minutes = (totalTime%3600)/60 + val seconds = totalTime%60 + if (hours > 0){ + holder.timeTextView.text = String.format("%d:%02d:%02d", hours, minutes, seconds) + } else { + holder.timeTextView.text = String.format("%d:%02d", minutes, seconds) + } + + holder.itemView.setOnClickListener { + val context = holder.itemView.context + val intent = Intent(context, AudioPlayer::class.java).apply{ + putExtra("AUDIO_URI",item.uri) + putExtra("AUDIO_TITLE",item.name) + } + ContextCompat.startForegroundService(context, intent) + } + + } + + override fun getItemCount(): Int = items.size + + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/AudioListManager.kt b/app/src/main/java/com/example/bcsd_android_2025_1/AudioListManager.kt new file mode 100644 index 0000000..598fa83 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/AudioListManager.kt @@ -0,0 +1,62 @@ +package com.example.bcsd_android_2025_1 + +import android.app.Application +import android.content.ContentUris +import android.provider.MediaStore.Audio.Media +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + + +class AudioListManager(application: Application) : AndroidViewModel(application) { + private val audioListIn = MutableLiveData>() + val audioListObserve: LiveData> get() = audioListIn + + fun loadAudioFiles() { + viewModelScope.launch(Dispatchers.IO) { + val list = mutableListOf() + val resolver = getApplication().contentResolver + + val projection = arrayOf( + Media._ID, + Media.DISPLAY_NAME, + Media.ARTIST, + Media.DURATION + ) + + val selection= "${Media.IS_MUSIC} != 0 AND ${Media.DURATION} > 0" + val selectionArgs: Array? = null + + val sortOrder = "${Media.DATE_ADDED} DESC" + + resolver.query( + Media.EXTERNAL_CONTENT_URI, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(Media._ID) + val nameColumn = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME) + val artistColumn = cursor.getColumnIndexOrThrow(Media.ARTIST) + val durationColumn = cursor.getColumnIndexOrThrow(Media.DURATION) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + val name = cursor.getString(nameColumn) + val artists = cursor.getString(artistColumn) + val duration = cursor.getLong(durationColumn) + val uri = ContentUris.withAppendedId(Media.EXTERNAL_CONTENT_URI, id) + + list.add(MainActivity.AudioItem(id, name, artists, duration, uri)) + + } + } + + audioListIn.postValue(list) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/AudioPlayer.kt b/app/src/main/java/com/example/bcsd_android_2025_1/AudioPlayer.kt new file mode 100644 index 0000000..afe035e --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/AudioPlayer.kt @@ -0,0 +1,92 @@ +package com.example.bcsd_android_2025_1 + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.media.MediaPlayer +import android.net.Uri +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat + + +@Suppress("DEPRECATION") +class AudioPlayer : Service() { + + private lateinit var mediaPlayer: MediaPlayer + private var currentAudioTitle: String = "" + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val audioUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent?.getParcelableExtra("AUDIO_URI", Uri::class.java) + } else { + @Suppress("DEPRECATION") + intent?.getParcelableExtra("AUDIO_URI") + + } + currentAudioTitle = intent?.getStringExtra("AUDIO_TITLE") ?: "음악" + + audioUri?.let { + mediaPlayer = MediaPlayer.create(this, audioUri) + mediaPlayer.setOnCompletionListener { + stopForeground(true) + } + mediaPlayer.start() + showNotification(currentAudioTitle) + nowPlaying(currentAudioTitle) + } + return START_NOT_STICKY + } + + private fun showNotification(title: String) { + val channelId = "music_channel" + val channelName = "음악 재생 채널" + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + channelName, + NotificationManager.IMPORTANCE_LOW + ) + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + + val intent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + + + val notification = NotificationCompat.Builder(this, channelId) + .setContentTitle("재생 중") + .setContentText(title) + .setSmallIcon(R.drawable.ic_launcher_background) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build() + + startForeground(1, notification) + } + + override fun onDestroy() { + if (::mediaPlayer.isInitialized){ + mediaPlayer.release() + } + super.onDestroy() + } + + override fun onBind(p0: Intent?): IBinder? = null + + private fun nowPlaying(title: String) { + val intent = Intent(MainActivity.NOW_PLAYING) + intent.putExtra("AUDIO_TITLE",title) + sendBroadcast(intent) + } + +} \ 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..381a5aa 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,163 @@ package com.example.bcsd_android_2025_1 +import android.Manifest +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build import android.os.Bundle -import androidx.activity.enableEdgeToEdge +import android.provider.Settings +import android.view.View +import android.widget.Button +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat +import androidx.core.app.ActivityCompat +import androidx.core.net.toUri +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView class MainActivity : AppCompatActivity() { + + private lateinit var permissionName: String + private lateinit var audioAdapter: AudioAdapter + + private lateinit var tvMain : TextView + private lateinit var btnMain : Button + + private lateinit var audioListManager : AudioListManager + + private lateinit var playingAudioAdapter : PlayingAudioAdapter + + companion object { + const val NOW_PLAYING = "nowPlaying" + } + + private val requestPermission = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + when (isGranted) { + true -> getAudioFile() + else -> { + when (shouldShowRequestPermissionRationale(permissionName)) { + true -> permissionDialog(true) + else -> permissionDialog(false) + } + } + } + } + + private val nowPlayingReceive = object : BroadcastReceiver() { + override fun onReceive(p0: Context?, p1: Intent?) { + val title = p1?.getStringExtra("AUDIO_TITLE") ?: return + val id = p1.getLongExtra("AUDIO_ID", -1) + val artist = p1.getStringExtra("AUDIO_ARTIST") ?: "알 수 없음" + val duration = p1.getLongExtra("AUDIO_DURATION",0) + val uri = p1.getStringExtra("AUDIO_URI")?.toUri() ?: return + + val item = AudioItem(id, title, artist, duration, uri) + playingAudioAdapter.setItem(item) + + } + } + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + permissionName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_AUDIO + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + 1001 + ) + } + } + +//초기화 + playingAudioAdapter = PlayingAudioAdapter() + audioAdapter =AudioAdapter() + audioListManager = AudioListManager(application) +//리사이클(전체) + val recyclerView = findViewById(R.id.rv_main) + recyclerView.adapter = audioAdapter + recyclerView.layoutManager = LinearLayoutManager(this) +//리사이클(현재) + val playingRecyclerView = findViewById(R.id.rv_playing) + playingRecyclerView.adapter = playingAudioAdapter + playingRecyclerView.layoutManager = LinearLayoutManager(this) + + requestPermission.launch(permissionName) + + tvMain = findViewById(R.id.tv_main) + btnMain = findViewById(R.id.btn_main) + + audioListManager.audioListObserve.observe(this) {list -> + audioAdapter.submitList(list) + } + + } + + override fun onResume() { + super.onResume() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(nowPlayingReceive, IntentFilter(NOW_PLAYING), RECEIVER_NOT_EXPORTED) + } } + + override fun onPause() { + super.onPause() + unregisterReceiver(nowPlayingReceive) + } + + private fun permissionDialog(isShowRationale: Boolean) { + if (isShowRationale) { + AlertDialog.Builder(this) + .setTitle(R.string.tv_main) + .setPositiveButton(R.string.alert_btn_positive){ + _, _ -> + requestPermission.launch(permissionName) + } + .setNegativeButton(R.string.alert_btn_negative, null) + .show() + + } + + tvMain.visibility = View.VISIBLE + btnMain.visibility = View.VISIBLE + + btnMain.setOnClickListener { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = "package:$packageName".toUri() + } + startActivity(intent) + } + } + + private fun getAudioFile() { + tvMain.visibility = View.GONE + btnMain.visibility = View.GONE + audioListManager.loadAudioFiles() + } + + data class AudioItem( + val id: Long, + val name: String, + val artist: String, + val duration: Long, + val uri: Uri + ) + + } \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/PlayingAudioAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/PlayingAudioAdapter.kt new file mode 100644 index 0000000..39949ba --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/PlayingAudioAdapter.kt @@ -0,0 +1,38 @@ +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 PlayingAudioAdapter : RecyclerView.Adapter(){ + + + private var playItem : MainActivity.AudioItem? = null + + fun setItem(item: MainActivity.AudioItem){ + playItem = item + notifyDataSetChanged() + } + + inner class PlayingAudioHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val playTitle = itemView.findViewById(R.id.tv_item_title) + val playArtist = itemView.findViewById(R.id.tv_item_artist) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlayingAudioHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item, parent, false) + return PlayingAudioHolder(view) + } + + override fun getItemCount(): Int = if (playItem != null) 1 else 0 + + override fun onBindViewHolder(holder: PlayingAudioHolder, position: Int) { + playItem?.let { + holder.playTitle.text = it.name + holder.playArtist.text = it.artist + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 311f3cb..c3be349 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,42 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + + + + +