*.[修复]修复重新骑行时,最高速度等信息有时没有重新计算的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