update
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
ProMusic
|
||||||
26
.idea/appInsightsSettings.xml
generated
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
67
app/build.gradle.kts
Normal 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
@ -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
|
||||||
|
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
BIN
app/src/main/assets/Sound of instrument data/Boil water.mp3
Normal file
BIN
app/src/main/assets/Sound of instrument data/Bubble.mp3
Normal file
BIN
app/src/main/assets/Sound of instrument data/Guitar sound.mp3
Normal file
BIN
app/src/main/assets/Sound of instrument data/Motorcycle.mp3
Normal file
BIN
app/src/main/assets/Sound of instrument data/Pencil.mp3
Normal file
BIN
app/src/main/assets/Sound of instrument data/Piano.mp3
Normal file
BIN
app/src/main/assets/Sound of instrument data/Sea breeze.mp3
Normal file
BIN
app/src/main/assets/Sound of instrument/Boil water.png
Normal file
|
After Width: | Height: | Size: 429 KiB |
BIN
app/src/main/assets/Sound of instrument/Bubble.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
app/src/main/assets/Sound of instrument/Guitar sound.jpg
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
app/src/main/assets/Sound of instrument/Motorcycle.png
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
app/src/main/assets/Sound of instrument/Pencil.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
app/src/main/assets/Sound of instrument/Piano.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
app/src/main/assets/Sound of instrument/Sea breeze.png
Normal file
|
After Width: | Height: | Size: 459 KiB |
BIN
app/src/main/assets/Sound of instrument/Sheep eating grass.png
Normal file
|
After Width: | Height: | Size: 777 KiB |
|
After Width: | Height: | Size: 161 KiB |
BIN
app/src/main/assets/Voice of Nature data/Birdsong.mp3
Normal file
BIN
app/src/main/assets/Voice of Nature data/Early morning.mp3
Normal file
BIN
app/src/main/assets/Voice of Nature data/Frog croaking.mp3
Normal file
BIN
app/src/main/assets/Voice of Nature data/High -speed stream.mp3
Normal file
BIN
app/src/main/assets/Voice of Nature data/Lightning Storm.mp3
Normal file
BIN
app/src/main/assets/Voice of Nature data/Wind blow.mp3
Normal file
BIN
app/src/main/assets/Voice of Nature/Birdsong.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
app/src/main/assets/Voice of Nature/Early morning.png
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
app/src/main/assets/Voice of Nature/Frog croaking.png
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
app/src/main/assets/Voice of Nature/High -speed stream.jpg
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
app/src/main/assets/Voice of Nature/Lightning Storm.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
app/src/main/assets/Voice of Nature/Rain falls in the leaves.png
Normal file
|
After Width: | Height: | Size: 428 KiB |
BIN
app/src/main/assets/Voice of Nature/Wind blow.jpg
Normal file
|
After Width: | Height: | Size: 796 KiB |
BIN
app/src/main/assets/White noise data/Big Ben.mp3
Normal file
BIN
app/src/main/assets/White noise data/Hair Dryer.mp3
Normal file
BIN
app/src/main/assets/White noise data/Mechanical failure.mp3
Normal file
BIN
app/src/main/assets/White noise data/Peeling an apple.mp3
Normal file
BIN
app/src/main/assets/White noise data/Rhythm.mp3
Normal file
BIN
app/src/main/assets/White noise data/Speed car.mp3
Normal file
BIN
app/src/main/assets/White noise data/Type writer.mp3
Normal file
BIN
app/src/main/assets/White noise data/Voltage wire.mp3
Normal file
BIN
app/src/main/assets/White noise data/Wooden Door.mp3
Normal file
BIN
app/src/main/assets/White noise/Big Ben.jpg
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
app/src/main/assets/White noise/Hair Dryer.png
Normal file
|
After Width: | Height: | Size: 371 KiB |
BIN
app/src/main/assets/White noise/Mechanical failure.png
Normal file
|
After Width: | Height: | Size: 804 KiB |
BIN
app/src/main/assets/White noise/Peeling an apple.png
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
app/src/main/assets/White noise/Rhythm.jpg
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
app/src/main/assets/White noise/Speed car.png
Normal file
|
After Width: | Height: | Size: 588 KiB |
BIN
app/src/main/assets/White noise/Type writer.png
Normal file
|
After Width: | Height: | Size: 487 KiB |
BIN
app/src/main/assets/White noise/Voltage wire.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
app/src/main/assets/White noise/Wooden Door.png
Normal file
|
After Width: | Height: | Size: 672 KiB |
143
app/src/main/assets/resources.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.kitobochi.softapp.task.noisetimber.db.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class ResourcesList(
|
||||||
|
val categories: List<Category>
|
||||||
|
) : Serializable
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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://")
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||