This commit is contained in:
ocean 2024-06-05 10:09:06 +08:00
parent ef237a5c8d
commit 14d790a07e
174 changed files with 5229 additions and 0 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
ProMusic

26
.idea/appInsightsSettings.xml generated Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

10
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

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

@ -0,0 +1,67 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
}
android {
namespace = "com.kitobochi.softapp.task.noisetimber"
compileSdk = 34
defaultConfig {
applicationId = "com.offline.proMusic.player"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
setProperty("archivesBaseName", "Musicoo_${defaultConfig.versionName}(${defaultConfig.versionCode})")
}
buildTypes {
release {
isMinifyEnabled = false
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(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.common)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
}

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

@ -0,0 +1,22 @@
# 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.kitobochi.softapp.task.noisetimber
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.kitobochi.softapp.task.noisetimber", 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">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<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.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".ProApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NoiseTimber"
tools:targetApi="31">
<activity
android:name=".ui.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=".ui.activity.MainActivity"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.PlayDetailsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.AboutActivity"
android:screenOrientation="portrait" />
<service
android:name=".service.LocalPlaybackService"
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

View File

@ -0,0 +1,143 @@
{
"categories": [
{
"name": "Sound of instrument",
"audios": [
{
"name": "Mixing cup",
"file": "Sound of instrument data/The sound of mixing cup.mp3",
"image": "Sound of instrument/The sound of mixing cup.png"
},
{
"name": "Sheep eating grass",
"file": "Sound of instrument data/Sheep eating grass.mp3",
"image": "Sound of instrument/Sheep eating grass.png"
},
{
"name": "Motorcycle",
"file": "Sound of instrument data/Motorcycle.mp3",
"image": "Sound of instrument/Motorcycle.png"
},
{
"name": "Bubble",
"file": "Sound of instrument data/Bubble.mp3",
"image": "Sound of instrument/Bubble.png"
},
{
"name": "Pencil",
"file": "Sound of instrument data/Pencil.mp3",
"image": "Sound of instrument/Pencil.png"
},
{
"name": "Boil water",
"file": "Sound of instrument data/Boil water.mp3",
"image": "Sound of instrument/Boil water.png"
},
{
"name": "Sea breeze",
"file": "Sound of instrument data/Sea breeze.mp3",
"image": "Sound of instrument/Sea breeze.png"
},
{
"name": "Guitar sound",
"file": "Sound of instrument data/Guitar sound.mp3",
"image": "Sound of instrument/Guitar sound.jpg"
},
{
"name": "Piano",
"file": "Sound of instrument data/Piano.mp3",
"image": "Sound of instrument/Piano.png"
}
]
},
{
"name": "White noise",
"audios": [
{
"name": "Type writer",
"file": "White noise data/Type writer.mp3",
"image": "White noise/Type writer.png"
},
{
"name": "Hair Dryer",
"file": "White noise data/Hair Dryer.mp3",
"image": "White noise/Hair Dryer.png"
},
{
"name": "Wooden Door",
"file": "White noise data/Wooden Door.mp3",
"image": "White noise/Wooden Door.png"
},
{
"name": "Peeling an apple",
"file": "White noise data/Peeling an apple.mp3",
"image": "White noise/Peeling an apple.png"
},
{
"name": "Big Ben",
"file": "White noise data/Big Ben.mp3",
"image": "White noise/Big Ben.jpg"
},
{
"name": "Voltage wire",
"file": "White noise data/Voltage wire.mp3",
"image": "White noise/Voltage wire.jpg"
},
{
"name": "Mechanical failure",
"file": "White noise data/Mechanical failure.mp3",
"image": "White noise/Mechanical failure.png"
},
{
"name": "Rhythm",
"file": "White noise data/Rhythm.mp3",
"image": "White noise/Rhythm.jpg"
},{
"name": "Speed car",
"file": "White noise data/Speed car.mp3",
"image": "White noise/Speed car.png"
}
]
},
{
"name": "Voice of Nature",
"audios": [
{
"name": "Birdsong",
"file": "Voice of Nature data/Birdsong.mp3",
"image": "Voice of Nature/Birdsong.png"
},
{
"name": "Lightning Storm",
"file": "Voice of Nature data/Lightning Storm.mp3",
"image": "Voice of Nature/Lightning Storm.png"
},
{
"name": "High -speed stream",
"file": "Voice of Nature data/High -speed stream.mp3",
"image": "Voice of Nature/High -speed stream.jpg"
},
{
"name": "Frog croaking",
"file": "Voice of Nature data/Frog croaking.mp3",
"image": "Voice of Nature/Frog croaking.png"
},
{
"name": "Early morning",
"file": "Voice of Nature data/Early morning.mp3",
"image": "Voice of Nature/Early morning.png"
},
{
"name": "Rain falls in the leaves",
"file": "Voice of Nature data/Rain falls in the leaves.mp3",
"image": "Voice of Nature/Rain falls in the leaves.png"
},
{
"name": "Wind blow",
"file": "Voice of Nature data/Wind blow.mp3",
"image": "Voice of Nature/Wind blow.jpg"
}
]
}
]
}

View File

@ -0,0 +1,109 @@
package com.kitobochi.softapp.task.noisetimber
import android.app.Application
import android.content.Context
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.db.bean.CurrentPlayingAudio
import com.kitobochi.softapp.task.noisetimber.db.bean.ResourcesList
import com.kitobochi.softapp.task.noisetimber.db.tools.CurrentAudioManager
import com.kitobochi.softapp.task.noisetimber.db.tools.DatabaseManager
import com.kitobochi.softapp.task.noisetimber.tools.media.MediaControllerManager
import com.kitobochi.softapp.task.noisetimber.tools.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 ProApp : Application() {
companion object {
lateinit var appContext: Context
lateinit var proApp: ProApp
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) {
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) {
"Sound of instrument" -> realHumanVoiceList = category.audios
"White noise" -> soundsOfAppliancesList = category.audios
"Voice of Nature" -> soundsOfNatureList = category.audios
}
}
}
}
override fun onCreate() {
super.onCreate()
proApp = this
appContext = this
initialize(this)
MediaControllerManager.init(this)
currentAudioManager = CurrentAudioManager.getInstance(this)
databaseManager = DatabaseManager.getInstance(this)
initCurrentPlayingAudio()
initImportAudio()
}
}

View File

@ -0,0 +1,21 @@
package com.kitobochi.softapp.task.noisetimber.db.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,
@ColumnInfo(name = "collect") var collect: Boolean = false
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

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

View File

@ -0,0 +1,15 @@
package com.kitobochi.softapp.task.noisetimber.db.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,23 @@
package com.kitobochi.softapp.task.noisetimber.db.bean
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@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,42 @@
package com.kitobochi.softapp.task.noisetimber.db.bean
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@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>
@Query("update Audio set collect = 0")
suspend fun deleteAllCollect()
@Query("select * from Audio where collect = :collect ")
suspend fun getCollectData(collect: Boolean = true): List<Audio>
}

View File

@ -0,0 +1,7 @@
package com.kitobochi.softapp.task.noisetimber.db.bean
import java.io.Serializable
data class ResourcesList(
val categories: List<Category>
) : Serializable

View File

@ -0,0 +1,12 @@
package com.kitobochi.softapp.task.noisetimber.db.tools
import androidx.room.Database
import androidx.room.RoomDatabase
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.db.bean.LocalAudioDao
@Database(entities = [Audio::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun localAudioDao(): LocalAudioDao
}

View File

@ -0,0 +1,38 @@
package com.kitobochi.softapp.task.noisetimber.db.tools
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.room.Room
import com.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import kotlinx.coroutines.launch
class CollectViewModel : ViewModel() {
private var likeData: MutableLiveData<List<Audio>> = MutableLiveData<List<Audio>>()
private val database = Room.databaseBuilder(
ProApp.appContext, AppDatabase::class.java, "local_audio_viewer_database"
).build()
private val audioFileDao = database.localAudioDao()
init {
viewModelScope.launch {
likeData.value = audioFileDao.getCollectData()
}
}
fun update() {
viewModelScope.launch {
likeData.value = audioFileDao.getCollectData()
}
}
fun getList() = likeData
override fun onCleared() {
super.onCleared()
}
}

View File

@ -0,0 +1,12 @@
package com.kitobochi.softapp.task.noisetimber.db.tools
import androidx.room.Database
import androidx.room.RoomDatabase
import com.kitobochi.softapp.task.noisetimber.db.bean.CurrentPlayingAudio
import com.kitobochi.softapp.task.noisetimber.db.bean.CurrentPlayingAudioDao
@Database(entities = [CurrentPlayingAudio::class], version = 1, exportSchema = false)
abstract class CurrentAudioDatabase : RoomDatabase() {
abstract fun currentPlayingAudioDao(): CurrentPlayingAudioDao
}

View File

@ -0,0 +1,40 @@
package com.kitobochi.softapp.task.noisetimber.db.tools
import android.content.Context
import androidx.room.Room
import com.kitobochi.softapp.task.noisetimber.db.bean.CurrentPlayingAudio
import com.kitobochi.softapp.task.noisetimber.db.bean.CurrentPlayingAudioDao
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,84 @@
package com.kitobochi.softapp.task.noisetimber.db.tools
import android.content.Context
import androidx.room.Room
import com.kitobochi.softapp.task.noisetimber.db.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,54 @@
package com.kitobochi.softapp.task.noisetimber.service
import android.content.Intent
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
@UnstableApi
class LocalPlaybackService : 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,21 @@
package com.kitobochi.softapp.task.noisetimber.tools
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.kitobochi.softapp.task.noisetimber.tools
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.kitobochi.softapp.task.noisetimber.tools
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,46 @@
package com.kitobochi.softapp.task.noisetimber.tools
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
const val PRIVACY_POLICY_URL = "https://promusicapp.mystrikingly.com/privacy\n"
const val TERMS_OF_SERVICE_URL = "https://promusicapp.mystrikingly.com/terms"
const val EMAIL = "vivien11520@gmail.com"
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: ActivityNotFoundException) {
Toast.makeText(context,"There is no app that supports sending emails",Toast.LENGTH_LONG).show()
}
}

View File

@ -0,0 +1,68 @@
package com.kitobochi.softapp.task.noisetimber.tools
import android.content.Context
import android.database.Cursor
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.provider.MediaStore
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.db.bean.Category
import com.kitobochi.softapp.task.noisetimber.db.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://")
}

View File

@ -0,0 +1,175 @@
package com.kitobochi.softapp.task.noisetimber.tools.media
import android.content.ComponentName
import android.content.Context
import android.net.Uri
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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.db.bean.CurrentPlayingAudio
import com.kitobochi.softapp.task.noisetimber.service.LocalPlaybackService
import com.kitobochi.softapp.task.noisetimber.tools.containsContent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
//LOGO
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, LocalPlaybackService::class.java))
controllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
controllerFuture?.addListener({
mediaController = controllerFuture?.get()
}, 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")
}
val resourceId = R.mipmap.ic_launcher
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 {
ProApp.currentAudioManager.setCurrentPlayingAudio(currentPlayingAudio)
withContext(Dispatchers.Main) {
ProApp.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")
}
val resourceId = R.mipmap.ic_launcher
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 {
ProApp.currentAudioManager.setCurrentPlayingAudio(currentPlayingAudio)
withContext(Dispatchers.Main) {
ProApp.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,66 @@
package com.kitobochi.softapp.task.noisetimber.tools.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,27 @@
package com.kitobochi.softapp.task.noisetimber.ui.activity
import android.os.Bundle
import com.gyf.immersionbar.ktx.immersionBar
import com.kitobochi.softapp.task.noisetimber.databinding.ActivityAboutBinding
import com.kitobochi.softapp.task.noisetimber.tools.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,16 @@
package com.kitobochi.softapp.task.noisetimber.ui.activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
}
}

View File

@ -0,0 +1,42 @@
package com.kitobochi.softapp.task.noisetimber.ui.activity
import android.content.Intent
import android.os.Bundle
import com.gyf.immersionbar.ktx.immersionBar
import com.kitobochi.softapp.task.noisetimber.databinding.ActivityLaunchBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class LaunchActivity : BaseActivity() {
private lateinit var binding: ActivityLaunchBinding
private val coroutineScope = CoroutineScope(Dispatchers.Main)
private val countTime: Long = 3000
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLaunchBinding.inflate(layoutInflater)
setContentView(binding.root)
initImmersionBar()
initTimer()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(true)
fullScreen(true)
}
}
private fun initTimer() {
coroutineScope.launch {
delay(countTime)
startMainActivity()
}
}
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@ -0,0 +1,250 @@
package com.kitobochi.softapp.task.noisetimber.ui.activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.ActivityMainBinding
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.tools.getAudioDurationFromAssets
import com.kitobochi.softapp.task.noisetimber.tools.media.MediaControllerManager
import com.kitobochi.softapp.task.noisetimber.ui.fragment.HomeFragment
import com.kitobochi.softapp.task.noisetimber.ui.fragment.MeFragment
import com.kitobochi.softapp.task.noisetimber.ui.fragment.SettingsFragment
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)
initView()
}
private fun initView() {
initClick()
initFragment()
}
override fun onResume() {
super.onResume()
val currentPlayer = MediaControllerManager.getController()
if (ProApp.currentPlayingAudio == null) {
binding.playingStatusLayout.visibility = View.GONE
} else {
binding.playingStatusLayout.visibility = View.VISIBLE
val currentAudio = ProApp.currentPlayingAudio
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.default_list_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.meBtn.setOnClickListener {
changeFragment(1)
updateBtnState(1)
}
binding.appSetting.setOnClickListener {
changeFragment(2)
updateBtnState(2)
}
binding.playingStatusLayout.setOnClickListener {
val currentAudio = ProApp.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.playBlackBtn.setOnClickListener {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null) {
if (currentPlayer.playbackState == Player.STATE_READY) {
if (currentPlayer.isPlaying) {
currentPlayer.pause()
updatePlayState(false)
} else {
currentPlayer.play()
updatePlayState(true)
}
updateProgressState()
} else {
MediaControllerManager.setupMedia(this@MainActivity, ProApp.currentPlayingAudio!!,
object : Player.Listener {
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean,
reason: Int
) {
updatePlayState(playWhenReady)
updateProgressState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
updateProgressState()
}
})
}
} else {
}
}
}
private fun updatePlayState(b: Boolean) {
if (b) {
binding.playStatusImg.setImageResource(R.drawable.main_playing_icon)
} else {
binding.playStatusImg.setImageResource(R.drawable.main_play_icon)
}
}
private fun initFragment() {
mFragments.clear()
mFragments.add(HomeFragment())
mFragments.add(MeFragment())
mFragments.add(SettingsFragment())
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
}
)
meImg.setImageResource(
when (index) {
1 -> R.drawable.me_select_icon
else -> R.drawable.me_unselect_icon
}
)
homeSetting.setImageResource(
when (index) {
2 -> R.drawable.settings_select_icon
else -> R.drawable.settings_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,212 @@
package com.kitobochi.softapp.task.noisetimber.ui.activity
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
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.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.ActivityPlayDetailsBinding
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.tools.containsContent
import com.kitobochi.softapp.task.noisetimber.tools.convertMillisToMinutesAndSecondsString
import com.kitobochi.softapp.task.noisetimber.tools.getAudioDurationFromAssets
import com.kitobochi.softapp.task.noisetimber.tools.media.MediaControllerManager
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()
}
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)
} else {
binding.image.setImageResource(R.mipmap.default_list_img)
}
binding.seekBar.value = 0f
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
) {
updatePlayState(playWhenReady, "playWhenReady")
updateProgressState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
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.details_play_icon)
}
}
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,71 @@
package com.kitobochi.softapp.task.noisetimber.ui.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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.ParentsVoiceLayoutBinding
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.tools.containsContent
import com.kitobochi.softapp.task.noisetimber.tools.convertMillisToMinutesAndSecondsString
import com.kitobochi.softapp.task.noisetimber.tools.getAudioDurationFromAssets
import com.kitobochi.softapp.task.noisetimber.ui.activity.PlayDetailsActivity
//LOGO
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.default_list_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 (ProApp.currentPlayingAudio != null) {
if (ProApp.currentPlayingAudio?.file == audio.file) {
playingLayout.visibility = View.VISIBLE
name.setTextColor(context.getColor(R.color.blue))
desc.setTextColor(context.getColor(R.color.blue))
} else {
playingLayout.visibility = View.GONE
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white_60))
}
}
}
}
}
}

View File

@ -0,0 +1,67 @@
package com.kitobochi.softapp.task.noisetimber.ui.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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.RealHumanVoiceLayoutBinding
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.tools.convertMillisToMinutesAndSecondsString
import com.kitobochi.softapp.task.noisetimber.tools.getAudioDurationFromAssets
import com.kitobochi.softapp.task.noisetimber.ui.activity.PlayDetailsActivity
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 (ProApp.currentPlayingAudio != null) {
if (ProApp.currentPlayingAudio?.file == audio.file) {
stateImg.setImageResource(R.drawable.playing_white_icon)
name.setTextColor(context.getColor(R.color.blue))
desc.setTextColor(context.getColor(R.color.blue))
} else {
stateImg.setImageResource(R.drawable.amin_item_play_icon)
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white_60))
}
}
}
}
}
}

View File

@ -0,0 +1,79 @@
package com.kitobochi.softapp.task.noisetimber.ui.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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.SoundsOfAppliancesLayoutBinding
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.tools.convertMillisToMinutesAndSecondsString
import com.kitobochi.softapp.task.noisetimber.tools.getAudioDurationFromAssets
import com.kitobochi.softapp.task.noisetimber.ui.activity.PlayDetailsActivity
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 (ProApp.currentPlayingAudio != null) {
if (ProApp.currentPlayingAudio?.file == audio.file) {
playingLayout.visibility = View.VISIBLE
name.setTextColor(context.getColor(R.color.blue))
desc.setTextColor(context.getColor(R.color.blue))
} else {
playingLayout.visibility = View.GONE
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white_60))
}
}
}
}
}
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.kitobochi.softapp.task.noisetimber.ui.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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.SoundsOfNatureLayoutBinding
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.tools.convertMillisToMinutesAndSecondsString
import com.kitobochi.softapp.task.noisetimber.tools.getAudioDurationFromAssets
import com.kitobochi.softapp.task.noisetimber.ui.activity.PlayDetailsActivity
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 (ProApp.currentPlayingAudio != null) {
if (ProApp.currentPlayingAudio?.file == audio.file) {
playingLayout.visibility = View.VISIBLE
name.setTextColor(context.getColor(R.color.blue))
desc.setTextColor(context.getColor(R.color.blue))
} else {
playingLayout.visibility = View.GONE
name.setTextColor(context.getColor(R.color.white))
desc.setTextColor(context.getColor(R.color.white_60))
}
}
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,126 @@
package com.kitobochi.softapp.task.noisetimber.ui.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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.databinding.FragmentHome2Binding
import com.kitobochi.softapp.task.noisetimber.tools.GridSpacingItemDecoration
import com.kitobochi.softapp.task.noisetimber.tools.HorizontalSpaceItemDecoration
import com.kitobochi.softapp.task.noisetimber.ui.adapter.RealHumanVoiceAdapter
import com.kitobochi.softapp.task.noisetimber.ui.adapter.SoundsOfAppliancesAdapter
import com.kitobochi.softapp.task.noisetimber.ui.adapter.SoundsOfNatureAdapter
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHome2Binding
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 = FragmentHome2Binding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
initData()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private fun initView() {
if (ProApp.resourcesList.categories.isNotEmpty()) {
binding.soundsName.text = ProApp.resourcesList.categories[1].name
binding.natureName.text = ProApp.resourcesList.categories[2].name
}
}
private fun initData(){
if (ProApp.realHumanVoiceList.isNotEmpty()) {
realHumanVoiceAdapter = RealHumanVoiceAdapter(requireActivity(), ProApp.realHumanVoiceList)
binding.realRv.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false)
binding.realRv.addItemDecoration(HorizontalSpaceItemDecoration(requireActivity()))
binding.realRv.adapter = realHumanVoiceAdapter
}
if (ProApp.soundsOfAppliancesList.isNotEmpty()) {
soundsOfAppliancesAdapter =
SoundsOfAppliancesAdapter(requireActivity(), ProApp.soundsOfAppliancesList)
binding.soundsRv.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false)
binding.soundsRv.addItemDecoration(HorizontalSpaceItemDecoration(requireActivity()))
binding.soundsRv.adapter = soundsOfAppliancesAdapter
}
if (ProApp.soundsOfNatureList.isNotEmpty()) {
soundsOfNatureAdapter =
SoundsOfNatureAdapter(requireActivity(), ProApp.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 (ProApp.currentPlayingAudio != null) {
if (ProApp.realHumanVoiceList.isNotEmpty()) {
for ((index, audio) in ProApp.realHumanVoiceList.withIndex()) {
if (audio.file == ProApp.currentPlayingAudio?.file) {
notifyDataSetChanged()
break
}
}
}
if (ProApp.soundsOfAppliancesList.isNotEmpty()) {
for ((index, audio) in ProApp.soundsOfAppliancesList.withIndex()) {
if (audio.file == ProApp.currentPlayingAudio?.file) {
notifyDataSetChanged()
break
}
}
}
if (ProApp.soundsOfNatureList.isNotEmpty()) {
for ((index, audio) in ProApp.soundsOfNatureList.withIndex()) {
if (audio.file == ProApp.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,284 @@
package com.kitobochi.softapp.task.noisetimber.ui.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.pm.PackageManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.kitobochi.softapp.task.noisetimber.ProApp
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.FragmentMeBinding
import com.kitobochi.softapp.task.noisetimber.db.bean.Audio
import com.kitobochi.softapp.task.noisetimber.ui.adapter.ParentsVoiceAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MeFragment : Fragment() {
private lateinit var binding: FragmentMeBinding
private var parentsVoiceAdapter: ParentsVoiceAdapter? = null
private var importAdapterList: MutableList<Audio> = mutableListOf()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentMeBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
private fun initView() {
binding.addBtn.setOnClickListener {
binding.addBtn.visibility = View.GONE
checkAndRequestPermissions()
}
importAdapterList.clear()
importAdapterList.addAll(ProApp.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 (ProApp.currentPlayingAudio != null) {
if (ProApp.importList.isNotEmpty()) {
importAdapterList.clear()
importAdapterList.addAll(ProApp.importList)
for ((index, audio) in importAdapterList.withIndex()) {
if (audio.file == ProApp.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)
CoroutineScope(Dispatchers.IO).launch {
if (audio.duration > 0) {
ProApp.databaseManager.insertAudioFile(audio)
}
withContext(Dispatchers.Main) {
ProApp.initImportAudio {
importAdapterList.clear()
importAdapterList.addAll(ProApp.importList)
parentsVoiceAdapter?.notifyDataSetChanged()
if (importAdapterList.isNotEmpty()) {
binding.noContentLayout.visibility = View.GONE
} else {
binding.noContentLayout.visibility = View.VISIBLE
showNoDataDialog()
}
binding.loadingLayout.visibility = View.GONE
}
}
}
}
}
return musicFiles
}
private fun showNoDataDialog() {
AlertDialog.Builder(requireActivity())
.setTitle(getString(R.string.prompt))
.setMessage(getString(R.string.no_data_prompt_dialog_content))
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
binding.addBtn.visibility = View.VISIBLE
dialog.dismiss()
}
.setCancelable(false)
.show()
}
private fun checkAndRequestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionsToRequest = mutableListOf<String>()
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_MEDIA_IMAGES
) != PackageManager.PERMISSION_GRANTED
) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_IMAGES)
}
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_MEDIA_VIDEO
) != PackageManager.PERMISSION_GRANTED
) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_VIDEO)
}
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_MEDIA_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_AUDIO)
}
if (permissionsToRequest.isNotEmpty()) {
requestMultiplePermissionsLauncher.launch(permissionsToRequest.toTypedArray())
} else {
openAudioPicker()
}
} else {
// 请求旧版本的权限
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
openAudioPicker()
}
}
}
private val requestMultiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
permissions.forEach { (permission, isGranted) ->
if (isGranted) {
openAudioPicker()
} else {
showExplanationDialog()
}
}
}
}

View File

@ -0,0 +1,75 @@
package com.kitobochi.softapp.task.noisetimber.ui.fragment
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.gyf.immersionbar.ktx.immersionBar
import com.kitobochi.softapp.task.noisetimber.R
import com.kitobochi.softapp.task.noisetimber.databinding.FragmentSettingBinding
import com.kitobochi.softapp.task.noisetimber.tools.EMAIL
import com.kitobochi.softapp.task.noisetimber.tools.PRIVACY_POLICY_URL
import com.kitobochi.softapp.task.noisetimber.tools.TERMS_OF_SERVICE_URL
import com.kitobochi.softapp.task.noisetimber.tools.openPrivacyPolicy
import com.kitobochi.softapp.task.noisetimber.tools.openTermsOfService
import com.kitobochi.softapp.task.noisetimber.tools.sendFeedback
import com.kitobochi.softapp.task.noisetimber.tools.shareApp
import com.kitobochi.softapp.task.noisetimber.ui.activity.AboutActivity
class SettingsFragment : Fragment() {
private lateinit var binding: FragmentSettingBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSettingBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initImmersionBar()
initView()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (!hidden) {
initImmersionBar()
}
}
override fun onResume() {
super.onResume()
initImmersionBar()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private fun initView() {
binding.aboutBtn.setOnClickListener {
startActivity(Intent(requireContext(), AboutActivity::class.java))
}
binding.feedbackBtn.setOnClickListener {
sendFeedback(requireContext(), EMAIL, getString(R.string.app_name))
}
binding.shareBtn.setOnClickListener {
shareApp(requireContext())
}
binding.ppBtn.setOnClickListener {
openPrivacyPolicy(requireContext(), PRIVACY_POLICY_URL)
}
binding.tosBtn.setOnClickListener {
openTermsOfService(requireContext(), TERMS_OF_SERVICE_URL)
}
}
}

View File

@ -0,0 +1,90 @@
package com.kitobochi.softapp.task.noisetimber.ui.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.kitobochi.softapp.task.noisetimber.ui.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)
}
}

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