*.[修复]修复重新骑行时,最高速度等信息有时没有重新计算的BUG。
This commit is contained in:
commit
288dbb8160
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Publish/
|
||||
Res/
|
||||
**/obj
|
||||
*.pdb
|
||||
**/dcu
|
||||
*.dcu
|
||||
**/Secure
|
||||
*/.vs
|
||||
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
## :date:2026-06-30 星期二更新
|
||||
|
||||
### GPSSpeed V1.2.1
|
||||
- :lady_beetle:[修复]修复重新骑行时,最高速度等信息有时没有重新计算的BUG。
|
||||
|
||||
BIN
Source/GPSSpeed/.gradle/9.4.1/checksums/checksums.lock
Normal file
BIN
Source/GPSSpeed/.gradle/9.4.1/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
Source/GPSSpeed/.gradle/9.4.1/checksums/md5-checksums.bin
Normal file
BIN
Source/GPSSpeed/.gradle/9.4.1/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
Source/GPSSpeed/.gradle/9.4.1/checksums/sha1-checksums.bin
Normal file
BIN
Source/GPSSpeed/.gradle/9.4.1/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Source/GPSSpeed/.gradle/9.4.1/fileChanges/last-build.bin
Normal file
BIN
Source/GPSSpeed/.gradle/9.4.1/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
Source/GPSSpeed/.gradle/9.4.1/fileHashes/fileHashes.bin
Normal file
BIN
Source/GPSSpeed/.gradle/9.4.1/fileHashes/fileHashes.bin
Normal file
Binary file not shown.
BIN
Source/GPSSpeed/.gradle/9.4.1/fileHashes/fileHashes.lock
Normal file
BIN
Source/GPSSpeed/.gradle/9.4.1/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
BIN
Source/GPSSpeed/.gradle/9.4.1/fileHashes/resourceHashesCache.bin
Normal file
BIN
Source/GPSSpeed/.gradle/9.4.1/fileHashes/resourceHashesCache.bin
Normal file
Binary file not shown.
0
Source/GPSSpeed/.gradle/9.4.1/gc.properties
Normal file
0
Source/GPSSpeed/.gradle/9.4.1/gc.properties
Normal file
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
|||
#Fri Jun 12 10:56:38 CST 2026
|
||||
gradle.version=9.4.1
|
||||
BIN
Source/GPSSpeed/.gradle/buildOutputCleanup/outputFiles.bin
Normal file
BIN
Source/GPSSpeed/.gradle/buildOutputCleanup/outputFiles.bin
Normal file
Binary file not shown.
2
Source/GPSSpeed/.gradle/config.properties
Normal file
2
Source/GPSSpeed/.gradle/config.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#Fri Mar 06 08:40:18 CST 2026
|
||||
java.home=D\:\\Program Files\\Android\\Android Studio\\jbr
|
||||
BIN
Source/GPSSpeed/.gradle/file-system.probe
Normal file
BIN
Source/GPSSpeed/.gradle/file-system.probe
Normal file
Binary file not shown.
0
Source/GPSSpeed/.gradle/vcs-1/gc.properties
Normal file
0
Source/GPSSpeed/.gradle/vcs-1/gc.properties
Normal file
3
Source/GPSSpeed/.idea/.gitignore
vendored
Normal file
3
Source/GPSSpeed/.idea/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
6
Source/GPSSpeed/.idea/AndroidProjectSystem.xml
Normal file
6
Source/GPSSpeed/.idea/AndroidProjectSystem.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
14
Source/GPSSpeed/.idea/assetWizardSettings.xml
Normal file
14
Source/GPSSpeed/.idea/assetWizardSettings.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WizardSettings">
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="vectorWizard">
|
||||
<value>
|
||||
<PersistentState />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1990
Source/GPSSpeed/.idea/caches/deviceStreaming.xml
Normal file
1990
Source/GPSSpeed/.idea/caches/deviceStreaming.xml
Normal file
File diff suppressed because it is too large
Load Diff
123
Source/GPSSpeed/.idea/codeStyles/Project.xml
Normal file
123
Source/GPSSpeed/.idea/codeStyles/Project.xml
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
Source/GPSSpeed/.idea/codeStyles/codeStyleConfig.xml
Normal file
5
Source/GPSSpeed/.idea/codeStyles/codeStyleConfig.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
Source/GPSSpeed/.idea/compiler.xml
Normal file
6
Source/GPSSpeed/.idea/compiler.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
26
Source/GPSSpeed/.idea/deploymentTargetSelector.xml
Normal file
26
Source/GPSSpeed/.idea/deploymentTargetSelector.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-06-12T03:00:34.793188800Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\zilin\.android\avd\Medium_Phone.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection>
|
||||
<targets>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\zilin\.android\avd\Medium_Phone.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</targets>
|
||||
</DialogSelection>
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
13
Source/GPSSpeed/.idea/deviceManager.xml
Normal file
13
Source/GPSSpeed/.idea/deviceManager.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
7
Source/GPSSpeed/.idea/dictionaries/project.xml
Normal file
7
Source/GPSSpeed/.idea/dictionaries/project.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>nowtime</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
18
Source/GPSSpeed/.idea/gradle.xml
Normal file
18
Source/GPSSpeed/.idea/gradle.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
61
Source/GPSSpeed/.idea/inspectionProfiles/Project_Default.xml
Normal file
61
Source/GPSSpeed/.idea/inspectionProfiles/Project_Default.xml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
10
Source/GPSSpeed/.idea/migrations.xml
Normal file
10
Source/GPSSpeed/.idea/migrations.xml
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>
|
||||
16
Source/GPSSpeed/.idea/misc.xml
Normal file
16
Source/GPSSpeed/.idea/misc.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
<component name="VisualizationToolProject">
|
||||
<option name="state">
|
||||
<ProjectState>
|
||||
<option name="scale" value="0.19048339843749995" />
|
||||
</ProjectState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
17
Source/GPSSpeed/.idea/runConfigurations.xml
Normal file
17
Source/GPSSpeed/.idea/runConfigurations.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1
Source/GPSSpeed/app/.gitignore
vendored
Normal file
1
Source/GPSSpeed/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
75
Source/GPSSpeed/app/build.gradle
Normal file
75
Source/GPSSpeed/app/build.gradle
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
// 👇 就加这一行
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
namespace 'com.itrycn.gpsspeed'
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.itrycn.gpsspeed"
|
||||
minSdk 26
|
||||
targetSdk 36
|
||||
versionCode 4
|
||||
versionName "1.2.1"
|
||||
buildConfigField "boolean", "PrivateSoft", "false"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
// 宏定义
|
||||
//buildConfigField "boolean", "PrivateSoft", "false"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
//buildConfigField "boolean", "PrivateSoft", "false"
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
viewBinding {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation "com.google.code.gson:gson:2.10.1"
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.lifecycle.runtime.ktx
|
||||
implementation libs.androidx.activity.compose
|
||||
implementation platform(libs.androidx.compose.bom)
|
||||
implementation libs.androidx.compose.ui
|
||||
implementation libs.androidx.compose.ui.graphics
|
||||
implementation libs.androidx.compose.ui.tooling.preview
|
||||
implementation libs.androidx.compose.material3
|
||||
implementation libs.androidx.appcompat
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation libs.androidx.preference
|
||||
// 分组设置界面必备
|
||||
implementation libs.androidx.preference.ktx
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.compose.ui.test.junit4
|
||||
debugImplementation libs.androidx.compose.ui.tooling
|
||||
debugImplementation libs.androidx.compose.ui.test.manifest
|
||||
|
||||
}
|
||||
21
Source/GPSSpeed/app/proguard-rules.pro
vendored
Normal file
21
Source/GPSSpeed/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.itrycn.gpsspeed
|
||||
|
||||
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.itrycn.gpsspeed", appContext.packageName)
|
||||
}
|
||||
}
|
||||
54
Source/GPSSpeed/app/src/main/AndroidManifest.xml
Normal file
54
Source/GPSSpeed/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?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" />
|
||||
<!-- GPS定位权限 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 后台定位权限(Android 10+) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- 悬浮窗权限 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<!-- ✅ 新增:修复你崩溃的核心权限(必须加) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/icon"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:resizeableActivity="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<service
|
||||
android:name=".FloatingWindowService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="location"
|
||||
android:permission="TODO"></service>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="unspecified"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:theme="@style/Theme.NoTitleFullScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="骑行设置"
|
||||
android:screenOrientation="unspecified"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:theme="@style/Theme.NoActionBar.Settings"
|
||||
/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,648 @@
|
|||
package com.itrycn.gpsspeed
|
||||
import okhttp3.*
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.PixelFormat
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.NotificationCompat
|
||||
import org.json.JSONObject
|
||||
import java.util.Locale
|
||||
import androidx.core.content.edit
|
||||
|
||||
// 行驶记录数据类
|
||||
class FloatingWindowService : Service() {
|
||||
|
||||
data class DriveRecord(
|
||||
val distance: Float, // 公里
|
||||
val avgSpeed: Float,
|
||||
val maxSpeed: Float,
|
||||
val durationMs: Long, // 耗时毫秒
|
||||
val startLocation: Location?, // 开始定位点
|
||||
val startLocationStr: String?,
|
||||
val startLocationJson: String?,
|
||||
val endLocationStr: String?,
|
||||
val endLocation: Location?, // 结束定位点
|
||||
val endLocationJson: String?,
|
||||
val startTime: Long, // 开始时间
|
||||
val endTime: Long // 结束时间
|
||||
)
|
||||
// 用于传递数据
|
||||
companion object {
|
||||
const val ACTION_SHOW_FLOAT = "SHOW_FLOAT"
|
||||
const val ACTION_HIDE_FLOAT = "HIDE_FLOAT"
|
||||
const val ACTION_CLOSE_FLOAT = "CLOSE_FLOAT"
|
||||
const val ACTION_REAL_TIME_REPORT = "REAL_TIME_REPORT"
|
||||
const val ACTION_REAL_TIME = "ACTION_REAL_TIME"
|
||||
var lastDriveRecord: DriveRecord? = null
|
||||
const val ACTION_REPORT_UPDATE = "REPORT_UPDATE" // 新增
|
||||
}
|
||||
private lateinit var windowManager: WindowManager
|
||||
private lateinit var floatingView: View
|
||||
private lateinit var tvSpeed: TextView
|
||||
private lateinit var tvTime: TextView
|
||||
private lateinit var tvDistance: TextView // 新增
|
||||
private lateinit var tvDuration: TextView // 耗时
|
||||
private lateinit var tvTotalDuration: TextView // 耗时
|
||||
private var lowSpeedStartTime: Long = 0 //低速行驶开始的时间
|
||||
private var RideState: Boolean = false //骑行状态是否在骑行
|
||||
private var auto_ride_speed:Int = 0
|
||||
private var stop_ride_time:Int = 0
|
||||
private var totalDistance = 0.0f // 总里程(米)
|
||||
private var maxSpeed = 0.0f
|
||||
private var totalSpeed = 0.0f
|
||||
private var speedCount = 0
|
||||
private var startFirstTime: Long = 0 // 第一次开始行驶时间
|
||||
// 行驶耗时(新增)
|
||||
private var startTime: Long = 0 // 开始行驶时间
|
||||
private var totalDurationMs = 0L // 总耗时(毫秒)
|
||||
private var isRunning = false // 是否正在计时
|
||||
private var lastLocation: Location? = null // 上一次定位点
|
||||
private var startLocation: Location?=null // 开始定位点
|
||||
private var endLocation: Location?=null // 结束定位点
|
||||
private lateinit var locationManager: LocationManager
|
||||
private var speedUpdateListener: SpeedUpdateListener? = null
|
||||
// 新增
|
||||
private val client = OkHttpClient()
|
||||
// 悬浮窗参数
|
||||
private val layoutParams by lazy {
|
||||
WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
else
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
gravity = Gravity.TOP or Gravity.LEFT
|
||||
x = 100
|
||||
y = 100
|
||||
}
|
||||
}
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_SHOW_FLOAT -> showFloatingWindow() // 显示
|
||||
ACTION_HIDE_FLOAT -> hideFloatingWindow() // 隐藏
|
||||
}
|
||||
//是否是开始骑行
|
||||
if(intent?.getBooleanExtra("start_ride",false) ?:false )
|
||||
{
|
||||
RideState=true
|
||||
speedCount=0
|
||||
maxSpeed=0f
|
||||
totalSpeed=0f
|
||||
totalDurationMs=0L
|
||||
totalDistance=0f
|
||||
startFirstTime=0L
|
||||
isRunning=false
|
||||
auto_ride_speed= intent.getIntExtra("auto_ride_speed",10)
|
||||
stop_ride_time= intent.getIntExtra("stop_ride_time",2)
|
||||
}
|
||||
if(intent?.getBooleanExtra("end_ride",false) ?:false )
|
||||
{
|
||||
RideState=false
|
||||
// 保存本次记录
|
||||
val endTime = System.currentTimeMillis()
|
||||
val totalMs = if (isRunning) {
|
||||
totalDurationMs + (endTime - startTime)
|
||||
} else {
|
||||
totalDurationMs
|
||||
}
|
||||
endLocation=lastLocation;
|
||||
val start_addr=getAddress(startLocation)
|
||||
val end_addr=getAddress(endLocation)
|
||||
if(lastDriveRecord==null) {
|
||||
lastDriveRecord = DriveRecord(
|
||||
distance = totalDistance / 1000f,
|
||||
avgSpeed = if (speedCount == 0) 0f else totalSpeed / speedCount,
|
||||
maxSpeed = maxSpeed,
|
||||
durationMs = totalMs,
|
||||
startLocation = startLocation,
|
||||
endLocation = endLocation,
|
||||
startTime = startFirstTime,
|
||||
endTime = endTime,
|
||||
startLocationJson = getAddressJson(start_addr),
|
||||
startLocationStr = getAddressStr(start_addr),
|
||||
endLocationStr = getAddressStr(end_addr),
|
||||
endLocationJson = getAddressJson(end_addr),
|
||||
)
|
||||
}
|
||||
// ======================
|
||||
// 🔥 这里自动上传数据
|
||||
// ======================
|
||||
lastDriveRecord?.let { record ->
|
||||
speedUpdateListener?.onDriveEnd(record)
|
||||
uploadReport(record)
|
||||
// ========== 发送实时报表到主界面(新增) ==========
|
||||
val intent = Intent(ACTION_REPORT_UPDATE)
|
||||
intent.putExtra("distance", totalDistance / 1000f)
|
||||
intent.putExtra("avgSpeed", if (speedCount == 0) 0f else totalSpeed / speedCount)
|
||||
intent.putExtra("maxSpeed", maxSpeed)
|
||||
intent.putExtra("durationMs", totalMs)
|
||||
intent.putExtra("startTime", startFirstTime)
|
||||
intent.putExtra("endTime", endTime)
|
||||
intent.putExtra("startLocationStr", record.startLocationStr)
|
||||
intent.putExtra("endLocationStr", record.endLocationStr)
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
// 显示悬浮窗
|
||||
private fun showFloatingWindow() {
|
||||
if (floatingView.parent == null) {
|
||||
windowManager.addView(floatingView, layoutParams)
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏悬浮窗
|
||||
private fun hideFloatingWindow() {
|
||||
if (floatingView.parent != null) {
|
||||
windowManager.removeView(floatingView)
|
||||
}
|
||||
}
|
||||
// 速度回调
|
||||
interface SpeedUpdateListener {
|
||||
fun onSpeedChanged(speed: Int)
|
||||
fun onDriveEnd(drive: DriveRecord)
|
||||
}
|
||||
|
||||
fun setSpeedListener(listener: SpeedUpdateListener) {
|
||||
speedUpdateListener = listener
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initFloatingWindow()
|
||||
initLocation()
|
||||
startForegroundNotification()
|
||||
startTimeUpdate()
|
||||
}
|
||||
// 定时刷新时间
|
||||
private fun startTimeUpdate() {
|
||||
val handler = android.os.Handler(mainLooper)
|
||||
val runnable = object : Runnable {
|
||||
@SuppressLint("SetTextI18n", "DefaultLocale")
|
||||
override fun run() {
|
||||
if(RideState) {
|
||||
// 获取当前时间 HH:mm:ss
|
||||
val sdf = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault())
|
||||
val currentTime = sdf.format(java.util.Date())
|
||||
tvTime.text = currentTime
|
||||
if (startFirstTime == 0L) {
|
||||
tvTotalDuration.text = "总耗时: 00:00:00"
|
||||
} else {
|
||||
// 耗时格式化
|
||||
val totalMs = System.currentTimeMillis() - startFirstTime
|
||||
val totalH = (totalMs / 3600000) % 24
|
||||
val totalM = (totalMs / 60000) % 60
|
||||
val totalS = (totalMs / 1000) % 60
|
||||
val totalDuration = String.format("%02d:%02d:%02d", totalH, totalM, totalS)
|
||||
tvTotalDuration.text = "总耗时:$totalDuration"
|
||||
}
|
||||
if (lowSpeedStartTime > 0L && stop_ride_time > 0 && RideState) {
|
||||
if ((System.currentTimeMillis() - lowSpeedStartTime) / 1000 >= stop_ride_time * 60) {
|
||||
lowSpeedStartTime = 0
|
||||
RideState=false
|
||||
openMainActivity(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 每秒刷新一次
|
||||
handler.postDelayed(this, 1000)
|
||||
}
|
||||
}
|
||||
handler.post(runnable)
|
||||
}
|
||||
// 初始化悬浮窗
|
||||
private fun initFloatingWindow() {
|
||||
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||
floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null)
|
||||
tvSpeed = floatingView.findViewById(R.id.tv_speed)
|
||||
tvTime = floatingView.findViewById(R.id.tv_time)
|
||||
tvDistance = floatingView.findViewById(R.id.tv_distance) // 新增
|
||||
tvDuration = floatingView.findViewById(R.id.tv_duration)
|
||||
tvTotalDuration= floatingView.findViewById(R.id.tv_total_duration)
|
||||
// 悬浮窗拖动
|
||||
var lastX = 0
|
||||
var lastY = 0
|
||||
var isClick = true // 标记是否是点击
|
||||
|
||||
floatingView.setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
lastX = event.rawX.toInt()
|
||||
lastY = event.rawY.toInt()
|
||||
isClick = true // 按下时标记为点击
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val dx = event.rawX.toInt() - lastX
|
||||
val dy = event.rawY.toInt() - lastY
|
||||
|
||||
// 如果移动超过一定距离,就不是点击
|
||||
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
||||
isClick = false
|
||||
layoutParams.x += dx
|
||||
layoutParams.y += dy
|
||||
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||
lastX = event.rawX.toInt()
|
||||
lastY = event.rawY.toInt()
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// 抬起时,如果是点击,就打开主界面
|
||||
if (isClick) {
|
||||
floatingView.performClick()
|
||||
openMainActivity(false)
|
||||
//val intent = Intent(ACTION_CLOSE_FLOAT).apply {
|
||||
// putExtra("avgSpeed", if (speedCount == 0) 0f else totalSpeed / speedCount)
|
||||
//}
|
||||
//sendBroadcast(intent)
|
||||
//closeFloatAndStopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
windowManager.addView(floatingView, layoutParams)
|
||||
}
|
||||
|
||||
/** 移除悬浮窗 + 停止当前服务 */
|
||||
private fun closeFloatAndStopService() {
|
||||
// 第一步:安全移除窗口(防止View未挂载崩溃)
|
||||
if (floatingView.parent != null) {
|
||||
windowManager.removeView(floatingView)
|
||||
}
|
||||
// 第二步:停止自身服务
|
||||
stopSelf()
|
||||
}
|
||||
// 点击悬浮窗 → 打开主界面
|
||||
private fun openMainActivity(StopRide: Boolean) {
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val endTime = System.currentTimeMillis()
|
||||
val rideMs = if (isRunning) {
|
||||
totalDurationMs + (endTime - startTime)
|
||||
} else {
|
||||
totalDurationMs
|
||||
}
|
||||
val km = totalDistance / 1000f
|
||||
if (StopRide){intent.putExtra("StopRide", true)}
|
||||
intent.putExtra("RideData",true)
|
||||
intent.putExtra("RideState",RideState)
|
||||
intent.putExtra("maxSpeed",maxSpeed)
|
||||
intent.putExtra("avgSpeed",if (speedCount == 0) 0f else totalSpeed / speedCount)
|
||||
intent.putExtra("realSpeed",0)
|
||||
intent.putExtra("rideMs",rideMs)
|
||||
intent.putExtra("totalDistance",String.format("%.2f km", km))
|
||||
intent.putExtra("startFirstTime",startFirstTime)
|
||||
// getSharedPreferences("temp_data", Context.MODE_PRIVATE)
|
||||
// .edit {
|
||||
// putBoolean("RideData", true)
|
||||
// if (StopRide){putBoolean("StopRide", true)}
|
||||
// putFloat("maxSpeed", maxSpeed)
|
||||
// putFloat("avgSpeed", if (speedCount == 0) 0f else totalSpeed / speedCount)
|
||||
// putInt("realSpeed", 0)
|
||||
// putInt("rideMs", 0)
|
||||
// putInt("totalDistance", 0)
|
||||
// putLong("startFirstTime", startFirstTime)
|
||||
// }
|
||||
startActivity(intent)
|
||||
}
|
||||
// 初始化GPS定位
|
||||
private fun initLocation() {
|
||||
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
100, // 最小更新间隔 100ms
|
||||
0.1f, // 最小位移 0.1米
|
||||
locationListener
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// GPS位置监听
|
||||
private val locationListener = object : LocationListener {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onLocationChanged(location: Location) {
|
||||
// 速度计算
|
||||
// 1. 当前速度
|
||||
val currentSpeed = location.speed * 3.6f // m/s -> km/h
|
||||
val speedKmh = currentSpeed.toInt()
|
||||
tvSpeed.text = speedKmh.toString()
|
||||
speedUpdateListener?.onSpeedChanged(speedKmh)
|
||||
// 2. 最大速度
|
||||
if (currentSpeed > maxSpeed) maxSpeed = currentSpeed
|
||||
// 3. 累计平均速度
|
||||
if (currentSpeed > 0) {
|
||||
totalSpeed += currentSpeed
|
||||
speedCount++
|
||||
}
|
||||
|
||||
// ======================
|
||||
// 里程计算(核心代码)
|
||||
// ======================
|
||||
if (lastLocation != null) {
|
||||
// 计算两点之间距离(单位:米)
|
||||
val distance = lastLocation!!.distanceTo(location)
|
||||
// 过滤掉跳跃GPS信号(小于100米才累计)
|
||||
if (distance in 0.0f..100.0f) {
|
||||
totalDistance += distance
|
||||
// ========== 开始行驶计时(新增) ==========
|
||||
|
||||
if (!isRunning && speedKmh > 1.0f) {
|
||||
if (startFirstTime == 0L && RideState) {
|
||||
totalSpeed=0f
|
||||
totalDistance=0f
|
||||
totalDurationMs=0L
|
||||
startFirstTime = System.currentTimeMillis();
|
||||
startLocation = location
|
||||
}
|
||||
startTime = System.currentTimeMillis()
|
||||
isRunning = true
|
||||
}
|
||||
// 速度过低停止计时
|
||||
else if (isRunning && speedKmh < 0.5f) {
|
||||
totalDurationMs += System.currentTimeMillis() - startTime
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
lastLocation = location
|
||||
// ========== 计算总耗时 ==========
|
||||
var currentMs = totalDurationMs
|
||||
if (isRunning) {
|
||||
currentMs += System.currentTimeMillis() - startTime
|
||||
}
|
||||
// 转成 00:00:00 格式
|
||||
val seconds = (currentMs / 1000) % 60
|
||||
val minutes = (currentMs / (1000 * 60)) % 60
|
||||
val hours = (currentMs / (1000 * 60 * 60)) % 24
|
||||
val durationStr = String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||
tvDuration.text = "行驶: $durationStr" // 显示耗时
|
||||
// 显示:保留2位小数,转成公里
|
||||
val km = totalDistance / 1000f
|
||||
tvDistance.text ="里程: "+ String.format("%.2f km", km)
|
||||
val endTime = System.currentTimeMillis()
|
||||
val rideMs = if (isRunning) {
|
||||
totalDurationMs + (endTime - startTime)
|
||||
} else {
|
||||
totalDurationMs
|
||||
}
|
||||
if(auto_ride_speed>0)
|
||||
{
|
||||
//如果低速,则记录初始低速的时间
|
||||
if(currentSpeed<auto_ride_speed)
|
||||
{
|
||||
if(lowSpeedStartTime==0L)
|
||||
{
|
||||
val totalMs = if (isRunning) {
|
||||
totalDurationMs + (endTime - startTime)
|
||||
} else {
|
||||
totalDurationMs
|
||||
}
|
||||
val start_addr=getAddress(startLocation)
|
||||
val end_addr=getAddress(endLocation)
|
||||
lowSpeedStartTime= System.currentTimeMillis()
|
||||
lastDriveRecord=DriveRecord(
|
||||
distance = totalDistance / 1000f,
|
||||
avgSpeed = if (speedCount == 0) 0f else totalSpeed / speedCount,
|
||||
maxSpeed = maxSpeed,
|
||||
durationMs = totalMs,
|
||||
startLocation =startLocation,
|
||||
endLocation =endLocation,
|
||||
startTime = startFirstTime,
|
||||
endTime = endTime,
|
||||
startLocationJson = getAddressJson(start_addr),
|
||||
startLocationStr = getAddressStr(start_addr),
|
||||
endLocationStr =getAddressStr(end_addr),
|
||||
endLocationJson = getAddressJson(end_addr),
|
||||
)
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(lowSpeedStartTime>0L)
|
||||
{
|
||||
lowSpeedStartTime=0L
|
||||
lastDriveRecord=null
|
||||
}
|
||||
}
|
||||
}
|
||||
val intent = Intent(ACTION_REAL_TIME).apply {
|
||||
putExtra("RideState", RideState)
|
||||
putExtra("maxSpeed", maxSpeed)
|
||||
putExtra("avgSpeed", if (speedCount == 0) 0f else totalSpeed / speedCount)
|
||||
putExtra("realSpeed", currentSpeed.toInt())
|
||||
putExtra("rideMs", rideMs)
|
||||
putExtra("totalDistance", String.format("%.2f km", km))
|
||||
putExtra("startFirstTime", startFirstTime)
|
||||
if(startFirstTime==0L)
|
||||
{
|
||||
putExtra("totalMs", 0)
|
||||
}
|
||||
else {
|
||||
putExtra("totalMs", System.currentTimeMillis() - startFirstTime)
|
||||
}
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
|
||||
override fun onProviderEnabled(provider: String) {}
|
||||
override fun onProviderDisabled(provider: String) {}
|
||||
}
|
||||
|
||||
// 前台服务通知(防止后台被杀)
|
||||
private fun startForegroundNotification() {
|
||||
val channelId = "GPS_SPEED_SERVICE"
|
||||
val channel = NotificationChannel(
|
||||
channelId, "GPS测速服务", NotificationManager.IMPORTANCE_LOW
|
||||
).apply { enableVibration(false) }
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
|
||||
val notification: Notification = NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle("GPS测速中")
|
||||
.setContentText("悬浮窗实时显示速度")
|
||||
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
startForeground(1001, notification)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (floatingView.parent != null) {
|
||||
if (::floatingView.isInitialized) windowManager.removeView(floatingView)
|
||||
}
|
||||
if (::locationManager.isInitialized) locationManager.removeUpdates(locationListener)
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
super.onDestroy()
|
||||
}
|
||||
private fun getAddressStr(adder:android.location.Address?): String{
|
||||
val address = adder?.let { adder ->
|
||||
if(adder.featureName.indexOf("广场")>0)
|
||||
{
|
||||
adder.featureName
|
||||
}
|
||||
else {
|
||||
listOfNotNull(
|
||||
//adder.adminArea, // 省
|
||||
// adder.locality, // 市
|
||||
//adder.subLocality, // 区
|
||||
adder.thoroughfare, // 街道
|
||||
adder.subThoroughfare,// 门牌号
|
||||
adder.featureName // 小区/大厦
|
||||
).filter { it.isNotBlank() }.joinToString(" ")
|
||||
}
|
||||
} ?: "未知位置"
|
||||
return address;
|
||||
}
|
||||
/** 编译时宏:只有为 true 时,代码块才会被编译进去 */
|
||||
inline fun ifBuild(cond: Boolean, block: () -> Unit) {
|
||||
if (cond) block()
|
||||
}
|
||||
private fun getAddressJson(adder:android.location.Address?): String{
|
||||
val json = JSONObject()
|
||||
adder?.let { it ->
|
||||
json.put("code", 200)
|
||||
json.put("country", it.countryName ?: "")
|
||||
json.put("province", it.adminArea ?: "")
|
||||
json.put("city", it.locality ?: "")
|
||||
json.put("district", it.subLocality ?: "")
|
||||
json.put("street", it.thoroughfare ?: "")
|
||||
json.put("subThoroughfare", it.subThoroughfare ?: "")
|
||||
json.put("detail", it.featureName ?: "")
|
||||
json.put("address", "${it.thoroughfare ?: ""} ${it.subThoroughfare ?: ""} ${it.featureName ?: ""}".trim())
|
||||
} ?: run {
|
||||
json.put("code", 404)
|
||||
json.put("address", "暂无地址")
|
||||
}
|
||||
return json.toString()
|
||||
}
|
||||
private fun getAddress(location: Location?): android.location.Address? {
|
||||
if(location==null){return null}
|
||||
return getAddress(location.latitude,location.longitude);
|
||||
}
|
||||
// GPS 经纬度 转 详细地址
|
||||
// 🔥 新版:GPS经纬度转地址(已修复过时方法,兼容所有安卓版本)
|
||||
private fun getAddress(lat: Double, lng: Double): android.location.Address? {
|
||||
if (lat == 0.0 || lng == 0.0) return null
|
||||
|
||||
try {
|
||||
val geocoder = Geocoder(this, Locale.getDefault())
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// 异步模式,必须等待结果(同步阻塞获取,保证能返回)
|
||||
val latch = java.util.concurrent.CountDownLatch(1)
|
||||
var result: android.location.Address? = null
|
||||
|
||||
geocoder.getFromLocation(lat, lng, 1) { addresses ->
|
||||
if (addresses.isNotEmpty()) {
|
||||
result= addresses[0]
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await() // 等待回调完成
|
||||
return result
|
||||
} else {
|
||||
// 旧版同步,直接返回
|
||||
@Suppress("DEPRECATION")
|
||||
val addresses = geocoder.getFromLocation(lat, lng, 1)
|
||||
return if (!addresses.isNullOrEmpty()) {
|
||||
addresses[0]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
// 上传行驶报表到服务器
|
||||
private fun uploadReport(record: DriveRecord) {
|
||||
Thread {
|
||||
try {
|
||||
//如果骑行时间少于15秒,则不进行上传
|
||||
if (record.durationMs <= 15000) {
|
||||
return@Thread
|
||||
}
|
||||
// 1. 构建 JSON 格式数据(你要的全部字段)
|
||||
val start_address_jo = try {
|
||||
JSONObject(record.startLocationJson ?: "{}")
|
||||
} catch (e: Exception) {
|
||||
JSONObject()
|
||||
}
|
||||
if (record.startLocation != null) {
|
||||
start_address_jo.put("lat", record.startLocation.latitude)
|
||||
start_address_jo.put("lng", record.startLocation.longitude)
|
||||
}
|
||||
val end_address_jo = try {
|
||||
JSONObject(record.endLocationJson ?: "{}")
|
||||
} catch (e: Exception) {
|
||||
JSONObject()
|
||||
}
|
||||
if (record.endLocation != null) {
|
||||
end_address_jo.put("lat", record.endLocation.latitude)
|
||||
end_address_jo.put("lng", record.endLocation.longitude)
|
||||
}
|
||||
val json = org.json.JSONObject().apply {
|
||||
put("distance", record.distance) // 里程
|
||||
put("avg_speed", record.avgSpeed) // 平均速度
|
||||
put("max_speed", record.maxSpeed) // 最高速度
|
||||
put("drive_duration", record.durationMs / 1000) // 秒数
|
||||
put("start_time", record.startTime / 1000) // 时间戳
|
||||
put("end_time", record.endTime / 1000) // 时间戳
|
||||
put("start_address", record.startLocationStr ?: "") // 开始地址
|
||||
put("end_address", record.endLocationStr ?: "")
|
||||
put("start_address_jo", start_address_jo)
|
||||
put("end_address_jo", end_address_jo)
|
||||
// 结束地址
|
||||
}.toString()
|
||||
// if (BuildConfig.) {
|
||||
// 编译时才会包含的代码
|
||||
// }
|
||||
|
||||
ifBuild(BuildConfig.PrivateSoft) {
|
||||
// 发送 POST
|
||||
val url = java.net.URL("http://gs2.dangyou.com:8081/xin.aspx?api=SaveDriveLog")
|
||||
val conn = url.openConnection() as java.net.HttpURLConnection
|
||||
conn.requestMethod = "POST"
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8")
|
||||
conn.doOutput = true
|
||||
|
||||
conn.outputStream.use { it.write(json.toByteArray(Charsets.UTF_8)) }
|
||||
conn.responseCode
|
||||
conn.disconnect()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
override fun onBind(intent: Intent): IBinder? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.itrycn.gpsspeed.Fragment
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.itrycn.gpsspeed.R
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
@SuppressLint("ResourceType")
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_main, rootKey)
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// listView.setPadding(0, dp2px(68), 0, 0)
|
||||
//listView.clipToPadding = false
|
||||
// 关键3:移除RecyclerView自带默认顶部margin,杜绝叠加空白
|
||||
//listView.setClipChildren(false)
|
||||
}
|
||||
|
||||
private fun dp2px(dp: Int): Int {
|
||||
return (dp * resources.displayMetrics.density + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
package com.itrycn.gpsspeed
|
||||
|
||||
import UpdateInfo
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.edit
|
||||
import com.itrycn.gpsspeed.databinding.ActivityMainBinding
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import androidx.core.graphics.toColorInt
|
||||
|
||||
class MainActivity : AppCompatActivity(), FloatingWindowService.SpeedUpdateListener {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var startFirstTime: Long = 0 // 第一次开始行驶时间
|
||||
private var lowSpeedStartTime: Long = 0 //低速行驶开始的时间
|
||||
private var isServiceRunning = false
|
||||
private var AutoOpen = true
|
||||
private var auto_ride:Int = 0
|
||||
private var auto_stop_ride:Int = 0
|
||||
// 权限请求码
|
||||
private val REQUEST_LOCATION = 1001
|
||||
private val REQUEST_OVERLAY = 1002
|
||||
private lateinit var receiver: BroadcastReceiver
|
||||
private lateinit var tvRealSpeed: TextView
|
||||
private lateinit var tvCurrentTime: TextView
|
||||
private lateinit var tvRideTime: TextView
|
||||
private lateinit var tvTotalTime: TextView
|
||||
private lateinit var tvTotalDistance: TextView
|
||||
private lateinit var tvAvgSpeed: TextView
|
||||
private lateinit var tvHighestSpeed: TextView
|
||||
private val exitInterval = 2000L
|
||||
private var lastBackTime = 0L
|
||||
private var toast: Toast? = null
|
||||
// 你的服务器json地址
|
||||
private val UPDATE_JSON_URL = "https://files.cnblogs.com/files/zilin/GPSSpeed.json"
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
// 每秒刷新系统时间
|
||||
private val timeRunnable = object : Runnable {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun run() {
|
||||
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||
tvCurrentTime.text = sdf.format(Date())
|
||||
if(startFirstTime==0L)
|
||||
{
|
||||
tvTotalTime.text="00:00:00";
|
||||
}
|
||||
else {
|
||||
tvTotalTime.text=formatTime(System.currentTimeMillis() - startFirstTime)
|
||||
}
|
||||
mainHandler.postDelayed(this, 1000)
|
||||
}
|
||||
}
|
||||
override fun onNewIntent(newIntent: Intent?) {
|
||||
super.onNewIntent(newIntent)
|
||||
// 关键:把新Intent赋值给页面,后续onResume才能读到
|
||||
setIntent(newIntent)
|
||||
}
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if(intent!=null) {
|
||||
if (intent.getBooleanExtra("StopRide", false)) {
|
||||
stopSpeedService()
|
||||
}
|
||||
if (intent.getBooleanExtra("RideData", false)) {
|
||||
val avgSpeed = intent.getFloatExtra("avgSpeed", 0f)
|
||||
val speed = intent.getIntExtra("realSpeed", 0)
|
||||
val rideMs = intent.getLongExtra("rideMs", 0L)
|
||||
val totalMs = intent.getLongExtra("totalMs", 0L)
|
||||
startFirstTime = intent.getLongExtra("startFirstTime", 0L)
|
||||
tvRealSpeed.text = speed.toString()
|
||||
tvRideTime.text = formatTime(rideMs)
|
||||
tvTotalTime.text = formatTime(totalMs)
|
||||
tvTotalDistance.text = intent.getStringExtra("totalDistance")
|
||||
tvAvgSpeed.text = "%.1f km/h".format(avgSpeed)
|
||||
tvHighestSpeed.text = "%.1f km/h".format(intent.getFloatExtra("maxSpeed", 0f))
|
||||
if (intent.getBooleanExtra("RideState", false)) {
|
||||
isServiceRunning = true
|
||||
binding.btnStart.text = "停止骑行"
|
||||
binding.btnStart.setTextColor("#FF0000".toColorInt())
|
||||
}
|
||||
else{
|
||||
isServiceRunning=false
|
||||
}
|
||||
}
|
||||
}
|
||||
loadSetting()
|
||||
}
|
||||
private fun checkAppUpdate() {
|
||||
val sp = getSharedPreferences("temp_data", Context.MODE_PRIVATE)
|
||||
val checkUpdateTime = sp.getLong("CheckUpdateTime", 0L)
|
||||
if((System.currentTimeMillis()-checkUpdateTime)/60000<720)//小于12小时没检测
|
||||
{
|
||||
return
|
||||
}
|
||||
UpdateCheckUtil.checkUpdate(UPDATE_JSON_URL) { info ->
|
||||
info ?: return@checkUpdate // 网络失败直接返回
|
||||
|
||||
val localCode = UpdateCheckUtil.getLocalVersionCode(this)
|
||||
// 线上版本更大,需要更新
|
||||
if (info.versionCode > localCode) {
|
||||
getSharedPreferences("temp_data", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putLong("CheckUpdateTime", System.currentTimeMillis())
|
||||
}
|
||||
runOnUiThread {
|
||||
showUpdateDialog(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出更新弹窗
|
||||
private fun showUpdateDialog(info: UpdateInfo) {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_update, null)
|
||||
val tvContent = dialogView.findViewById<TextView>(R.id.tv_update_content)
|
||||
tvContent.text = info.updateContent
|
||||
dialogView.findViewById<TextView>(R.id.tv_update_title).text="发现新版本 V"+info.versionName
|
||||
val builder = AlertDialog.Builder(this)
|
||||
.setView(dialogView)
|
||||
.setCancelable(false) // 默认不可取消
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
// 立即更新按钮
|
||||
dialogView.findViewById<android.widget.Button>(R.id.btn_update_now).setOnClickListener {
|
||||
// 跳转浏览器下载APK
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(info.downloadUrl))
|
||||
startActivity(intent)
|
||||
if (!info.forceUpdate) dialog.dismiss()
|
||||
}
|
||||
|
||||
// 稍后更新按钮,强制更新则隐藏
|
||||
val btnLater = dialogView.findViewById<android.widget.Button>(R.id.btn_update_later)
|
||||
if (info.forceUpdate) {
|
||||
btnLater.visibility = android.view.View.GONE
|
||||
} else {
|
||||
btnLater.setOnClickListener { dialog.dismiss() }
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
private fun loadSetting()
|
||||
{
|
||||
// 强制横屏
|
||||
// 获取默认 SharedPreferences
|
||||
val sp = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if(sp.getBoolean("landscape", false))
|
||||
{
|
||||
setRequestedOrientation(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
||||
}
|
||||
// 绑定中控控件
|
||||
val llLeft:LinearLayout= findViewById(R.id.llLeft)
|
||||
val llRight:LinearLayout= findViewById(R.id.llRight)
|
||||
val layoutParams_left = llLeft.layoutParams as ConstraintLayout.LayoutParams
|
||||
val layoutParams_right = llRight.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams_left.width=400
|
||||
layoutParams_right.width=400
|
||||
if(sp.getBoolean("left_show", true))
|
||||
{
|
||||
layoutParams_left.startToStart=ConstraintLayout.LayoutParams.PARENT_ID
|
||||
layoutParams_left.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
layoutParams_left.endToEnd = ConstraintLayout.LayoutParams.UNSET
|
||||
//
|
||||
// 设置:顶部贴父布局顶部、右侧贴父布局右侧
|
||||
layoutParams_right.startToStart= ConstraintLayout.LayoutParams.UNSET
|
||||
layoutParams_right.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
layoutParams_right.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
}
|
||||
else {
|
||||
// 设置:顶部贴父布局顶部、右侧贴父布局右侧
|
||||
layoutParams_left.startToStart= ConstraintLayout.LayoutParams.UNSET
|
||||
layoutParams_left.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
layoutParams_left.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
//
|
||||
layoutParams_right.startToStart=ConstraintLayout.LayoutParams.PARENT_ID
|
||||
layoutParams_right.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
layoutParams_right.endToEnd = ConstraintLayout.LayoutParams.UNSET
|
||||
}
|
||||
// 应用参数
|
||||
llLeft.layoutParams = layoutParams_left
|
||||
llRight.layoutParams = layoutParams_right
|
||||
auto_ride=sp.getInt("auto_ride_speed", 10)
|
||||
auto_stop_ride=sp.getInt("stop_ride_time", 2)
|
||||
// 1. 获取根布局
|
||||
val rootLayout = findViewById<androidx.constraintlayout.widget.ConstraintLayout>(R.id.uiWidget)
|
||||
// 2. 设置背景色
|
||||
// 写法1:直接十六进制色值
|
||||
rootLayout.setBackgroundColor(android.graphics.Color.parseColor(sp.getString("key_theme_color", "#000000")))
|
||||
}
|
||||
@SuppressLint("SetTextI18n")
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// 全屏沉浸式
|
||||
window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
tvRealSpeed = findViewById(R.id.tvRealSpeed)
|
||||
tvCurrentTime = findViewById(R.id.tvCurrentTime)
|
||||
tvRideTime = findViewById(R.id.tvRideTime)
|
||||
tvTotalTime = findViewById(R.id.tvTotalTime)
|
||||
tvTotalDistance = findViewById(R.id.tvTotalDistance)
|
||||
tvAvgSpeed= findViewById(R.id.tvAvgSpeed)
|
||||
tvHighestSpeed= findViewById(R.id.tvHighestSpeed)
|
||||
loadSetting()
|
||||
// 开始刷新系统时间
|
||||
mainHandler.post(timeRunnable)
|
||||
|
||||
// 注册广播接收器(实时报表 + 实时速度时间)
|
||||
receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
FloatingWindowService.ACTION_REPORT_UPDATE -> {
|
||||
val distance = intent.getFloatExtra("distance", 0f)
|
||||
val avgSpeed = intent.getFloatExtra("avgSpeed", 0f)
|
||||
val maxSpeed = intent.getFloatExtra("maxSpeed", 0f)
|
||||
val durationMs = intent.getLongExtra("durationMs", 0L)
|
||||
val startTime = intent.getLongExtra("startTime", 0L)
|
||||
val endTime = intent.getLongExtra("endTime", 0L)
|
||||
|
||||
showReport(
|
||||
FloatingWindowService.DriveRecord(
|
||||
distance = distance,
|
||||
avgSpeed = avgSpeed,
|
||||
maxSpeed = maxSpeed,
|
||||
durationMs = durationMs,
|
||||
startTime = startTime,
|
||||
endTime = endTime,
|
||||
startLocation = null,
|
||||
startLocationStr = intent.getStringExtra("startLocationStr"),
|
||||
endLocationStr = intent.getStringExtra("endLocationStr"),
|
||||
endLocation = null,
|
||||
startLocationJson = null,
|
||||
endLocationJson = null
|
||||
)
|
||||
)
|
||||
}
|
||||
// 接收实时速度、骑行时间、总时间
|
||||
FloatingWindowService.ACTION_REAL_TIME -> {
|
||||
val avgSpeed = intent.getFloatExtra("avgSpeed", 0f)
|
||||
val speed = intent.getIntExtra("realSpeed", 0)
|
||||
val rideMs = intent.getLongExtra("rideMs", 0L)
|
||||
val totalMs = intent.getLongExtra("totalMs", 0L)
|
||||
val RideState=intent.getBooleanExtra("RideState", false)
|
||||
startFirstTime= intent.getLongExtra("startFirstTime", 0L)
|
||||
tvRealSpeed.text = speed.toString()
|
||||
tvRideTime.text = formatTime(rideMs)
|
||||
tvTotalTime.text = formatTime(totalMs)
|
||||
tvTotalDistance.text = intent.getStringExtra("totalDistance")
|
||||
tvAvgSpeed.text="%.1f km/h".format(avgSpeed)
|
||||
tvHighestSpeed.text="%.1f km/h".format( intent.getFloatExtra("maxSpeed", 0f))
|
||||
if(!RideState && speed>=auto_ride && !isServiceRunning && AutoOpen)
|
||||
{
|
||||
startSpeedService()
|
||||
}
|
||||
if(RideState && !isServiceRunning)
|
||||
{
|
||||
isServiceRunning=true
|
||||
binding.btnStart.text="停止骑行"
|
||||
binding.btnStart.setTextColor("#FF0000".toColorInt())
|
||||
}
|
||||
}
|
||||
FloatingWindowService.ACTION_CLOSE_FLOAT -> {
|
||||
//stopSpeedService()
|
||||
//Toast.makeText(context, "接收到", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(FloatingWindowService.ACTION_REPORT_UPDATE)
|
||||
addAction(FloatingWindowService.ACTION_REAL_TIME)
|
||||
addAction(FloatingWindowService.ACTION_CLOSE_FLOAT)
|
||||
}
|
||||
registerReceiver(receiver, filter, RECEIVER_EXPORTED)
|
||||
|
||||
binding.btnStart.setOnClickListener {
|
||||
if(!isServiceRunning)
|
||||
{
|
||||
if (checkPermissions()) startSpeedService()
|
||||
else requestAllPermissions()
|
||||
}
|
||||
else{
|
||||
AutoOpen=false
|
||||
stopSpeedService()
|
||||
}
|
||||
//startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
binding.btnSetting.setOnClickListener {
|
||||
if(isServiceRunning)
|
||||
{
|
||||
Toast.makeText(this, "骑行时不允许修改设置。", Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
checkAppUpdate()
|
||||
// 注册返回按键/手势统一回调,全版本兼容
|
||||
val backCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
// 先判断Fragment回退栈,有页面先返回Fragment
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
return
|
||||
}
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastBackTime < exitInterval) {
|
||||
toast?.cancel()
|
||||
// 关闭悬浮窗服务(按需开启)
|
||||
// stopService(Intent(this@MainActivity, FloatService::class.java))
|
||||
//finish()
|
||||
finishAndRemoveTask()
|
||||
} else {
|
||||
toast = Toast.makeText(this@MainActivity, "再按一次返回键退出应用", Toast.LENGTH_SHORT)
|
||||
toast?.show()
|
||||
lastBackTime = currentTime
|
||||
}
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, backCallback)
|
||||
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
// 横屏
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
tvRealSpeed.textSize = 120f
|
||||
}
|
||||
// 竖屏
|
||||
else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
tvRealSpeed.textSize = 80f
|
||||
}
|
||||
}
|
||||
// 毫秒转 00:00:00
|
||||
@SuppressLint("DefaultLocale")
|
||||
private fun formatTime(ms: Long): String {
|
||||
val h = (ms / 3600000) % 24
|
||||
val m = (ms / 60000) % 60
|
||||
val s = (ms / 1000) % 60
|
||||
return String.format("%02d:%02d:%02d", h, m, s)
|
||||
}
|
||||
|
||||
// 前后台控制悬浮窗
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (isServiceRunning) {
|
||||
val intent = Intent(this, FloatingWindowService::class.java)
|
||||
intent.action = FloatingWindowService.ACTION_SHOW_FLOAT
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val intent = Intent(this, FloatingWindowService::class.java)
|
||||
intent.action = FloatingWindowService.ACTION_HIDE_FLOAT
|
||||
startService(intent)
|
||||
}
|
||||
|
||||
// 检查悬浮窗+定位权限
|
||||
private fun checkPermissions(): Boolean {
|
||||
val overlayPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Settings.canDrawOverlays(this)
|
||||
} else true
|
||||
|
||||
val locationPermission = ActivityCompat.checkSelfPermission(
|
||||
this, android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
|
||||
return overlayPermission && locationPermission
|
||||
}
|
||||
|
||||
// 申请权限
|
||||
private fun requestAllPermissions() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
|
||||
startActivityForResult(intent, REQUEST_OVERLAY)
|
||||
}
|
||||
ActivityCompat.requestPermissions(
|
||||
this, arrayOf(
|
||||
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
), REQUEST_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
// 启动测速服务
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun startSpeedService() {
|
||||
if (isServiceRunning) return
|
||||
isServiceRunning = true
|
||||
binding.btnStart.text="停止骑行"
|
||||
binding.btnStart.setTextColor("#FF0000".toColorInt())
|
||||
tvRealSpeed.text = "0"
|
||||
tvRideTime.text = "00:00:00"
|
||||
tvTotalTime.text = "00:00:00"
|
||||
tvTotalDistance.text = "0.00 km"
|
||||
tvAvgSpeed.text = "0.0 km/h"
|
||||
tvHighestSpeed.text = "0.0 km/h"
|
||||
val intent = Intent(this, FloatingWindowService::class.java)
|
||||
intent.putExtra("start_ride",true)
|
||||
intent.putExtra("auto_ride_speed",auto_ride)
|
||||
intent.putExtra("stop_ride_time",auto_stop_ride)
|
||||
intent.action = FloatingWindowService.ACTION_HIDE_FLOAT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
binding.reportLayout.visibility = android.view.View.GONE
|
||||
Toast.makeText(this, "GPS测速已启动", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
// 停止服务
|
||||
private fun stopSpeedService() {
|
||||
isServiceRunning = false
|
||||
binding.btnStart.text="开始骑行"
|
||||
binding.btnStart.setTextColor("#028678".toColorInt())
|
||||
val intent = Intent(this, FloatingWindowService::class.java)
|
||||
intent.putExtra("end_ride",true)
|
||||
intent.action = FloatingWindowService.ACTION_HIDE_FLOAT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
binding.reportLayout.visibility = android.view.View.GONE
|
||||
FloatingWindowService.lastDriveRecord?.let { showReport(it) }
|
||||
Toast.makeText(this, "已停止测速", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n", "DefaultLocale")
|
||||
private fun showReport(record: FloatingWindowService.DriveRecord) {
|
||||
binding.reportLayout.visibility = android.view.View.VISIBLE
|
||||
|
||||
val ms = record.durationMs
|
||||
val h = (ms / 3600000) % 24
|
||||
val m = (ms / 60000) % 60
|
||||
val s = (ms / 1000) % 60
|
||||
val duration = String.format("%02d:%02d:%02d", h, m, s)
|
||||
|
||||
val totalMs = if (record.startTime == 0L) 0L else (record.endTime - record.startTime)
|
||||
val totalH = (totalMs / 3600000) % 24
|
||||
val totalM = (totalMs / 60000) % 60
|
||||
val totalS = (totalMs / 1000) % 60
|
||||
val totalDuration = String.format("%02d:%02d:%02d", totalH, totalM, totalS)
|
||||
|
||||
val sdf = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault())
|
||||
val start = if (record.startTime == 0L) "未行驶" else sdf.format(record.startTime)
|
||||
val end = sdf.format(record.endTime)
|
||||
val startLocation = record.startLocationStr
|
||||
val endLocation = record.endLocationStr
|
||||
|
||||
binding.reportDistance.text = "总里程:%.2f km".format(record.distance)
|
||||
binding.reportAvgSpeed.text = "平均速度:%.1f km/h".format(record.avgSpeed)
|
||||
binding.reportMaxSpeed.text = "最高速度:%.1f km/h".format(record.maxSpeed)
|
||||
binding.reportDuration.text = "行驶耗时:$duration"
|
||||
binding.reportTotalDuration.text = "总耗时:$totalDuration"
|
||||
binding.reportMoney.text = "资费:%.2f 元".format(record.distance * 0.9)
|
||||
binding.reportStartTime.text = "开始时间:$start"
|
||||
binding.reportEndTime.text = "结束时间:$end"
|
||||
binding.reportStartAddress.text = "起点:$startLocation"
|
||||
binding.reportEndAddress.text = "目的地:$endLocation"
|
||||
}
|
||||
|
||||
override fun onSpeedChanged(speed: Int) {}
|
||||
override fun onDriveEnd(drive: FloatingWindowService.DriveRecord) {
|
||||
runOnUiThread { showReport(drive) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
mainHandler.removeCallbacks(timeRunnable)
|
||||
unregisterReceiver(receiver)
|
||||
val intent = Intent(this, FloatingWindowService::class.java)
|
||||
stopService(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.itrycn.gpsspeed
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.itrycn.gpsspeed.Fragment.SettingsFragment
|
||||
import com.itrycn.gpsspeed.R
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// 加载布局,不再直接替换根content
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, SettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import com.google.gson.Gson
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.IOException
|
||||
|
||||
object UpdateCheckUtil {
|
||||
private val client = OkHttpClient()
|
||||
private val gson = Gson()
|
||||
|
||||
// 检测更新
|
||||
fun checkUpdate(url: String, callback: (UpdateInfo?) -> Unit) {
|
||||
Thread {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
try {
|
||||
val resp = client.newCall(request).execute()
|
||||
if (resp.isSuccessful) {
|
||||
val json = resp.body?.string()
|
||||
val info = gson.fromJson(json, UpdateInfo::class.java)
|
||||
callback(info)
|
||||
} else {
|
||||
callback(null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
callback(null)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// 获取本地 versionCode
|
||||
fun getLocalVersionCode(context: android.content.Context): Int {
|
||||
val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
return pkgInfo.versionCode
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
data class UpdateInfo(
|
||||
val versionCode: Int,
|
||||
val versionName: String,
|
||||
val forceUpdate: Boolean,
|
||||
val updateContent: String,
|
||||
val downloadUrl: String
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.itrycn.gpsspeed.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.itrycn.gpsspeed.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GPSSpeedTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.itrycn.gpsspeed.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.itrycn.gpsspeed
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
5
Source/GPSSpeed/build.gradle
Normal file
5
Source/GPSSpeed/build.gradle
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
666
Source/GPSSpeed/build/reports/problems/problems-report.html
Normal file
666
Source/GPSSpeed/build/reports/problems/problems-report.html
Normal file
File diff suppressed because one or more lines are too long
23
Source/GPSSpeed/gradle.properties
Normal file
23
Source/GPSSpeed/gradle.properties
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
13
Source/GPSSpeed/gradle/gradle-daemon-jvm.properties
Normal file
13
Source/GPSSpeed/gradle/gradle-daemon-jvm.properties
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#This file is generated by updateDaemonJvm
|
||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
||||
toolchainVendor=JETBRAINS
|
||||
toolchainVersion=21
|
||||
37
Source/GPSSpeed/gradle/libs.versions.toml
Normal file
37
Source/GPSSpeed/gradle/libs.versions.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[versions]
|
||||
agp = "9.2.1"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
kotlin = "2.2.10"
|
||||
composeBom = "2024.09.00"
|
||||
appcompat = "1.7.1"
|
||||
preference = "1.2.1"
|
||||
preferenceKtx = "1.2.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
BIN
Source/GPSSpeed/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
Source/GPSSpeed/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
8
Source/GPSSpeed/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
8
Source/GPSSpeed/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#Fri Mar 06 08:40:15 CST 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
251
Source/GPSSpeed/gradlew
vendored
Normal file
251
Source/GPSSpeed/gradlew
vendored
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
Source/GPSSpeed/gradlew.bat
vendored
Normal file
94
Source/GPSSpeed/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
10
Source/GPSSpeed/local.properties
Normal file
10
Source/GPSSpeed/local.properties
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
## This file is automatically generated by Android Studio.
|
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||
#
|
||||
# This file should *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
sdk.dir=C\:\\Users\\zilin\\AppData\\Local\\Android\\Sdk
|
||||
26
Source/GPSSpeed/settings.gradle
Normal file
26
Source/GPSSpeed/settings.gradle
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "GPSSpeed"
|
||||
include ':app'
|
||||
Loading…
Reference in New Issue
Block a user