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)
|
||||
}
|
||||
}
|
||||
|
||||