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..02a7102 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ 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/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /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..3ec5dfe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,7 +36,6 @@ android { } dependencies { - 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..8dd65ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,12 @@ + + + + + + + + + + + + @@ -21,6 +33,10 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/ListData.kt b/app/src/main/java/com/example/bcsd_android_2025_1/ListData.kt new file mode 100644 index 0000000..8e523da --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/ListData.kt @@ -0,0 +1,6 @@ +package com.example.bcsd_android_2025_1 + +import android.net.Uri + +data class ListData(val musicUri: Uri, val albumImgUri: Long, val title:String, val name:String, val time:Long) + diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/LocalChangeReceiver.kt b/app/src/main/java/com/example/bcsd_android_2025_1/LocalChangeReceiver.kt new file mode 100644 index 0000000..3ee2dcf --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/LocalChangeReceiver.kt @@ -0,0 +1,16 @@ +package com.example.bcsd_android_2025_1 + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class LocalChangeReceiver: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val title = intent?.getStringExtra(MusicService.TITLE_KEY) ?: R.string.default_title + + val toMainIntent = Intent(MainActivity.MAIN_RECEIVER_ACTION) + toMainIntent.putExtra(MusicService.TITLE_KEY, title) + toMainIntent.setPackage("com.example.bcsd_android_2025_1") + context?.sendBroadcast(toMainIntent) + } +} \ 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..2484e9a 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,252 @@ package com.example.bcsd_android_2025_1 +import android.Manifest +import android.content.BroadcastReceiver +import android.content.ContentUris +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.MediaStore +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.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import android.widget.Toast class MainActivity : AppCompatActivity() { + companion object { + const val BATTERY_LEVEL = "level" + const val BATTERY_SCALE = "scale" + const val MAIN_RECEIVER_ACTION = "action" + } + + private lateinit var recyclerView: RecyclerView + private lateinit var permissionTextView: TextView + private lateinit var permissionButton: Button + private lateinit var serviceIntent : Intent + private lateinit var playingMusicTextView: TextView + + private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + when(isGranted) { + true->{ + recyclerView.visibility = View.VISIBLE + permissionButton.visibility = View.GONE + permissionTextView.visibility = View.GONE + loadMusics() + } + else->{ + when(shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) + { + true->permissionDialog(true) + else->{ + permissionDialog(false) + } + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + + serviceIntent = Intent(this,MusicService::class.java)/////// + + recyclerView = findViewById(R.id.recycler_view) + permissionTextView = findViewById(R.id.need_permission_textview) + permissionButton = findViewById(R.id.need_permission_button) + playingMusicTextView = findViewById(R.id.playing_music_textview) + + recyclerView.visibility = View.GONE + permissionButton.visibility = View.GONE + permissionTextView.visibility = View.GONE + + requestPermissionLaunch() + + permissionButton.setOnClickListener{ + val intent = Intent() + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", packageName, null) + intent.setData(uri) + startActivity(intent) + } + } + + private val mainReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val title = intent?.getStringExtra(MusicService.TITLE_KEY) ?: R.string.default_title + playingMusicTextView.text = getString(R.string.playing_music_view, title) + } + } + + override fun onResume() { + super.onResume() + + val batteryFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + registerReceiver(batteryCheckReceiver, batteryFilter) + + val intentfilter = IntentFilter(MAIN_RECEIVER_ACTION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(mainReceiver, intentfilter, RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(mainReceiver, intentfilter) + } + + if(permissionGranted()){ + recyclerView.visibility = View.VISIBLE + permissionButton.visibility = View.GONE + permissionTextView.visibility = View.GONE + loadMusics() + }else{ + recyclerView.visibility = View.GONE + permissionButton.visibility = View.VISIBLE + permissionTextView.visibility = View.VISIBLE + } + } + + override fun onPause() { + super.onPause() + unregisterReceiver(batteryCheckReceiver) + unregisterReceiver(mainReceiver) + } + + private fun permissionGranted():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 requestPermissionLaunch(){ + val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_AUDIO + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + + if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + recyclerView.visibility = View.VISIBLE + permissionButton.visibility = View.GONE + permissionTextView.visibility = View.GONE + loadMusics() + } else { + requestPermission.launch(permission) + } } -} \ No newline at end of file + + private fun permissionDialog(requestDialog : Boolean){ + val builder = androidx.appcompat.app.AlertDialog.Builder(this) + builder.setTitle(R.string.permission_title_dialog) + builder.setMessage(R.string.permission_message_dialog) + builder.setPositiveButton(R.string.ok){dialog, _-> + dialog.dismiss() + if(!requestDialog){ + val intent = Intent() + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", packageName, null) + intent.setData(uri) + startActivity(intent) + } + } + builder.setNegativeButton(R.string.no){dialog, _-> + dialog.dismiss() + } + builder.show() + } + + + private fun loadMusics(){ + val list = ArrayList() + + val projection = arrayOf( + MediaStore.Audio.Media.TITLE, + MediaStore.Audio.Media.ARTIST, + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.ALBUM_ID) + + contentResolver.query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + projection, + null, + null, + null + )?.use{ cursor-> + val idIndex = cursor.getColumnIndex(MediaStore.Audio.Media._ID) + val titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE) + val nameIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST) + val timeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION) + val albumIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID) + while (cursor.moveToNext()){ + val id = cursor.getLong(idIndex) + val title = cursor.getString(titleIndex)?:getString(R.string.default_title) + val name = cursor.getString(nameIndex)?:getString(R.string.default_name) + val time = cursor.getLong(timeIndex) + val albumId = cursor.getLong(albumIdIndex) + val musicUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id) + + list.add(ListData(musicUri, albumId, title, name, time)) + } + } + + recyclerView.adapter = RecyclerViewAdapter(list, object:RecyclerViewAdapter.OnItemClickListener{ + override fun itemClick(item:ListData){ + playMusic(item) + } + }) + } + + private fun playMusic(item:ListData) { + serviceIntent.putExtra(MusicService.MUSIC_URI_KEY, item.musicUri.toString()) + serviceIntent.putExtra(MusicService.MUSIC_TITLE_KEY, item.title) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + } + + private val batteryCheckReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val level = intent?.getIntExtra(BATTERY_LEVEL, -1) ?: -1 + val scale = intent?.getIntExtra(BATTERY_SCALE, -1) ?: -1 + + if (level != -1 && scale != -1) { + val batteryPercent = (level / scale.toFloat()) * 100 + + if (batteryPercent <= 20f) { + Toast.makeText(context, R.string.battery_alert_message, Toast.LENGTH_LONG).show() + } + } + } + } + + fun serviceStart(view : View){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } + else{ + startService(serviceIntent) + } + } + + fun serviceStop(view : View){ + stopService(serviceIntent) + } +} + + + + diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt new file mode 100644 index 0000000..48ad754 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt @@ -0,0 +1,92 @@ +package com.example.bcsd_android_2025_1 + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo +import android.media.MediaPlayer +import android.net.Uri +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat + +class MusicService : Service() { + companion object{ + const val CHANNEL_ID = "notification_music" + const val CHANNEL_NAME = "music_channel" + const val NOTIFICATION_ID = 1111 + const val TITLE_KEY = "title" + const val MUSIC_URI_KEY = "musicUri" + const val MUSIC_TITLE_KEY = "musicTitle" + const val TITLE_INTENT_ACTION = "MUSIC_PLAYING" + } + + private var mediaPlayer: MediaPlayer? = null + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + createNotificationChannel() + + val musicUriString = intent?.getStringExtra(MUSIC_URI_KEY) + val title = intent?.getStringExtra(MUSIC_TITLE_KEY) ?: getString(R.string.default_title) + + if (musicUriString != null) { + val uri = Uri.parse(musicUriString) + + mediaPlayer?.release() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, buildNotification(title), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) + } else { + startForeground(NOTIFICATION_ID, buildNotification(title)) + } + + mediaPlayer = MediaPlayer().apply { + setOnPreparedListener { + it.start() + } + setDataSource(applicationContext, uri) + prepareAsync() + } + + val intentTitle = Intent(this, LocalChangeReceiver::class.java) + intentTitle.action = TITLE_INTENT_ACTION + intentTitle.putExtra(TITLE_KEY, title) + sendBroadcast(intentTitle) + } + + return START_STICKY + } + + private fun buildNotification(title: String): Notification { + val intent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.notification_playing_music)) + .setContentText(title) + .setSmallIcon(android.R.drawable.ic_media_play) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setAutoCancel(false) + .build() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH) + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + } + } + + override fun onDestroy() { + super.onDestroy() + mediaPlayer?.release() + mediaPlayer = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/RecyclerViewAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/RecyclerViewAdapter.kt new file mode 100644 index 0000000..f58e4db --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/RecyclerViewAdapter.kt @@ -0,0 +1,72 @@ +package com.example.bcsd_android_2025_1 + +import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView + +class RecyclerViewAdapter(private val items:MutableList, private val itemClickListener: OnItemClickListener): RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflatedView = LayoutInflater.from(parent.context).inflate(R.layout.item_recyclerview, parent, false) + return ViewHolder(inflatedView) + } + + interface OnItemClickListener{ + fun itemClick(item:ListData) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + holder.bind(item, position) + } + + override fun getItemCount(): Int = items.size + + inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + fun bind(item: ListData, position: Int) { + val imageItem: ImageView = view.findViewById(R.id.imageview_item) + val titleItem: TextView = view.findViewById(R.id.title_textview_item) + val nameItem: TextView = view.findViewById(R.id.name_textview_item) + val timeItem: TextView = view.findViewById(R.id.time_textview_item) + titleItem.text = item.title + nameItem.text = item.name + timeItem.text = item.time.timeSet() + + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(view.context, item.musicUri) + + val art = retriever.embeddedPicture + if (art != null) { + val bitmap = BitmapFactory.decodeByteArray(art, 0, art.size) + imageItem.setImageBitmap(bitmap) + } else { + imageItem.setImageResource(R.drawable.album_image_default_aespa) + } + retriever.release() + } catch (e: Exception) { + e.printStackTrace() + imageItem.setImageResource(R.drawable.album_image_default_aespa) + } + + view.setOnClickListener{ + itemClickListener.itemClick(item) + } + } + } + + private fun Long.timeSet():String{ + val hour = this/(1000*60*60) + val min = (this/(1000*60))%60 + val sec = (this/1000)%60 + return if(hour>0){ + "%d:%02d:%02d".format(hour, min, sec) + }else{ + "%02d:%02d".format(min,sec) + } + } +} diff --git a/app/src/main/res/drawable/album_image_default_aespa.png b/app/src/main/res/drawable/album_image_default_aespa.png new file mode 100644 index 0000000..4eaeb47 Binary files /dev/null and b/app/src/main/res/drawable/album_image_default_aespa.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 311f3cb..b11fae5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,53 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + + app:layout_constraintTop_toTopOf="parent"/> + +