first commit

This commit is contained in:
ocean 2024-04-16 18:14:11 +08:00
commit c0b1731e01
166 changed files with 5497 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
.idea/.gitignore
.idea/compiler.xml
.idea/deploymentTargetDropDown.xml
.idea/gradle.xml
.idea/kotlinc.xml
.idea/migrations.xml
.idea/misc.xml
.idea/vcs.xml

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

68
app/build.gradle.kts Normal file
View File

@ -0,0 +1,68 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
}
android {
namespace = "com.player.musicoo"
compileSdk = 34
defaultConfig {
applicationId = "com.player.musicoo"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
setProperty("archivesBaseName", "Musicoo_${defaultConfig.versionName}(${defaultConfig.versionCode})")
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.media3:media3-session:1.3.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-runtime:2.6.1")
//noinspection KaptUsageInsteadOfKsp
kapt("androidx.room:room-compiler:2.6.1")
implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
implementation("com.github.lihangleo2:ShadowLayout:3.4.0")
implementation("androidx.media3:media3-exoplayer:1.3.1")
implementation("androidx.media3:media3-ui:1.3.1")
implementation("androidx.media3:media3-common:1.3.1")
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.player.musicoo
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.player.musicoo", appContext.packageName)
}
}

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="LockedOrientationActivity">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/musicoo_logo_img"
android:label="@string/app_name"
android:roundIcon="@mipmap/musicoo_logo_img"
android:supportsRtl="true"
android:theme="@style/Theme.Musicoo"
tools:targetApi="31">
<activity
android:name=".activity.LaunchActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.PlayDetailsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.SettingsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.AboutActivity"
android:screenOrientation="portrait" />
<service android:name=".service.AudioPlayerService" />
<service
android:name=".service.PlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,99 @@
{
"categories": [
{
"name": "Real human voice",
"audios": [
{
"name": "Breathe",
"file": "Real human voice/Breathe.mp3",
"image": "Real human voice pic/Breathe.png"
},
{
"name": "Shh Shh",
"file": "Real human voice/Shh Shh.mp3",
"image": "Real human voice pic/Shh Shh.png"
},
{
"name": "Shhh...",
"file": "Real human voice/Shhh….mp3",
"image": "Real human voice pic/Shhh….png"
}
]
},
{
"name": "Sounds of appliances",
"audios": [
{
"name": "Fireplace",
"file": "Sounds of appliances/Fireplace.mp3",
"image": "Sounds of appliances pic/Fireplace.png"
},
{
"name": "Mountain stream",
"file": "Sounds of appliances/Mountain stream.mp3",
"image": "Sounds of appliances pic/Mountain stream.png"
},
{
"name": "TV",
"file": "Sounds of appliances/TV.mp3",
"image": "Sounds of appliances pic/TV.png"
},
{
"name": "Water droplet",
"file": "Sounds of appliances/Water droplet.mp3",
"image": "Sounds of appliances pic/Water droplet.png"
}
]
},
{
"name": "Sounds of nature",
"audios": [
{
"name": "Beach",
"file": "Sounds of nature/Beach.mp3",
"image": "Sounds of nature pic/Beach.png"
},
{
"name": "Call of Seagulls",
"file": "Sounds of nature/Call of Seagulls.mp3",
"image": "Sounds of nature pic/Call of Seagulls.png"
},
{
"name": "Chirping of Birds",
"file": "Sounds of nature/Chirping of Birds.mp3",
"image": "Sounds of nature pic/Chirping of Birds.png"
},
{
"name": "Cicada Chirping",
"file": "Sounds of nature/Cicada Chirping.mp3",
"image": "Sounds of nature pic/Cicada Chirping.png"
},
{
"name": "Howling Wind",
"file": "Sounds of nature/Howling Wind.mp3",
"image": "Sounds of nature pic/Howling Wind.png"
},
{
"name": "Nocturnal Insects",
"file": "Sounds of nature/Nocturnal Insects.mp3",
"image": "Sounds of nature pic/Nocturnal Insects.png"
},
{
"name": "Seawater Surging",
"file": "Sounds of nature/Seawater Surging.mp3",
"image": "Sounds of nature pic/Seawater Surging.png"
},
{
"name": "Summer Insects",
"file": "Sounds of nature/Summer Insects.mp3",
"image": "Sounds of nature pic/Summer Insects.png"
},
{
"name": "waterfall",
"file": "Sounds of nature/waterfall.mp3",
"image": "Sounds of nature pic/waterfall.png"
}
]
}
]
}

View File

@ -0,0 +1,111 @@
package com.player.musicoo
import android.app.Application
import android.content.Context
import android.util.Log
import com.player.musicoo.bean.Audio
import com.player.musicoo.bean.CurrentPlayingAudio
import com.player.musicoo.bean.ResourcesList
import com.player.musicoo.database.AppDatabase
import com.player.musicoo.database.CurrentAudioDatabase
import com.player.musicoo.database.CurrentAudioManager
import com.player.musicoo.database.DatabaseManager
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.util.parseResources
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
class App : Application() {
companion object {
lateinit var app: App
private set
lateinit var currentAudioManager: CurrentAudioManager
private set
lateinit var databaseManager: DatabaseManager
private set
var currentPlayingAudio: CurrentPlayingAudio? = null
private set
lateinit var importList: List<Audio>
private set
lateinit var resourcesList: ResourcesList
private set
lateinit var realHumanVoiceList: List<Audio>
private set
lateinit var soundsOfAppliancesList: List<Audio>
private set
lateinit var soundsOfNatureList: List<Audio>
private set
private var isInitialized = false
fun initialize(context: Context) {
if (!isInitialized) {
val jsonString = readAssetsFile(context)
resourcesList = parseResources(context, jsonString)
splitResourcesList()
isInitialized = true
}
}
fun initCurrentPlayingAudio() {
CoroutineScope(Dispatchers.IO).launch {
currentPlayingAudio = currentAudioManager.getCurrentPlayingAudio()
}
}
fun initImportAudio(callback: (List<Audio>) -> Unit = {}) {
CoroutineScope(Dispatchers.IO).launch {
importList = databaseManager.getAllAudioFiles()
withContext(Dispatchers.Main) {
Log.d("ocean", "initImportAudio importList->${importList.size}")
callback(importList)
}
}
}
private fun readAssetsFile(context: Context): String {
val assetManager = context.assets
val inputStream = assetManager.open("resources.json")
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
val stringBuilder = StringBuilder()
var line: String?
while (bufferedReader.readLine().also { line = it } != null) {
stringBuilder.append(line)
}
return stringBuilder.toString()
}
private fun splitResourcesList() {
realHumanVoiceList = mutableListOf()
soundsOfAppliancesList = mutableListOf()
soundsOfNatureList = mutableListOf()
for (category in resourcesList.categories) {
when (category.name) {
"Real human voice" -> realHumanVoiceList = category.audios
"Sounds of appliances" -> soundsOfAppliancesList = category.audios
"Sounds of nature" -> soundsOfNatureList = category.audios
}
}
}
}
override fun onCreate() {
super.onCreate()
app = this
initialize(this)
MediaControllerManager.init(this)
currentAudioManager = CurrentAudioManager.getInstance(this)
databaseManager = DatabaseManager.getInstance(this)
initCurrentPlayingAudio()
initImportAudio()
}
}

View File

@ -0,0 +1,26 @@
package com.player.musicoo.activity
import android.os.Bundle
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.databinding.ActivityAboutBinding
import com.player.musicoo.util.getAppVersion
class AboutActivity : BaseActivity() {
private lateinit var binding: ActivityAboutBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
binding.versionTv.text = "Version: " + getAppVersion(this)
binding.backBtn.setOnClickListener {
finish()
}
}
}

View File

@ -0,0 +1,18 @@
package com.player.musicoo.activity
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.player.musicoo.media.MediaControllerManager
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
}
}

View File

@ -0,0 +1,47 @@
package com.player.musicoo.activity
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.databinding.ActivityLaunchBinding
class LaunchActivity : BaseActivity() {
private lateinit var binding: ActivityLaunchBinding
private val totalTime = 3000 // 5秒
private val interval = 50 // 更新间隔,毫秒
private val steps = totalTime / interval
private val progressPerStep = 100 / steps
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLaunchBinding.inflate(layoutInflater)
setContentView(binding.root)
initTimer()
immersionBar {
fullScreen(true)
statusBarDarkFont(false)
}
}
private fun initTimer() {
val progressBar = binding.customProgressBar
val timer = object : CountDownTimer(totalTime.toLong(), interval.toLong()) {
override fun onTick(millisUntilFinished: Long) {
progressBar.setProgress(progressBar.getProgress() + progressPerStep)
}
override fun onFinish() {
progressBar.setProgress(100)
toMainActivity()
}
}
timer.start()
}
private fun toMainActivity() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View File

@ -0,0 +1,256 @@
package com.player.musicoo.activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.media3.common.Player
import com.bumptech.glide.Glide
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.ActivityMainBinding
import com.player.musicoo.fragment.HomeFragment
import com.player.musicoo.fragment.ImportFragment
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class MainActivity : BaseActivity() {
private lateinit var binding: ActivityMainBinding
private val mFragments: MutableList<Fragment> = ArrayList()
private var currentIndex: Int = 0
private var mCurrentFragment: Fragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// initImmersionBar()
initView()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private fun initView() {
initClick()
initFragment()
}
override fun onResume() {
super.onResume()
val currentPlayer = MediaControllerManager.getController()
if (App.currentPlayingAudio == null) {
binding.playingStatusLayout.visibility = View.GONE
} else {
binding.playingStatusLayout.visibility = View.VISIBLE
val currentAudio = App.currentPlayingAudio
Log.d("ocean","main currentAudio->$currentAudio")
val maxProgress = try {
getAudioDurationFromAssets(this, currentAudio?.file!!)
} catch (e: Exception) {
currentAudio?.duration
}
if (maxProgress != null) {
binding.progressBar.setMaxProgress(maxProgress)
}
if (currentAudio?.image?.isNotEmpty() == true) {
Glide.with(this)
.load("file:///android_asset/${currentAudio?.image}")
.into(binding.audioImg)
} else {
binding.audioImg.setImageResource(R.mipmap.musicoo_logo_img)
}
binding.name.text = currentAudio?.name
binding.desc.text = currentAudio?.name
}
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY) {
val isPlaying = currentPlayer.isPlaying
updatePlayState(isPlaying)
if (isPlaying) {
updateProgressState()
}
}
}
private fun initClick() {
binding.homeBtn.setOnClickListener {
changeFragment(0)
updateBtnState(0)
}
binding.importBtn.setOnClickListener {
changeFragment(1)
updateBtnState(1)
}
binding.playingStatusLayout.setOnClickListener {
val currentAudio = App.currentPlayingAudio
val duration = try {
getAudioDurationFromAssets(
this, currentAudio?.file!!
)
} catch (e: Exception) {
currentAudio?.duration!!
}
val audio = Audio(
currentAudio?.name!!,
currentAudio.file,
currentAudio.image,
duration,
false
)
val intent = Intent(this, PlayDetailsActivity::class.java);
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
startActivity(intent)
}
binding.alarmClockBtn.setOnClickListener {
}
binding.playBlackBtn.setOnClickListener {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null) {
Log.d("ocean", "currentPlayer.playbackState->${currentPlayer.playbackState}")
if (currentPlayer.playbackState == Player.STATE_READY) {
if (currentPlayer.isPlaying) {
currentPlayer.pause()
updatePlayState(false)
} else {
currentPlayer.play()
updatePlayState(true)
}
updateProgressState()
} else {
MediaControllerManager.setupMedia(this@MainActivity, App.currentPlayingAudio!!,
object : Player.Listener {
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean,
reason: Int
) {
Log.d("ocean", "main2 onPlayWhenReadyChanged")
updatePlayState(playWhenReady)
updateProgressState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
Log.d("ocean", "main2 onPlaybackStateChanged")
updateProgressState()
}
})
}
} else {
Log.d("ocean", "main currentPlayer == null")
}
}
}
private fun updatePlayState(b: Boolean) {
if (b) {
binding.playStatusImg.setImageResource(R.drawable.playing_black_icon)
} else {
binding.playStatusImg.setImageResource(R.drawable.play_black_icon)
}
}
private fun initFragment() {
mFragments.clear()
mFragments.add(HomeFragment())
mFragments.add(ImportFragment())
changeFragment(0)
updateBtnState(0)
}
private fun changeFragment(index: Int) {
currentIndex = index
val ft: FragmentTransaction = supportFragmentManager.beginTransaction()
if (null != mCurrentFragment) {
ft.hide(mCurrentFragment!!)
}
var fragment = supportFragmentManager.findFragmentByTag(
mFragments[currentIndex].javaClass.name
)
if (null == fragment) {
fragment = mFragments[index]
}
mCurrentFragment = fragment
if (!fragment.isAdded) {
ft.add(R.id.frame_layout, fragment, fragment.javaClass.name)
} else {
ft.show(fragment)
}
ft.commit()
}
private fun updateBtnState(index: Int) {
binding.apply {
homeImg.setImageResource(
when (index) {
0 -> R.drawable.home_select_icon
else -> R.drawable.home_unselect_icon
}
)
importImg.setImageResource(
when (index) {
1 -> R.drawable.import_select_icon
else -> R.drawable.import_unselect_icon
}
)
}
}
private fun updateProgressState() {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
progressHandler.removeCallbacksAndMessages(null)
updatePlayState(currentPlayer.isPlaying)
progressHandler.sendEmptyMessage(1)
} else {
progressHandler.removeCallbacksAndMessages(null)
}
}
private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
val currentPosition = currentPlayer.currentPosition
binding.progressBar.setProgress(currentPosition)
sendEmptyMessageDelayed(1, 1000)
}
}
}
private var backPressedTime: Long = 0
private val backToast: Toast by lazy {
Toast.makeText(baseContext, "Press again to exit", Toast.LENGTH_SHORT)
}
override fun onBackPressed() {
if (backPressedTime + 2000 > System.currentTimeMillis()) {
super.onBackPressed()
backToast.cancel()
return
} else {
backToast.show()
}
backPressedTime = System.currentTimeMillis()
}
}

View File

@ -0,0 +1,267 @@
package com.player.musicoo.activity
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import android.util.Log
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.google.android.material.slider.Slider.OnChangeListener
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.R
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.ActivityPlayDetailsBinding
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.util.containsContent
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
import java.io.IOException
import java.io.InputStream
class PlayDetailsActivity : BaseActivity() {
companion object {
const val KEY_DETAILS_AUDIO = "key_details_audio"
}
private lateinit var binding: ActivityPlayDetailsBinding
private var audio: Audio? = null
private var rotationAnimator: ValueAnimator? = null
@SuppressLint("ForegroundServiceType")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPlayDetailsBinding.inflate(layoutInflater)
setContentView(binding.root)
initImmersionBar()
audio = intent.getSerializableExtra(KEY_DETAILS_AUDIO) as Audio?
if (audio == null) {
onBackPressed()
Toast.makeText(this, getString(R.string.data_error), Toast.LENGTH_SHORT).show()
}
Log.d("ocean", "PlayDetailsActivity audio->$audio")
initView()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
@OptIn(UnstableApi::class)
private fun initView() {
if (audio?.image?.isNotEmpty() == true) {
Glide.with(this)
.load("file:///android_asset/${audio?.image}")
.into(binding.image)
val bitmap = loadBitmapFromAsset(this, audio?.image!!)
val blurredBitmap = applyGaussianBlur(bitmap, 25f, this)
binding.imageView.setImageBitmap(blurredBitmap)
} else {
binding.image.setImageResource(R.mipmap.musicoo_logo_img)
val bitmap = loadBitmapFromAsset(R.mipmap.musicoo_logo_img)
val blurredBitmap = applyGaussianBlur(bitmap, 25f, this)
binding.imageView.setImageBitmap(blurredBitmap)
}
binding.seekBar.value = 0f
binding.title.text = ""
binding.nameTv.text = audio?.name
binding.descTv.text = audio?.name
if (containsContent(audio?.file!!)) {
binding.totalDurationTv.text = convertMillisToMinutesAndSecondsString(audio?.duration!!)
binding.seekBar.valueTo = audio?.duration!!.toFloat()
} else {
val s = getAudioDurationFromAssets(this, audio?.file!!)
binding.totalDurationTv.text = convertMillisToMinutesAndSecondsString(s)
binding.seekBar.valueTo = s.toFloat()
}
binding.backBtn.setOnClickListener {
onBackPressed()
}
val currentPlayer = MediaControllerManager.getController()
currentPlayer?.addListener(object : Player.Listener {
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean,
reason: Int
) {
Log.d("ocean", "details onPlayWhenReadyChanged")
updatePlayState(playWhenReady, "playWhenReady")
updateProgressState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
Log.d("ocean", "details onPlaybackStateChanged")
updateProgressState()
}
})
binding.playImg.setOnClickListener {
if (currentPlayer != null) {
if (currentPlayer.isPlaying) {
currentPlayer.pause()
updatePlayState(false, "click")
} else {
currentPlayer.play()
updatePlayState(true, "click")
}
updateProgressState()
}
}
MediaControllerManager.setupMedia(this,
audio!!,
object : Player.Listener {
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean,
reason: Int
) {
updatePlayState(playWhenReady, "playWhenReady")
updateProgressState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
updateProgressState()
}
})
binding.seekBar.addOnChangeListener(OnChangeListener { slider, value, fromUser ->
if (fromUser) {
if (currentPlayer != null) {
currentPlayer.seekTo(value.toLong())
val ss = currentPlayer.isPlaying
if (!ss) {
currentPlayer.play()
}
}
}
})
}
override fun onResume() {
super.onResume()
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY) {
val isPlaying = currentPlayer.isPlaying
updatePlayState(isPlaying, "onResume")
if (isPlaying) {
updateProgressState()
} else {
val currentPosition = currentPlayer.currentPosition
val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
binding.progressDurationTv.text = currentString
binding.seekBar.value = currentPosition.toFloat()
}
}
}
private fun updatePlayState(b: Boolean, string: String) {
if (b) {
binding.playImg.setImageResource(R.drawable.playing_green_icon)
} else {
binding.playImg.setImageResource(R.drawable.play_green_icon)
}
}
private fun loadBitmapFromAsset(context: Context, filePath: String): Bitmap {
return try {
val inputStream = context.assets.open(filePath)
BitmapFactory.decodeStream(inputStream)
} catch (e: IOException) {
e.printStackTrace()
throw RuntimeException("Could not load bitmap from asset")
}
}
private fun loadBitmapFromAsset(id: Int): Bitmap {
return try {
val inputStream: InputStream = resources.openRawResource(id)
BitmapFactory.decodeStream(inputStream)
} catch (e: IOException) {
e.printStackTrace()
throw RuntimeException("Could not load bitmap from asset")
}
}
private fun applyGaussianBlur(inputBitmap: Bitmap, radius: Float, context: Context): Bitmap {
val rsContext = RenderScript.create(context)
val outputBitmap =
Bitmap.createBitmap(inputBitmap.width, inputBitmap.height, inputBitmap.config)
val blurScript = ScriptIntrinsicBlur.create(rsContext, Element.U8_4(rsContext))
val tmpIn = Allocation.createFromBitmap(rsContext, inputBitmap)
val tmpOut = Allocation.createFromBitmap(rsContext, outputBitmap)
blurScript.setRadius(radius)
blurScript.setInput(tmpIn)
blurScript.forEach(tmpOut)
tmpOut.copyTo(outputBitmap)
rsContext.finish()
return outputBitmap
}
override fun onPause() {
super.onPause()
finishWithAnimation()
}
override fun onBackPressed() {
super.onBackPressed()
}
private fun finishWithAnimation() {
overridePendingTransition(R.anim.no_animation, R.anim.slide_down)
Handler(Looper.getMainLooper()).postDelayed({
finish()
}, 500)
}
override fun onDestroy() {
super.onDestroy()
progressHandler.removeCallbacksAndMessages(null)
rotationAnimator?.cancel()
}
private fun updateProgressState() {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
updatePlayState(currentPlayer.isPlaying, "playWhenReady")
progressHandler.removeCallbacksAndMessages(null)
progressHandler.sendEmptyMessage(1)
} else {
progressHandler.removeCallbacksAndMessages(null)
}
}
private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
val currentPosition = currentPlayer.currentPosition
val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
binding.progressDurationTv.text = currentString
binding.seekBar.value = currentPosition.toFloat()
sendEmptyMessageDelayed(1, 1000)
}
}
}
}

View File

@ -0,0 +1,55 @@
package com.player.musicoo.activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.R
import com.player.musicoo.databinding.ActivitySettingsBinding
import com.player.musicoo.util.PRIVACY_POLICY_URL
import com.player.musicoo.util.TERMS_OF_SERVICE_URL
import com.player.musicoo.util.openPrivacyPolicy
import com.player.musicoo.util.openTermsOfService
import com.player.musicoo.util.sendFeedback
import com.player.musicoo.util.shareApp
class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
initImmersionBar()
initView()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private fun initView() {
binding.backBtn.setOnClickListener {
finish()
}
binding.aboutBtn.setOnClickListener {
startActivity(Intent(this, AboutActivity::class.java))
}
binding.feedbackBtn.setOnClickListener {
sendFeedback(this, "motaleb3024@gmail.com", getString(R.string.app_name))
}
binding.shareBtn.setOnClickListener {
shareApp(this)
}
binding.ppBtn.setOnClickListener {
openPrivacyPolicy(this, PRIVACY_POLICY_URL)
}
binding.tosBtn.setOnClickListener {
openTermsOfService(this, TERMS_OF_SERVICE_URL)
}
}
}

View File

@ -0,0 +1,81 @@
package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.ParentsVoiceLayoutBinding
import com.player.musicoo.util.containsContent
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class ParentsVoiceAdapter(
private val context: Context,
private val pdfList: List<Audio>,
) :
RecyclerView.Adapter<ParentsVoiceAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ParentsVoiceLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val audio = pdfList[position]
holder.bind(audio)
holder.itemView.setOnClickListener {
val intent = Intent(context, PlayDetailsActivity::class.java);
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
context.startActivity(intent)
}
}
override fun getItemCount(): Int = pdfList.size
inner class ViewHolder(private val binding: ParentsVoiceLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(audio: Audio) {
binding.apply {
image.setImageResource(R.mipmap.musicoo_logo_img)
name.text = audio.name
name.requestFocus()
if (containsContent(audio.file)) {
desc.text = convertMillisToMinutesAndSecondsString(audio.duration)
} else {
val s = getAudioDurationFromAssets(context, audio.file)
desc.text = convertMillisToMinutesAndSecondsString(s)
}
if (App.currentPlayingAudio != null) {
if (App.currentPlayingAudio?.file == audio.file) {
playingLayout.visibility = View.VISIBLE
name.setTextColor(context.getColor(R.color.green))
desc.setTextColor(context.getColor(R.color.green))
} else {
playingLayout.visibility = View.GONE
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white))
}
}
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,77 @@
package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.RealHumanVoiceLayoutBinding
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class RealHumanVoiceAdapter(
private val context: Context,
private val pdfList: List<Audio>,
) :
RecyclerView.Adapter<RealHumanVoiceAdapter.PDFViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PDFViewHolder {
val binding =
RealHumanVoiceLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
return PDFViewHolder(binding)
}
override fun onBindViewHolder(holder: PDFViewHolder, position: Int) {
val audio = pdfList[position]
holder.bind(audio)
holder.itemView.setOnClickListener {
val intent = Intent(context, PlayDetailsActivity::class.java);
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
context.startActivity(intent)
}
}
override fun getItemCount(): Int = pdfList.size
inner class PDFViewHolder(private val binding: RealHumanVoiceLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(audio: Audio) {
binding.apply {
Glide.with(context)
.load("file:///android_asset/${audio.image}")
.into(realImg)
name.text = audio.name
val s = getAudioDurationFromAssets(context, audio.file)
desc.text = convertMillisToMinutesAndSecondsString(s)
if (App.currentPlayingAudio != null) {
if (App.currentPlayingAudio?.file == audio.file) {
stateImg.setImageResource(R.drawable.playing_white_icon)
name.setTextColor(context.getColor(R.color.green))
desc.setTextColor(context.getColor(R.color.green))
} else {
stateImg.setImageResource(R.drawable.play_white_icon)
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white))
}
}
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,79 @@
package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class SoundsOfAppliancesAdapter(
private val context: Context,
private val pdfList: List<Audio>,
) :
RecyclerView.Adapter<SoundsOfAppliancesAdapter.PDFViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PDFViewHolder {
val binding =
SoundsOfAppliancesLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
return PDFViewHolder(binding)
}
override fun onBindViewHolder(holder: PDFViewHolder, position: Int) {
val audio = pdfList[position]
holder.bind(audio)
holder.itemView.setOnClickListener {
val intent = Intent(context, PlayDetailsActivity::class.java);
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
context.startActivity(intent)
// mediaPlayer.setDataSource(this, Uri.parse("file:///android_asset/${audio.file}"))
}
}
override fun getItemCount(): Int = pdfList.size
inner class PDFViewHolder(private val binding: SoundsOfAppliancesLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(audio: Audio) {
binding.apply {
Glide.with(context)
.load("file:///android_asset/${audio.image}")
.into(image)
name.text = audio.name
val s = getAudioDurationFromAssets(context, audio.file)
desc.text = convertMillisToMinutesAndSecondsString(s)
if (App.currentPlayingAudio != null) {
if (App.currentPlayingAudio?.file == audio.file) {
playingLayout.visibility = View.VISIBLE
name.setTextColor(context.getColor(R.color.green))
desc.setTextColor(context.getColor(R.color.green))
} else {
playingLayout.visibility = View.GONE
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white))
}
}
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,80 @@
package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class SoundsOfNatureAdapter(
private val context: Context,
private val pdfList: List<Audio>,
) :
RecyclerView.Adapter<SoundsOfNatureAdapter.PDFViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PDFViewHolder {
val binding =
SoundsOfNatureLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
return PDFViewHolder(binding)
}
override fun onBindViewHolder(holder: PDFViewHolder, position: Int) {
val audio = pdfList[position]
holder.bind(audio)
holder.itemView.setOnClickListener {
val intent = Intent(context, PlayDetailsActivity::class.java);
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
context.startActivity(intent)
// mediaPlayer.setDataSource(this, Uri.parse("file:///android_asset/${audio.file}"))
}
}
override fun getItemCount(): Int = pdfList.size
inner class PDFViewHolder(private val binding: SoundsOfNatureLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(audio: Audio) {
binding.apply {
Glide.with(context)
.load("file:///android_asset/${audio.image}")
.into(image)
name.text = audio.name
val s = getAudioDurationFromAssets(context, audio.file)
desc.text = convertMillisToMinutesAndSecondsString(s)
if (App.currentPlayingAudio != null) {
if (App.currentPlayingAudio?.file == audio.file) {
playingLayout.visibility = View.VISIBLE
name.setTextColor(context.getColor(R.color.green))
desc.setTextColor(context.getColor(R.color.green))
} else {
playingLayout.visibility = View.GONE
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white))
}
}
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,20 @@
package com.player.musicoo.bean
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Keep
@Entity
data class Audio(
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "file") var file: String,
@ColumnInfo(name = "image") var image: String,
@ColumnInfo(name = "duration") var duration: Long,
@ColumnInfo(name = "selected") var selected: Boolean
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,8 @@
package com.player.musicoo.bean
import java.io.Serializable
data class Category(
val name: String,
val audios: List<Audio>
) : Serializable

View File

@ -0,0 +1,15 @@
package com.player.musicoo.bean
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "current_playing_audio")
data class CurrentPlayingAudio(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "file") val file: String,
@ColumnInfo(name = "image") val image: String,
@ColumnInfo(name = "duration") var duration: Long,
@ColumnInfo(name = "selected") val selected: Boolean
)

View File

@ -0,0 +1,7 @@
package com.player.musicoo.bean
import java.io.Serializable
data class ResourcesList(
val categories: List<Category>
) : Serializable

View File

@ -0,0 +1,11 @@
package com.player.musicoo.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.player.musicoo.bean.Audio
@Database(entities = [Audio::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun localAudioDao(): LocalAudioDao
}

View File

@ -0,0 +1,11 @@
package com.player.musicoo.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.player.musicoo.bean.CurrentPlayingAudio
@Database(entities = [CurrentPlayingAudio::class], version = 1, exportSchema = false)
abstract class CurrentAudioDatabase : RoomDatabase() {
abstract fun currentPlayingAudioDao(): CurrentPlayingAudioDao
}

View File

@ -0,0 +1,39 @@
package com.player.musicoo.database
import android.content.Context
import androidx.room.Room
import com.player.musicoo.bean.CurrentPlayingAudio
class CurrentAudioManager private constructor(context: Context) {
private val database: CurrentAudioDatabase = Room.databaseBuilder(
context.applicationContext,
CurrentAudioDatabase::class.java, "current_audio_app_database"
).build()
private val currentPlayingAudioDao: CurrentPlayingAudioDao = database.currentPlayingAudioDao()
suspend fun getCurrentPlayingAudio(): CurrentPlayingAudio? {
return currentPlayingAudioDao.getCurrentPlayingAudio()
}
suspend fun setCurrentPlayingAudio(audio: CurrentPlayingAudio) {
val currentAudio = getCurrentPlayingAudio()
if (currentAudio == null) {
currentPlayingAudioDao.insertCurrentPlayingAudio(audio)
} else {
// 如果已有数据,先删除现有数据,然后再插入新数据
currentPlayingAudioDao.deleteCurrentPlayingAudio(currentAudio)
currentPlayingAudioDao.insertCurrentPlayingAudio(audio)
}
}
companion object {
@Volatile private var instance: CurrentAudioManager? = null
fun getInstance(context: Context): CurrentAudioManager {
return instance ?: synchronized(this) {
instance ?: CurrentAudioManager(context).also { instance = it }
}
}
}
}

View File

@ -0,0 +1,24 @@
package com.player.musicoo.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.player.musicoo.bean.CurrentPlayingAudio
@Dao
interface CurrentPlayingAudioDao {
@Query("SELECT * FROM current_playing_audio LIMIT 1")
suspend fun getCurrentPlayingAudio(): CurrentPlayingAudio?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCurrentPlayingAudio(audio: CurrentPlayingAudio)
@Update
suspend fun updateCurrentPlayingAudio(audio: CurrentPlayingAudio)
@Delete
suspend fun deleteCurrentPlayingAudio(audio: CurrentPlayingAudio)
}

View File

@ -0,0 +1,84 @@
package com.player.musicoo.database
import android.content.Context
import androidx.room.Room
import com.player.musicoo.bean.Audio
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DatabaseManager private constructor(context: Context) {
private val database = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java, "local_audio_viewer_database"
).build()
private val audioFileDao = database.localAudioDao()
suspend fun insertAudioFile(audio: Audio) {
withContext(Dispatchers.IO) {
val existingAudioFile = getAudioFileByPath(audio.name)
if (existingAudioFile == null) {
audioFileDao.insertAudioFile(audio)
} else {
audioFileDao.updateAudioFile(audio)
}
}
}
suspend fun insertAudioFiles(audios: List<Audio>) {
withContext(Dispatchers.IO) {
for (audio in audios) {
val existingAudioFile = getAudioFileByPath(audio.file)
if (existingAudioFile == null) {
audioFileDao.insertAudioFile(audio)
} else {
audioFileDao.updateAudioFile(audio)
}
}
}
}
suspend fun getAllAudioFiles(): List<Audio> {
return withContext(Dispatchers.IO) {
audioFileDao.getAllAudioFile()
}
}
suspend fun deleteAudioFile(audioFile: Audio) {
withContext(Dispatchers.IO) {
audioFileDao.deleteAudioFile(audioFile)
}
}
suspend fun deleteAllAudioFiles() {
withContext(Dispatchers.IO) {
audioFileDao.deleteAllAudioFile()
}
}
suspend fun updateAudioFiles(audioFile: Audio) {
withContext(Dispatchers.IO) {
audioFileDao.updateAudioFile(audioFile)
}
}
suspend fun getAudioFileByPath(path: String): Audio? {
return audioFileDao.getAudioFileByPath(path)
}
suspend fun getAudioBySelect(): List<Audio> {
return audioFileDao.getAudioBySelected()
}
companion object {
@Volatile
private var instance: DatabaseManager? = null
fun getInstance(context: Context): DatabaseManager {
return instance ?: synchronized(this) {
instance ?: DatabaseManager(context).also { instance = it }
}
}
}
}

View File

@ -0,0 +1,34 @@
package com.player.musicoo.database
import androidx.room.*
import com.player.musicoo.bean.Audio
@Dao
interface LocalAudioDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAudioFile(barcode: Audio)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAudioFiles(audios: List<Audio>)
@Query("SELECT * FROM Audio")
suspend fun getAllAudioFile(): List<Audio>
@Delete
suspend fun deleteAudioFile(barcode: Audio)
@Query("DELETE FROM Audio")
suspend fun deleteAllAudioFile()
@Update
suspend fun updateAudioFile(audioFile: Audio)
@Query("SELECT * FROM Audio WHERE name = :path LIMIT 1")
suspend fun getAudioFileByPath(path: String): Audio?
@Query("SELECT * FROM Audio WHERE selected = 1")
suspend fun getAudioBySelected(): List<Audio>
}

View File

@ -0,0 +1,123 @@
package com.player.musicoo.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.adapter.RealHumanVoiceAdapter
import com.player.musicoo.adapter.SoundsOfAppliancesAdapter
import com.player.musicoo.adapter.SoundsOfNatureAdapter
import com.player.musicoo.databinding.FragmentHomeBinding
import com.player.musicoo.util.GridSpacingItemDecoration
import com.player.musicoo.util.HorizontalSpaceItemDecoration
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private var realHumanVoiceAdapter: RealHumanVoiceAdapter? = null
private var soundsOfAppliancesAdapter: SoundsOfAppliancesAdapter? = null
private var soundsOfNatureAdapter: SoundsOfNatureAdapter? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private fun initView() {
if (App.resourcesList.categories.isNotEmpty()) {
binding.soundsName.text = App.resourcesList.categories[1].name
binding.natureName.text = App.resourcesList.categories[2].name
}
if (App.realHumanVoiceList.isNotEmpty()) {
realHumanVoiceAdapter = RealHumanVoiceAdapter(requireActivity(), App.realHumanVoiceList)
binding.realRv.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false)
binding.realRv.addItemDecoration(HorizontalSpaceItemDecoration(requireActivity()))
binding.realRv.adapter = realHumanVoiceAdapter
}
if (App.soundsOfAppliancesList.isNotEmpty()) {
soundsOfAppliancesAdapter =
SoundsOfAppliancesAdapter(requireActivity(), App.soundsOfAppliancesList)
binding.soundsRv.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false)
binding.soundsRv.addItemDecoration(HorizontalSpaceItemDecoration(requireActivity()))
binding.soundsRv.adapter = soundsOfAppliancesAdapter
}
if (App.soundsOfNatureList.isNotEmpty()) {
soundsOfNatureAdapter =
SoundsOfNatureAdapter(requireActivity(), App.soundsOfNatureList)
binding.natureRv.layoutManager =
GridLayoutManager(requireActivity(), 3, GridLayoutManager.HORIZONTAL, false)
binding.natureRv.addItemDecoration(GridSpacingItemDecoration(requireActivity(), 10, 3))
binding.natureRv.adapter = soundsOfNatureAdapter
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onResume() {
super.onResume()
if (App.currentPlayingAudio != null) {
if (App.realHumanVoiceList.isNotEmpty()) {
for ((index, audio) in App.realHumanVoiceList.withIndex()) {
if (audio.file == App.currentPlayingAudio?.file) {
notifyDataSetChanged()
break
}
}
}
if (App.soundsOfAppliancesList.isNotEmpty()) {
for ((index, audio) in App.soundsOfAppliancesList.withIndex()) {
if (audio.file == App.currentPlayingAudio?.file) {
notifyDataSetChanged()
break
}
}
}
if (App.soundsOfNatureList.isNotEmpty()) {
for ((index, audio) in App.soundsOfNatureList.withIndex()) {
if (audio.file == App.currentPlayingAudio?.file) {
notifyDataSetChanged()
break
}
}
}
}
initImmersionBar()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if(!hidden){
initImmersionBar()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun notifyDataSetChanged(){
soundsOfAppliancesAdapter?.notifyDataSetChanged()
realHumanVoiceAdapter?.notifyDataSetChanged()
soundsOfNatureAdapter?.notifyDataSetChanged()
}
}

View File

@ -0,0 +1,236 @@
package com.player.musicoo.fragment
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.SettingsActivity
import com.player.musicoo.adapter.ParentsVoiceAdapter
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.FragmentImportBinding
import com.player.musicoo.util.uriToFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ImportFragment : Fragment() {
private lateinit var binding: FragmentImportBinding
private var parentsVoiceAdapter: ParentsVoiceAdapter? = null
private var importAdapterList: MutableList<Audio> = mutableListOf()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentImportBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
private fun initView() {
binding.settingBtn.setOnClickListener {
startActivity(Intent(requireActivity(), SettingsActivity::class.java))
}
binding.addBtn.setOnClickListener {
binding.addBtn.visibility = View.GONE
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
openAudioPicker()
}
}
importAdapterList.clear()
importAdapterList.addAll(App.importList)
parentsVoiceAdapter = ParentsVoiceAdapter(requireActivity(), importAdapterList)
binding.importRv.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false)
binding.importRv.adapter = parentsVoiceAdapter
}
private var requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
openAudioPicker()
} else {
showExplanationDialog()
}
}
private fun openAudioPicker() {
binding.loadingLayout.visibility = View.VISIBLE
getMusicFiles(requireActivity())
}
private fun showExplanationDialog() {
AlertDialog.Builder(requireActivity())
.setTitle(getString(R.string.permission_request))
.setMessage(getString(R.string.permission_request_desc))
.setPositiveButton(getString(R.string.ok)) { dialog, which ->
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
dialog.dismiss()
}
.setNegativeButton(getString(R.string.cancel)) { dialog, which ->
dialog.dismiss()
}
.show()
}
@SuppressLint("NotifyDataSetChanged")
override fun onResume() {
super.onResume()
initImmersionBar()
if (importAdapterList.isNotEmpty()) {
binding.noContentLayout.visibility = View.GONE
} else {
binding.noContentLayout.visibility = View.VISIBLE
}
if (App.currentPlayingAudio != null) {
if (App.importList.isNotEmpty()) {
importAdapterList.clear()
importAdapterList.addAll(App.importList)
for ((index, audio) in importAdapterList.withIndex()) {
if (audio.file == App.currentPlayingAudio?.file) {
parentsVoiceAdapter?.notifyDataSetChanged()
break
}
}
}
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
initImmersionBar()
}
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private fun getAudioDuration(uri: Uri): Long {
var duration = 0L
try {
val mediaPlayer = MediaPlayer.create(requireContext(), uri)
duration = mediaPlayer.duration.toLong()
mediaPlayer.release()
return duration
} catch (e: Exception) {
e.printStackTrace()
}
return duration
}
@SuppressLint("NotifyDataSetChanged")
fun getMusicFiles(context: Context): List<String> {
val musicFiles = mutableListOf<String>()
val contentResolver: ContentResolver = context.contentResolver
// 定义查询参数
val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DATA
)
val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0"
val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
// 查询音频文件
val cursor = contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
null,
sortOrder
)
cursor?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val data = cursor.getString(dataColumn)
val contentUri = ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
val duration2 = getAudioDuration(contentUri)
val audio = Audio(name, contentUri.toString(), "", duration2, false)
musicFiles.add(contentUri.toString())
// 如果你想要获取文件的具体路径,可以使用 data 变量
// musicFiles.add(data)
Log.d(
"ocean",
"name->${name} " +
"uri.toString()->${contentUri.toString()} " +
"duration2->$duration2 "
+ "data->$data"
)
CoroutineScope(Dispatchers.IO).launch {
if (audio.duration > 0) {
App.databaseManager.insertAudioFile(audio)
}
withContext(Dispatchers.Main) {
App.initImportAudio {
importAdapterList.clear()
importAdapterList.addAll(App.importList)
parentsVoiceAdapter?.notifyDataSetChanged()
if (importAdapterList.isNotEmpty()) {
binding.noContentLayout.visibility = View.GONE
} else {
binding.noContentLayout.visibility = View.VISIBLE
}
binding.loadingLayout.visibility = View.GONE
binding.addBtn.visibility = View.VISIBLE
}
}
}
}
}
return musicFiles
}
}

View File

@ -0,0 +1,50 @@
package com.player.musicoo.media
import android.content.Context
import android.os.Bundle
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaNotification
import androidx.media3.session.MediaSession
import com.google.common.collect.ImmutableList
import com.player.musicoo.R
@UnstableApi
class MyMediaNotificationProvider(val context: Context) : MediaNotification.Provider {
companion object {
private const val CHANNEL_ID = "musicoo_notification_channel"
private const val NOTIFICATION_ID = 1231
}
override fun createNotification(
mediaSession: MediaSession,
customLayout: ImmutableList<CommandButton>,
actionFactory: MediaNotification.ActionFactory,
onNotificationChangedCallback: MediaNotification.Provider.Callback
): MediaNotification {
val customLayoutRes = R.layout.my_notification_layout
val customLayoutView = RemoteViews(context.packageName, customLayoutRes)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle("Custom Notification Title")
.setContentText("Custom Notification Text")
.setSmallIcon(R.mipmap.musicoo_logo_img)
.setCustomContentView(customLayoutView)
.build()
return MediaNotification(NOTIFICATION_ID, notification)
}
override fun handleCustomCommand(
session: MediaSession,
action: String,
extras: Bundle
): Boolean {
// 处理自定义命令
return false
}
}

View File

@ -0,0 +1,182 @@
package com.player.musicoo.media
import android.content.ComponentName
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.bean.Audio
import com.player.musicoo.bean.CurrentPlayingAudio
import com.player.musicoo.service.PlaybackService
import com.player.musicoo.util.containsContent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
object MediaControllerManager {
private var mediaController: MediaController? = null
private var controllerFuture: ListenableFuture<MediaController>? = null
private var currentAudioFile = ""
fun init(context: Context) {
val sessionToken =
SessionToken(context, ComponentName(context, PlaybackService::class.java))
controllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
controllerFuture?.addListener({
mediaController = controllerFuture?.get()
Log.d("ocean", "MediaControllerManager init")
Log.d("ocean", "MediaController connected: ${mediaController?.isConnected}")
}, MoreExecutors.directExecutor())
}
fun getController(): MediaController? {
return if (mediaController != null && mediaController!!.isConnected) {
mediaController
} else {
null
}
}
fun setupMedia(context: Context, audio: Audio, listener: Player.Listener) {
if (currentAudioFile != audio.file) {
currentAudioFile = audio.file
val uri: Uri? = if (containsContent(audio.file)) {
Uri.parse(audio.file)
} else {
Uri.parse("file:///android_asset/$currentAudioFile")
}
Log.d("ocean","uri->$uri")
val resourceId = R.mipmap.musicoo_logo_img
val imgUri: Uri? = if (audio.image.isNotEmpty()) {
Uri.parse("file:///android_asset/${audio.image}")
} else {
Uri.parse("android.resource://${context.packageName}/$resourceId")
}
val mediaItem =
MediaItem.Builder()
.setUri(uri)
.setMediaMetadata(
MediaMetadata.Builder()
.setArtist(audio.name)
.setTitle(audio.name)
.setArtworkUri(imgUri)
.build()
)
.build()
if (isConnected()) {
mediaController?.let {
it.addListener(listener)
it.setMediaItem(mediaItem)
it.repeatMode = Player.REPEAT_MODE_ONE
it.prepare()
it.play()
val currentPlayingAudio =
CurrentPlayingAudio(
audio.id,
audio.name,
audio.file,
audio.image,
audio.duration,
false
)
CoroutineScope(Dispatchers.IO).launch {
App.currentAudioManager.setCurrentPlayingAudio(currentPlayingAudio)
withContext(Dispatchers.Main) {
App.initCurrentPlayingAudio()//更新到入口变量中
}
}
}
}
}
}
fun setupMedia(context: Context, audio: CurrentPlayingAudio, listener: Player.Listener) {
if (currentAudioFile != audio.file) {
currentAudioFile = audio.file
val uri: Uri? = if (containsContent(audio.file)) {
Uri.parse(audio.file)
} else {
Uri.parse("file:///android_asset/$currentAudioFile")
}
Log.d("ocean","uri->$uri")
val resourceId = R.mipmap.musicoo_logo_img
val imgUri: Uri? = if (audio.image.isNotEmpty()) {
Uri.parse("file:///android_asset/${audio.image}")
} else {
Uri.parse("android.resource://${context.packageName}/$resourceId")
}
// val uri = Uri.parse("file:///android_asset/$currentAudioFile")
//// val mediaItem = MediaItem.fromUri(uri)
// val imgUri = Uri.parse("file:///android_asset/${audio.image}")
val mediaItem =
MediaItem.Builder()
.setUri(uri)
.setMediaMetadata(
MediaMetadata.Builder()
.setArtist(audio.name)
.setTitle(audio.name)
.setArtworkUri(imgUri)
.build()
)
.build()
if (isConnected()) {
mediaController?.let {
it.addListener(listener)
it.setMediaItem(mediaItem)
it.repeatMode = Player.REPEAT_MODE_ONE
it.prepare()
it.play()
val currentPlayingAudio =
CurrentPlayingAudio(
audio.id,
audio.name,
audio.file,
audio.image,
audio.duration,
false
)
CoroutineScope(Dispatchers.IO).launch {
App.currentAudioManager.setCurrentPlayingAudio(currentPlayingAudio)
withContext(Dispatchers.Main) {
App.initCurrentPlayingAudio()//更新到入口变量中
}
}
}
}
}
}
fun isConnected(): Boolean {
mediaController?.let {
return it.isConnected
}
return false
}
fun isPlaying(): Boolean {
mediaController?.let {
return it.isPlaying
}
return false
}
}

View File

@ -0,0 +1,94 @@
package com.player.musicoo.service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.player.musicoo.R
import com.player.musicoo.activity.MainActivity
class AudioPlayerService : Service() {
private var mediaPlayer: MediaPlayer? = null
private val binder = AudioPlayerBinder()
inner class AudioPlayerBinder : Binder() {
fun getService(): AudioPlayerService = this@AudioPlayerService
}
override fun onBind(intent: Intent?): IBinder? {
return binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_NOT_STICKY
}
fun playAudio(audioUri: String) {
mediaPlayer?.let {
it.stop()
it.reset() // 重置 MediaPlayer确保处于空闲状态
it.release()
}
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
val assetFileDescriptor = assets.openFd(audioUri)
setDataSource(
assetFileDescriptor.fileDescriptor,
assetFileDescriptor.startOffset,
assetFileDescriptor.length
)
prepareAsync()
setOnPreparedListener { start() }
isLooping = true // 开启重复播放
}
startForegroundService()
}
fun pauseAudio() {
mediaPlayer?.pause()
}
override fun onDestroy() {
mediaPlayer?.release()
super.onDestroy()
}
private fun startForegroundService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "audio_player_channel"
val channelName = "Audio Player"
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(channel)
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(this, channelId)
.setContentTitle("正在播放音频")
.setContentText("点击以返回应用")
.setSmallIcon(R.mipmap.musicoo_logo_img)
.setContentIntent(pendingIntent)
.build()
startForeground(1, notification)
}
}
}

View File

@ -0,0 +1,62 @@
package com.player.musicoo.service
import android.content.Intent
import android.os.Bundle
import androidx.media3.common.Player
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import com.player.musicoo.R
@UnstableApi
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
// Create your player and media session in the onCreate lifecycle event
override fun onCreate() {
super.onCreate()
val player = ExoPlayer.Builder(this).build()
mediaSession = MediaSession.Builder(this, player)
.build()
// setMediaNotificationProvider(MyMediaNotificationProvider(this))
}
// The user dismissed the app from the recent tasks
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaSession?.player!!
if (!player.playWhenReady
|| player.mediaItemCount == 0
|| player.playbackState == Player.STATE_ENDED
) {
// Stop the service if not playing, continue playing in the background
// otherwise.
stopSelf()
}
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
mediaSession
// Remember to release the player and media session in onDestroy
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
}

View File

@ -0,0 +1,66 @@
package com.player.musicoo.sp
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SharedPreferencesHelper(context: Context) {
companion object {
const val CURRENT_PLAYING_AUDIO = "current_playing_audio"
}
private val preferences: SharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
var currentPlayingAudio: Boolean by preferences.boolean(
key = CURRENT_PLAYING_AUDIO,
defaultValue = false
)
private inline fun <reified T : Any> SharedPreferences.boolean(
key: String,
defaultValue: T
): ReadWriteProperty<Any, T> {
return object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return when (T::class) {
Boolean::class -> getBoolean(key, defaultValue as Boolean) as T
else -> throw IllegalArgumentException("Unsupported type")
}
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
edit {
when (T::class) {
Boolean::class -> putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unsupported type")
}
}
}
}
}
private inline fun <reified T : Any> SharedPreferences.string(
key: String,
defaultValue: T
): ReadWriteProperty<Any, T> {
return object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return when (T::class) {
String::class -> getString(key, defaultValue as String) as T
else -> throw IllegalArgumentException("Unsupported type")
}
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
edit {
when (T::class) {
String::class -> putString(key, value as String)
else -> throw IllegalArgumentException("Unsupported type")
}
}
}
}
}
}

View File

@ -0,0 +1,71 @@
package com.player.musicoo.util
import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
class AudioPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
init {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setOnCompletionListener {
// 在播放完成时重置 MediaPlayer
mediaPlayer?.seekTo(0)
mediaPlayer?.start()
}
}
}
fun playAudio(fileName: String) {
try {
val assetFileDescriptor = context.assets.openFd(fileName)
mediaPlayer?.apply {
reset()
setDataSource(
assetFileDescriptor.fileDescriptor,
assetFileDescriptor.startOffset,
assetFileDescriptor.length
)
prepare()
start()
isLooping = true // 开启重复播放
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun pauseAudio() {
mediaPlayer?.pause()
}
fun resumeAudio() {
mediaPlayer?.start()
}
fun stopAudio() {
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
}
fun currentPosition(): Int {
return mediaPlayer?.currentPosition!!
}
fun seekTo(position: Int) {
mediaPlayer?.seekTo(position)
}
fun isPlaying(): Boolean {
return mediaPlayer?.isPlaying!!
}
}

View File

@ -0,0 +1,21 @@
package com.player.musicoo.util
import android.content.Context
import android.content.pm.PackageManager
fun convertMillisToMinutesAndSecondsString(millis: Long): String {
val totalSeconds = millis / 1000
val minutes = (totalSeconds / 60).toInt()
val seconds = (totalSeconds % 60).toInt()
return String.format("%02d:%02d", minutes, seconds)
}
fun getAppVersion(context: Context): String {
return try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
"N/A"
}
}

View File

@ -0,0 +1,29 @@
package com.player.musicoo.util
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class GridSpacingItemDecoration(
private val context: Context,
private val spacing: Int,
private val spanCount: Int
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view)
val column = position % spanCount
if (spanCount > 1) {
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing
}
}
}

View File

@ -0,0 +1,31 @@
package com.player.musicoo.util
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class HorizontalSpaceItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
private val defaultSpacing: Int
private val lastItemSpacing: Int
init {
val density = context.resources.displayMetrics.density
defaultSpacing = (8 * density).toInt()
lastItemSpacing = (16 * density).toInt()
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
val itemCount = parent.adapter?.itemCount ?: 0
// 设置间隔除了最后一个item外其余item的右边都有间隔
if (position < itemCount - 1) {
outRect.right = defaultSpacing
} else {
outRect.right = lastItemSpacing
}
}
}

View File

@ -0,0 +1,44 @@
package com.player.musicoo.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import com.player.musicoo.R
const val PRIVACY_POLICY_URL = "https://musicoo.app/privacy"
const val TERMS_OF_SERVICE_URL = "https://musicoo.app/terms"
fun openPrivacyPolicy(context: Context, privacyPolicyUrl: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(privacyPolicyUrl))
context.startActivity(intent)
}
fun openTermsOfService(context: Context, termsOfServiceUrl: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(termsOfServiceUrl))
context.startActivity(intent)
}
fun shareApp(context: Context) {
val appPackageName = context.packageName
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
val appPlayStoreLink = "https://play.google.com/store/apps/details?id=$appPackageName"
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Check out this app: $appName")
shareIntent.putExtra(Intent.EXTRA_TEXT, "Download $appName from Google Play: $appPlayStoreLink")
context.startActivity(Intent.createChooser(shareIntent, "Share $appName via"))
}
fun sendFeedback(context: Context, email: String, subject: String) {
val emailIntent = Intent(Intent.ACTION_SEND)
emailIntent.type = "message/rfc822"
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
try {
context.startActivity(Intent.createChooser(emailIntent, "Send Feedback"))
} catch (ex: android.content.ActivityNotFoundException) {
Toast.makeText(context,"There is no app that supports sending emails",Toast.LENGTH_LONG).show()
}
}

View File

@ -0,0 +1,91 @@
package com.player.musicoo.util
import android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.media.MediaMetadataRetriever
import android.media.MediaPlayer
import android.net.Uri
import android.provider.MediaStore
import com.player.musicoo.bean.Audio
import com.player.musicoo.bean.Category
import com.player.musicoo.bean.ResourcesList
import org.json.JSONObject
import java.io.File
import java.io.InputStream
fun parseResources(context: Context, jsonString: String): ResourcesList {
val jsonObject = JSONObject(jsonString)
val categoriesArray = jsonObject.getJSONArray("categories")
val categories = mutableListOf<Category>()
for (i in 0 until categoriesArray.length()) {
val categoryObject = categoriesArray.getJSONObject(i)
val categoryName = categoryObject.getString("name")
val audiosArray = categoryObject.getJSONArray("audios")
val audios = mutableListOf<Audio>()
for (j in 0 until audiosArray.length()) {
val audioObject = audiosArray.getJSONObject(j)
val audioName = audioObject.getString("name")
val audioFile = audioObject.getString("file")
val audioImage = audioObject.getString("image")
val audio = Audio(
audioName,
audioFile,
audioImage,
getAudioDurationFromAssets(context, audioFile),
false
)
audios.add(audio)
}
val category = Category(categoryName, audios)
categories.add(category)
}
return ResourcesList(categories)
}
fun getAudioDurationFromAssets(context: Context, fileName: String): Long {
val assetFileDescriptor = context.assets.openFd(fileName)
val retriever = MediaMetadataRetriever()
retriever.setDataSource(
assetFileDescriptor.fileDescriptor,
assetFileDescriptor.startOffset,
assetFileDescriptor.length
)
val durationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
val duration = durationString?.toLong() ?: 0
retriever.release()
assetFileDescriptor.close()
return duration
}
fun containsContent(str: String): Boolean {
return str.contains("content://")
}
fun getInputStreamFromUri(context: Context, uri: Uri): InputStream? {
return try {
context.contentResolver.openInputStream(uri)
} catch (e: Exception) {
null
}
}
fun uriToFile(context: Context, uri: Uri): File? {
val contentResolver = context.contentResolver
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val filePathColumn = it.getColumnIndex(MediaStore.Images.Media.DATA)
val filePath = it.getString(filePathColumn)
return File(filePath)
}
}
return null
}

View File

@ -0,0 +1,90 @@
package com.player.musicoo.view
import android.content.Context
import android.graphics.*
import android.renderscript.*
import android.util.AttributeSet
import android.widget.FrameLayout
class BlurLayout : FrameLayout {
private var blurRadius = 25f // 默认模糊半径
private val cornerRadius = dpToPx(18) // 圆角半径
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun dispatchDraw(canvas: Canvas) {
// 创建一个 Bitmap 用于绘制原始内容
val originalBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val originalCanvas = Canvas(originalBitmap)
// 调用父类的 dispatchDraw() 方法,把所有的子 View 绘制到这个 Bitmap 上
super.dispatchDraw(originalCanvas)
// 创建一个 Bitmap 用于绘制模糊效果
val blurredBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
// 使用 RenderScript 处理模糊效果
val rsContext = RenderScript.create(context)
val input = Allocation.createFromBitmap(rsContext, originalBitmap)
val output = Allocation.createTyped(
rsContext,
Type.createXY(
rsContext,
Element.RGBA_8888(rsContext),
originalBitmap.width,
originalBitmap.height
)
)
val script = ScriptIntrinsicBlur.create(rsContext, Element.U8_4(rsContext))
script.setInput(input)
script.setRadius(blurRadius)
script.forEach(output)
output.copyTo(blurredBitmap)
// 绘制原始内容
canvas.drawBitmap(originalBitmap, 0f, 0f, null)
// 创建一个画笔,用于绘制模糊区域
val paint = Paint()
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
// 定义模糊区域,即底部的 50dp
val blurHeight = dpToPx(50).toInt()
val blurRect = Rect(0, height - blurHeight, width, height)
// 绘制模糊效果
canvas.drawBitmap(blurredBitmap, blurRect, blurRect, paint)
// 绘制一个具有指定颜色的矩形作为背景
paint.color = Color.parseColor("#99000000") // 半透明黑色
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER) // 使用 SRC_OVER 模式覆盖绘制
paint.isAntiAlias = true
val rectF = RectF(blurRect)
canvas.drawRect(blurRect,paint)
// canvas.drawRoundRect(rectF,cornerRadius, cornerRadius, paint)
// 定义整个布局的矩形区域
val layoutRect = Rect(0, 0, width, height)
val rectFAll = RectF(layoutRect)
paint.color = Color.parseColor("#00000000") // 半透明黑色
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER) // 使用 SRC_OVER 模式覆盖绘制
paint.isAntiAlias = true
canvas.drawRoundRect(rectFAll,cornerRadius, cornerRadius, paint)
input.destroy()
output.destroy()
script.destroy()
rsContext.destroy()
}
private fun dpToPx(dp: Int): Float {
val density = resources.displayMetrics.density
return (dp * density)
}
}

View File

@ -0,0 +1,73 @@
package com.player.musicoo.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import kotlin.math.min
class CircularProgressBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var progressPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
isAntiAlias = true
strokeWidth = dpToPx(3) // 设置圆环的宽度为20像素
color = 0xFFFFFFFF.toInt() // 设置圆环的颜色为白色
}
private var backgroundPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
isAntiAlias = true
strokeWidth = dpToPx(3)
color = 0x00000000
}
private var progress: Long = 0 // 进度值默认为0
private var maxProgress: Long = 100 // 最大进度值默认为100
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2f
val centerY = height / 2f
val radius = (min(width, height) - progressPaint.strokeWidth) / 2f
// 绘制圆环背景
canvas.drawCircle(centerX, centerY, radius, backgroundPaint)
// 绘制圆环进度
val startAngle = -90f
val sweepAngle = -360f * progress / maxProgress
canvas.drawArc(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius,
startAngle,
sweepAngle,
false,
progressPaint
)
}
fun setProgress(progress: Long) {
// 确保进度值在有效范围内
this.progress = progress.coerceIn(0, maxProgress)
invalidate() // 重新绘制视图
}
fun setMaxProgress(maxProgress: Long) {
// 设置最大进度值
this.maxProgress = maxProgress
// 更新当前进度,确保当前进度在有效范围内
this.progress = this.progress.coerceIn(0, maxProgress)
invalidate() // 重新绘制视图
}
private fun dpToPx(dp: Int): Float {
val density = resources.displayMetrics.density
return (dp * density)
}
}

View File

@ -0,0 +1,62 @@
package com.player.musicoo.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
class CustomProgressBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var progress = 0 // 当前进度
private val maxProgress = 100 // 最大进度
private val progressBarHeight = 20f // 进度条高度
private val cornerRadius = 10f // 圆角半径
private val backgroundColor = Color.parseColor("#26FFFFFF")
private val startColor = Color.parseColor("#FF1CC8EE") // 起始颜色
private val middleColor = Color.parseColor("#FF69FE73") // 中间颜色
private val endColor = Color.parseColor("#FFCBD64B") // 结束颜色
private val paint = Paint()
private val paintTow = Paint()
init {
paint.style = Paint.Style.FILL
paint.isAntiAlias = true
paintTow.style = Paint.Style.FILL
paintTow.isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制底色矩形
paint.shader = null // 重置着色器
paint.color = backgroundColor
val backgroundRect = RectF(0f, (height / 2 - progressBarHeight / 2), width.toFloat(), (height / 2 + progressBarHeight / 2))
canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, paint)
// 计算进度条的宽度
val progressBarWidth = (width * progress.toFloat() / maxProgress).toInt()
// 创建颜色渐变对象
val gradient = LinearGradient(0f, 0f, width.toFloat(), 0f, intArrayOf(startColor, middleColor, endColor), null, Shader.TileMode.CLAMP)
paintTow.shader = gradient
// 绘制带圆角的进度条矩形
val rect = RectF(0f, (height / 2 - progressBarHeight / 2), progressBarWidth.toFloat(), (height / 2 + progressBarHeight / 2))
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paintTow)
}
fun getProgress():Int{
return progress
}
// 设置进度
fun setProgress(progress: Int) {
this.progress = progress
invalidate() // 请求重绘
}
}

View File

@ -0,0 +1,34 @@
package com.player.musicoo.view;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
public class MarqueeTextView extends androidx.appcompat.widget.AppCompatTextView {
public MarqueeTextView(Context context) {
super(context);
initView(context);
}
public MarqueeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public MarqueeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
this.setEllipsize(TextUtils.TruncateAt.MARQUEE);
this.setSingleLine(true);
this.setMarqueeRepeatLimit(-1);
}
//最关键的部分
public boolean isFocused() {
return true;
}
}

View File

@ -0,0 +1,47 @@
package com.player.musicoo.view
import android.content.Context
import android.graphics.*
import android.renderscript.*
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.RelativeLayout
class RadiusLayout : RelativeLayout {
private val cornerRadius = dpToPx(18) // 圆角半径
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun dispatchDraw(canvas: Canvas) {
// 创建一个 Bitmap 用于绘制原始内容
val originalBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val originalCanvas = Canvas(originalBitmap)
// 调用父类的 dispatchDraw() 方法,把所有的子 View 绘制到这个 Bitmap 上
super.dispatchDraw(originalCanvas)
// 绘制原始内容
canvas.drawBitmap(originalBitmap, 0f, 0f, null)
// 创建一个画笔
val paint = Paint()
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
// 定义整个布局的矩形区域
val layoutRect = Rect(0, 0, width, height)
val rectF = RectF(layoutRect)
// 绘制具有圆角的矩形作为背景
paint.color = Color.parseColor("#CCffffff") // 半透明黑色
paint.isAntiAlias = true
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint)
}
private fun dpToPx(dp: Int): Float {
val density = resources.displayMetrics.density
return (dp * density)
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="-100%"
android:toYDelta="0%"
android:duration="500"/>
</set>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="0%"
android:toYDelta="100%"
android:duration="500"/>
</set>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="100%"
android:toYDelta="0%"
android:duration="500"/>
</set>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M0,0h20v20h-20z"/>
<path
android:pathData="M9.886,13.773L14.034,7.136L12.761,6.341L9.615,11.376L8.86,7.602C8.826,7.433 8.734,7.28 8.6,7.17C8.466,7.06 8.298,7 8.125,7H6.25V8.5H7.51L8.515,13.523C8.545,13.67 8.618,13.805 8.725,13.91C8.832,14.015 8.968,14.086 9.116,14.113C9.264,14.14 9.416,14.122 9.553,14.061C9.691,14 9.807,13.9 9.886,13.773Z"
android:fillColor="#80F988"/>
<path
android:pathData="M10,19.375C11.231,19.375 12.45,19.132 13.588,18.661C14.725,18.19 15.759,17.5 16.629,16.629C17.5,15.759 18.19,14.725 18.661,13.588C19.132,12.45 19.375,11.231 19.375,10C19.375,8.769 19.132,7.55 18.661,6.412C18.19,5.275 17.5,4.241 16.629,3.371C15.759,2.5 14.725,1.81 13.588,1.339C12.45,0.867 11.231,0.625 10,0.625C7.514,0.625 5.129,1.613 3.371,3.371C1.613,5.129 0.625,7.514 0.625,10C0.625,12.486 1.613,14.871 3.371,16.629C5.129,18.387 7.514,19.375 10,19.375ZM10,18.125C7.845,18.125 5.778,17.269 4.255,15.745C2.731,14.222 1.875,12.155 1.875,10C1.875,7.845 2.731,5.778 4.255,4.255C5.778,2.731 7.845,1.875 10,1.875C12.155,1.875 14.222,2.731 15.745,4.255C17.269,5.778 18.125,7.845 18.125,10C18.125,12.155 17.269,14.222 15.745,15.745C14.222,17.269 12.155,18.125 10,18.125Z"
android:fillColor="#80F988"/>
</group>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M12.04,2.182C12.401,2.182 12.748,2.325 13.004,2.581C13.26,2.837 13.403,3.184 13.403,3.545L13.403,10.734H21C21.361,10.734 21.708,10.878 21.964,11.133C22.22,11.389 22.364,11.736 22.364,12.098C22.364,12.459 22.22,12.806 21.964,13.062C21.708,13.318 21.361,13.461 21,13.461H13.403V21C13.403,21.362 13.259,21.708 13.003,21.964C12.748,22.22 12.401,22.364 12.039,22.364C11.677,22.364 11.331,22.22 11.075,21.964C10.819,21.708 10.675,21.362 10.675,21V13.461H3.545C3.184,13.461 2.837,13.318 2.581,13.062C2.325,12.806 2.182,12.459 2.182,12.098C2.182,11.736 2.325,11.389 2.581,11.133C2.837,10.878 3.184,10.734 3.545,10.734H10.675V3.545C10.675,3.184 10.819,2.837 11.075,2.581C11.331,2.325 11.677,2.182 12.039,2.182H12.04Z"
android:fillColor="#282C33"/>
</group>
</vector>

View File

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="34dp"
android:viewportWidth="34"
android:viewportHeight="34">
<path
android:pathData="M17,31.403C24.172,31.403 29.986,25.589 29.986,18.417C29.986,11.245 24.172,5.43 17,5.43C9.828,5.43 4.014,11.245 4.014,18.417C4.014,25.589 9.828,31.403 17,31.403Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#333333"
android:strokeColor="#333333"/>
<path
android:pathData="M16.83,10.875L16.829,18.673L22.334,24.179"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M2.833,6.375L7.792,2.833"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#333333"
android:strokeLineCap="round"/>
<path
android:pathData="M31.167,6.375L26.208,2.833"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#333333"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M15.833,7.083L10,12.917L4.167,7.083"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M12.917,15.833L7.083,10L12.917,4.167"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="58dp"
android:height="58dp" />
<solid android:color="#FF80F988" />
</shape>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="1dp"
android:color="#4DFFFFFF" />
<size
android:width="42dp"
android:height="42dp" />
<solid android:color="#26FFFFFF" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:startColor="#26000000"
android:endColor="#000000"
android:angle="270"/>
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:startColor="#26000000"
android:endColor="#8080F988"
android:angle="270"/>
</shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:topLeftRadius="24dp"
android:topRightRadius="24dp" />
<gradient android:startColor="#3A3D3B"
android:endColor="#445145"
android:angle="270"/>
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FF80F988" />
<corners android:radius="36dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="18dp"
android:bottomRightRadius="18dp" />
<solid android:color="#CC000000" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#26FFFFFF" />
<size
android:width="48dp"
android:height="48dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="1dp"
android:color="#26FFFFFF" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M16.695,3.891H3.305C2.629,3.891 2.08,4.439 2.08,5.115V15.354C2.08,16.029 2.629,16.578 3.305,16.578H16.695C17.371,16.578 17.92,16.029 17.92,15.354V5.115C17.92,4.441 17.371,3.891 16.695,3.891ZM16.518,5.293V6.383C15.158,8.682 12.67,10.106 10,10.106C7.529,10.106 5.207,8.895 3.791,6.865C3.703,6.74 3.619,6.611 3.539,6.48C3.52,6.449 3.502,6.414 3.482,6.383V5.293H16.518ZM3.482,15.176V8.693C5.166,10.473 7.518,11.508 10,11.508C12.488,11.508 14.842,10.461 16.518,8.691V15.176H3.482Z"
android:fillColor="#80F988"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:strokeWidth="1"
android:pathData="M18,18m-17.5,0a17.5,17.5 0,1 1,35 0a17.5,17.5 0,1 1,-35 0"
android:fillColor="#00000000"
android:strokeColor="#80F988"/>
<path
android:pathData="M11.342,9.793C8.231,12.053 6.676,13.183 6.065,14.852C6.016,14.986 5.972,15.122 5.933,15.259C5.446,16.969 6.04,18.797 7.228,22.455C8.416,26.112 9.011,27.94 10.41,29.037C10.522,29.125 10.637,29.209 10.756,29.288C12.231,30.28 14.154,30.28 18,30.28C21.845,30.28 23.768,30.28 25.244,29.288C25.362,29.209 25.478,29.125 25.59,29.037C26.989,27.94 27.583,26.112 28.771,22.455C29.96,18.797 30.554,16.969 30.067,15.259C30.028,15.122 29.984,14.986 29.935,14.852C29.324,13.183 27.768,12.053 24.657,9.793C21.546,7.533 19.99,6.402 18.214,6.337C18.071,6.332 17.928,6.332 17.786,6.337C16.009,6.402 14.454,7.533 11.342,9.793ZM15.666,23.913C15.183,23.913 14.792,24.304 14.792,24.788C14.792,25.271 15.183,25.663 15.666,25.663H20.333C20.816,25.663 21.208,25.271 21.208,24.788C21.208,24.304 20.816,23.913 20.333,23.913H15.666Z"
android:fillColor="#80F988"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:strokeWidth="1"
android:pathData="M18,18m-17.5,0a17.5,17.5 0,1 1,35 0a17.5,17.5 0,1 1,-35 0"
android:fillColor="#00000000"
android:strokeColor="#9C9D9D"/>
<path
android:pathData="M11.342,9.793C8.231,12.053 6.676,13.183 6.065,14.852C6.016,14.986 5.972,15.122 5.933,15.259C5.446,16.969 6.04,18.797 7.228,22.455C8.416,26.112 9.011,27.94 10.41,29.037C10.522,29.125 10.637,29.209 10.756,29.288C12.231,30.28 14.154,30.28 18,30.28C21.845,30.28 23.768,30.28 25.244,29.288C25.362,29.209 25.478,29.125 25.59,29.037C26.989,27.94 27.583,26.112 28.771,22.455C29.96,18.797 30.554,16.969 30.067,15.259C30.028,15.122 29.984,14.986 29.935,14.852C29.324,13.183 27.768,12.053 24.657,9.793C21.546,7.533 19.99,6.402 18.214,6.337C18.071,6.332 17.928,6.332 17.786,6.337C16.009,6.402 14.454,7.533 11.342,9.793ZM15.666,23.913C15.183,23.913 14.792,24.304 14.792,24.788C14.792,25.271 15.183,25.663 15.666,25.663H20.333C20.816,25.663 21.208,25.271 21.208,24.788C21.208,24.304 20.816,23.913 20.333,23.913H15.666Z"
android:fillColor="#9C9D9D"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Some files were not shown because too many files have changed in this diff Show More