diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2a4bbd8..0c9f587 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,4 +45,6 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + val fragment_version = "1.8.9" + implementation("androidx.fragment:fragment-ktx:$fragment_version") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 398b755..f3d93d4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,10 @@ - + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/android_25_2/MainActivity.kt b/app/src/main/java/com/example/android_25_2/MainActivity.kt index aed359b..c4e6875 100644 --- a/app/src/main/java/com/example/android_25_2/MainActivity.kt +++ b/app/src/main/java/com/example/android_25_2/MainActivity.kt @@ -1,20 +1,184 @@ package com.example.android_25_2 +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build import android.os.Bundle -import androidx.activity.enableEdgeToEdge +import android.os.IBinder +import android.provider.MediaStore +import android.provider.Settings +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView class MainActivity : AppCompatActivity() { + private var isBound = false + private var musicService: MusicService? = null + private lateinit var player: ConstraintLayout + private lateinit var tvTitle: TextView + private lateinit var tvArtist: TextView + + private val serviceConnection = object : ServiceConnection{ + override fun onServiceConnected(name: ComponentName?, service: IBinder?){ + val binder = service as MusicService.MusicBinder + musicService = binder.getService() + isBound = true + + musicService?.setOnMusicChangeListener { music -> + updatePlayer(music) + } + updatePlayer(musicService?.currentMusic) + } + + override fun onServiceDisconnected(p0: ComponentName?) { + musicService = null + isBound = false + } + } + fun updatePlayer(music: MusicItem?) { + if (music != null) { + player.visibility = ConstraintLayout.VISIBLE + tvTitle.text = music.displayName + tvArtist.text = music.artist + } else { + player.visibility = ConstraintLayout.GONE + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() setContentView(R.layout.activity_main) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } + + player = findViewById(R.id.player) + tvTitle = findViewById(R.id.player_title) + tvArtist = findViewById(R.id.player_artist) + + permissionCheck() + createNotificationChannel() + + val musicList = mutableListOf() + + val projection = arrayOf( + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media.ARTIST, + MediaStore.Audio.Media._ID + ) + + applicationContext.contentResolver.query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + projection, + "", + arrayOf(), + "" + )?.use { cursor -> + val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME) + val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION) + val artistsColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST) + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) + while (cursor.moveToNext()){ + val displayName = cursor.getString(displayNameColumn) + val duration = cursor.getInt(durationColumn) + val artists = cursor.getString(artistsColumn) + val id = cursor.getString(idColumn) + val uri = Uri.withAppendedPath( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + id.toString() + ) + musicList.add(MusicItem(displayName, duration, artists, uri)) + } + } + + val recyclerView = findViewById(R.id.recyclerView) + val musicAdapter = MusicAdapter(musicList) { music -> + playMusicWithService(music) + } + recyclerView.layoutManager = LinearLayoutManager(this) + recyclerView.adapter = musicAdapter + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ + val channel = NotificationChannel( + "music_channel", + "Music Playback", + NotificationManager.IMPORTANCE_LOW + ) + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + private fun playMusicWithService(music: MusicItem) { + val intent = Intent(this, MusicService::class.java).apply { + action = "ACTION_PLAY" + putExtra("MUSIC_TITLE", music.displayName) + putExtra("MUSIC_ARTIST", music.artist) + putExtra("MUSIC_URI", music.uri.toString()) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + bindService(Intent(this, MusicService::class.java), serviceConnection, Context.BIND_AUTO_CREATE) + } + + private fun permissionCheck() { + val mediaPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_AUDIO + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + if (ContextCompat.checkSelfPermission(this, mediaPermission) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, arrayOf(mediaPermission), 1000 + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED){ + ActivityCompat.requestPermissions( + this, arrayOf(Manifest.permission.POST_NOTIFICATIONS + ), 1000 + ) + } + } + + if(!ActivityCompat.shouldShowRequestPermissionRationale(this, mediaPermission)) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:${packageName}")) + startActivity(intent) + } + if(!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:${packageName}")) + startActivity(intent) + } + } + } + override fun onDestroy() { + super.onDestroy() + if (isBound) { + unbindService(serviceConnection) + isBound = false + } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/android_25_2/MusicAdapter.kt b/app/src/main/java/com/example/android_25_2/MusicAdapter.kt new file mode 100644 index 0000000..c1c8bdc --- /dev/null +++ b/app/src/main/java/com/example/android_25_2/MusicAdapter.kt @@ -0,0 +1,49 @@ +package com.example.android_25_2 + +import android.content.Context +import android.media.MediaPlayer +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.constraintlayout.motion.widget.MotionScene.Transition.TransitionOnClick +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder + +class MusicAdapter(private val musicList: List, private val onClick: (MusicItem) -> Unit): RecyclerView.Adapter(){ + private var mediaPlayer: MediaPlayer? = null + + class MusicViewHolder(view: View) : RecyclerView.ViewHolder(view){ + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item, parent, false) + + return MusicViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val music = musicList[position] + val displayName: TextView = holder.itemView.findViewById(R.id.text_display_name) + val artists: TextView = holder.itemView.findViewById(R.id.text_artist) + val duration: TextView = holder.itemView.findViewById(R.id.text_duration) + displayName.text = music.displayName + artists.text = music.artist + duration.text = music.duration.toString() + + holder.itemView.setOnClickListener{ + mediaPlayer?.stop() + mediaPlayer?.release() + onClick(music) + + mediaPlayer = MediaPlayer().apply { + setDataSource(holder.itemView.context, music.uri) + prepare() + start() + } + } + } + + override fun getItemCount() = musicList.size +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android_25_2/MusicItem.kt b/app/src/main/java/com/example/android_25_2/MusicItem.kt new file mode 100644 index 0000000..a129658 --- /dev/null +++ b/app/src/main/java/com/example/android_25_2/MusicItem.kt @@ -0,0 +1,10 @@ +package com.example.android_25_2 + +import android.net.Uri + +data class MusicItem ( + val displayName: String, + val duration: Int, + val artist: String, + val uri: Uri +) \ No newline at end of file diff --git a/app/src/main/java/com/example/android_25_2/MusicService.kt b/app/src/main/java/com/example/android_25_2/MusicService.kt new file mode 100644 index 0000000..3313ecb --- /dev/null +++ b/app/src/main/java/com/example/android_25_2/MusicService.kt @@ -0,0 +1,114 @@ +package com.example.android_25_2 + +import android.app.Notification +import android.app.PendingIntent +import android.app.Service +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(){ + private var mediaPlayer: MediaPlayer? = null + var currentMusic: MusicItem? = null + private val binder = MusicBinder() + private var musicChangeListener: ((MusicItem?) -> Unit)? = null + + inner class MusicBinder : Binder() { + fun getService(): MusicService = this@MusicService + } + + fun setOnMusicChangeListener(listener: (MusicItem?) -> Unit) { + musicChangeListener = listener + } + + override fun onStartCommand(intent: Intent?, flags: Int, startID: Int): Int{ + when (intent?.action) { + "ACTION_PLAY" -> { + val title = intent.getStringExtra("MUSIC_TITLE")?: "" + val artist = intent.getStringExtra("MUSIC_ARTIST")?: "" + val uriString = intent.getStringExtra("MUSIC_URI")?: "" + val uri = Uri.parse(uriString) + + val music = MusicItem(title, 0, artist, uri) + playMusic(music) + } + "ACTION_STOP" -> { + stopMusic() + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } + } + return START_STICKY + } + + private fun playMusic(music: MusicItem) { + try { + mediaPlayer?.stop() + mediaPlayer?.release() + + mediaPlayer = MediaPlayer().apply { + setDataSource(applicationContext, music.uri) + prepare() + start() + } + + currentMusic = music + musicChangeListener?.invoke(music) + startForeground(1, createNotification(music)) + + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun stopMusic() { + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + currentMusic = null + musicChangeListener?.invoke(null) + } + + private fun createNotification(music: MusicItem): Notification { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + + val stopIntent = Intent(this, MusicService::class.java).apply { + action = "ACTION_STOP" + } + val stopPendingIntent = PendingIntent.getService( + this, + 1, + stopIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, "music_channel") + .setContentTitle(music.displayName) + .setContentText(music.artist) + .setSmallIcon(android.R.drawable.ic_media_play) + .setContentIntent(pendingIntent) + .addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent) + .setOngoing(true) + .build() + } + + override fun onBind(intent: Intent?): IBinder { + return binder + } + + override fun onDestroy() { + super.onDestroy() + stopMusic() + } +} \ 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 9affce0..254902b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,4 +7,49 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item.xml b/app/src/main/res/layout/item.xml new file mode 100644 index 0000000..4e691c8 --- /dev/null +++ b/app/src/main/res/layout/item.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index a92bfca..3167c98 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -2,12 +2,12 @@