安卓app

This commit is contained in:
xuyvqi 2025-07-02 17:48:41 +08:00
commit c9323493e2
90 changed files with 8112 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

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

@ -0,0 +1,83 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "com.example.myapplication"
compileSdk = 35
defaultConfig {
applicationId = "com.example.myapplication"
minSdk = 28
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
"room.schemaLocation" to "$projectDir/schemas".toString(),
"room.incremental" to "true"
)
}
}
}
buildTypes {
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// 添加以下配置确保混淆规则生效
matchingFallbacks += listOf("release")
}
debug {
isMinifyEnabled = false // 确保debug不混淆
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.room.runtime)
implementation(libs.firebase.crashlytics.buildtools)
annotationProcessor(libs.room.compiler)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraintlayout)
implementation(libs.navigation.fragment)
implementation(libs.navigation.ui)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
implementation(libs.okhttp)
implementation("com.google.android.gms:play-services-location:21.0.1")
implementation("androidx.exifinterface:exifinterface:1.3.3")
implementation ("commons-io:commons-io:2.11.0")
implementation ("com.getbase:floatingactionbutton:1.10.1")
implementation ("com.android.volley:volley:1.2.1")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation ("com.google.android.material:material:1.6.0")
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("com.squareup.retrofit2:adapter-rxjava2:2.9.0")
implementation ("com.squareup.okhttp3:logging-interceptor:4.9.3")
}

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

@ -0,0 +1,86 @@
# 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
# Retrofit
# Retrofit
# Retrofit
# 保留Retrofit接口
# 保留所有Retrofit接口及注解
# ========== 基础保留 ==========
-keepattributes Signature, RuntimeVisibleAnnotations, InnerClasses, EnclosingMethod
-keepnames class * { @retrofit2.http.* <methods>; }
# ========== Retrofit 保留规则 ==========
-keep interface ** { @retrofit2.http.* <methods>; }
-keep class retrofit2.** { *; }
-keepclasseswithmembers class * {
@retrofit2.* <methods>;
}
# ========== Gson 保留规则 ==========
-keep class com.google.gson.** { *; }
-keep class com.google.gson.stream.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# ========== 您的项目特定规则 ==========
# 替换 com.yourpackage 为您的实际包名
-keep class com.example.myapplication.api.** { *; } # 接口包
-keep class com.example.myapplication.model.** { *; } # 模型包
-keepclassmembers class com.example.myapplication.model.** { # 保留模型类成员
*;
}
# ========== Kotlin 支持 ==========
-keep class kotlin.** { *; }
-keep class kotlinx.** { *; }
-keepclassmembers class **$WhenMappings {
<fields>;
}
# ========== AndroidX 支持 ==========
-keep class androidx.** { *; }
-dontwarn androidx.**
# ========== 第三方库警告抑制 ==========
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.jetbrains.annotations.**
-dontwarn okio.**
-dontwarn okhttp3.**
-dontwarn retrofit2.**
# ========== 其他必要保留 ==========
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service

BIN
app/release/app-release.apk Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.example.myapplication",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 28
}

View File

@ -0,0 +1,70 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "4665a635f55b3fbad657fbed322d6c2a",
"entities": [
{
"tableName": "images",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT, `time` INTEGER NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `altitude` REAL NOT NULL, `user` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "altitude",
"columnName": "altitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4665a635f55b3fbad657fbed322d6c2a')"
]
}
}

View File

@ -0,0 +1,100 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "c0393d724bc510f7bf4da3184d917c49",
"entities": [
{
"tableName": "images",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT, `time` INTEGER NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `altitude` REAL NOT NULL, `user` TEXT, `audioPath` TEXT, `blade` INTEGER NOT NULL, `unit` TEXT, `project` TEXT, `b` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "altitude",
"columnName": "altitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "audioPath",
"columnName": "audioPath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "blade",
"columnName": "blade",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "project",
"columnName": "project",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "b",
"columnName": "b",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c0393d724bc510f7bf4da3184d917c49')"
]
}
}

View File

@ -0,0 +1,132 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "5f74a6e038a673526c7d7aa21dfc54de",
"entities": [
{
"tableName": "images",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT, `time` INTEGER NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `altitude` REAL NOT NULL, `user` TEXT, `audioPath` TEXT, `blade` INTEGER NOT NULL, `unit` TEXT, `project` TEXT, `b` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "altitude",
"columnName": "altitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "audioPath",
"columnName": "audioPath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "blade",
"columnName": "blade",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "project",
"columnName": "project",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "b",
"columnName": "b",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "turbines",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`turbineId` TEXT NOT NULL, `turbineName` TEXT, `projectId` TEXT, PRIMARY KEY(`turbineId`))",
"fields": [
{
"fieldPath": "turbineId",
"columnName": "turbineId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "turbineName",
"columnName": "turbineName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "projectId",
"columnName": "projectId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"turbineId"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5f74a6e038a673526c7d7aa21dfc54de')"
]
}
}

View File

@ -0,0 +1,132 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "5f74a6e038a673526c7d7aa21dfc54de",
"entities": [
{
"tableName": "images",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT, `time` INTEGER NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `altitude` REAL NOT NULL, `user` TEXT, `audioPath` TEXT, `blade` INTEGER NOT NULL, `unit` TEXT, `project` TEXT, `b` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "altitude",
"columnName": "altitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "audioPath",
"columnName": "audioPath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "blade",
"columnName": "blade",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "project",
"columnName": "project",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "b",
"columnName": "b",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "turbines",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`turbineId` TEXT NOT NULL, `turbineName` TEXT, `projectId` TEXT, PRIMARY KEY(`turbineId`))",
"fields": [
{
"fieldPath": "turbineId",
"columnName": "turbineId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "turbineName",
"columnName": "turbineName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "projectId",
"columnName": "projectId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"turbineId"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5f74a6e038a673526c7d7aa21dfc54de')"
]
}
}

View File

@ -0,0 +1,194 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "65748a86249bdef977f472ddeba83ee9",
"entities": [
{
"tableName": "images",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT, `time` INTEGER NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `altitude` REAL NOT NULL, `user` TEXT, `audioPath` TEXT, `blade` INTEGER NOT NULL, `unit` TEXT, `project` TEXT, `b` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "altitude",
"columnName": "altitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "audioPath",
"columnName": "audioPath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "blade",
"columnName": "blade",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "project",
"columnName": "project",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "b",
"columnName": "b",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "turbines",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`turbineId` TEXT NOT NULL, `turbineName` TEXT, `projectId` TEXT, PRIMARY KEY(`turbineId`))",
"fields": [
{
"fieldPath": "turbineId",
"columnName": "turbineId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "turbineName",
"columnName": "turbineName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "projectId",
"columnName": "projectId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"turbineId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "audio_entities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fileName` TEXT, `AudioPath` TEXT, `ImagePath` TEXT, `fileSize` INTEGER NOT NULL, `time` TEXT, `duration` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileName",
"columnName": "fileName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "AudioPath",
"columnName": "AudioPath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "ImagePath",
"columnName": "ImagePath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fileSize",
"columnName": "fileSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65748a86249bdef977f472ddeba83ee9')"
]
}
}

View File

@ -0,0 +1,26 @@
package com.example.myapplication;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.example.myapplication", appContext.getPackageName());
}
}

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 基础权限 -->
<!-- Android 14+ 新增权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
tools:ignore="SelectedPhotoAccess" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--位置权限-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> <!-- Android 14+ 必须 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:windowSoftInputMode="stateVisible|adjustResize"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31"
tools:ignore="ForegroundServicePermission">
<activity
android:name=".MainActivity">
</activity>
<activity android:name=".AdminActivity" />
<activity android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".Service.FloatingWindowService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service android:name=".Service.RecordingService"
android:foregroundServiceType="microphone"
android:enabled="true"
android:exported="false"/>
<service
android:name=".Service.ForegroundService"
android:foregroundServiceType="microphone"
android:enabled="true"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
<queries>
<!-- 允许查询所有能处理图片选择的 Intent -->
<intent>
<action android:name="android.intent.action.PICK" />
<data android:mimeType="image/*" />
</intent>
<intent>
<action android:name="android.provider.action.PICK_IMAGES" />
<data android:mimeType="image/*" />
</intent>
<intent>
<action android:name="android.intent.action.GET_CONTENT" />
<data android:mimeType="image/*" />
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,96 @@
package com.example.myapplication;
import android.annotation.SuppressLint;
import android.database.Cursor;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.myapplication.DataBase.DatabaseHelper;
public class AdminActivity extends AppCompatActivity {
private ListView lvUsers;
private DatabaseHelper dbHelper;
private SimpleCursorAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_admin);
dbHelper = new DatabaseHelper(this);
lvUsers = findViewById(R.id.lvUsers);
Button btnBack = findViewById(R.id.btnBack);
loadUsers();
btnBack.setOnClickListener(v -> finish());
}
private void loadUsers() {
Cursor cursor = dbHelper.getAllUsers();
String[] from = new String[]{
DatabaseHelper.COLUMN_USERNAME,
DatabaseHelper.COLUMN_PASSWORD,
DatabaseHelper.COLUMN_USER_PROJECT_NAME,
DatabaseHelper.COLUMN_USER_PROJECT_ID
};
int[] to = new int[]{
R.id.tvUsername,
R.id.tvPassword,
R.id.tvProjectName,
R.id.tvProjectId
};
adapter = new SimpleCursorAdapter(this, R.layout.user_item,
cursor, from, to, 0) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
// 获取当前行的数据
Cursor cursor = (Cursor) getItem(position);
@SuppressLint("Range") String username = cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_USERNAME));
// 设置删除按钮点击事件
Button btnDelete = view.findViewById(R.id.btnDelete);
btnDelete.setOnClickListener(v -> {
if (dbHelper.deleteUser(username)) {
Toast.makeText(AdminActivity.this, "用户已删除", Toast.LENGTH_SHORT).show();
// 重新加载数据
Cursor newCursor = dbHelper.getAllUsers();
changeCursor(newCursor);
} else {
Toast.makeText(AdminActivity.this, "删除失败", Toast.LENGTH_SHORT).show();
}
});
TextView tvProjectName = view.findViewById(R.id.tvProjectName);
tvProjectName.setOnClickListener(v -> {
if (tvProjectName.getMaxLines() == 1) {
tvProjectName.setMaxLines(Integer.MAX_VALUE); // 展开
} else {
tvProjectName.setMaxLines(1); // 收起
}
});
return view;
}
};
lvUsers.setAdapter(adapter);
}
@Override
protected void onDestroy() {
dbHelper.close();
super.onDestroy();
}
}

View File

@ -0,0 +1,45 @@
package com.example.myapplication.DataBase;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import com.example.myapplication.model.AudioEntity;
import com.example.myapplication.model.ImageEntity;
import com.example.myapplication.model.Turbine;
@Database(entities = {ImageEntity.class, Turbine.class, AudioEntity.class}, version = 4) // 版本号增加添加新实体
public abstract class AppDatabase extends RoomDatabase {
public abstract ImageDao imageDao();
public abstract TurbineDao turbineDao(); // 添加新的DAO
public abstract AudioDao AudioDao();
private static volatile AppDatabase INSTANCE;
private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
}
};
public static AppDatabase getDatabase(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "app_database")
.fallbackToDestructiveMigration() // 简单处理正式项目应该实现Migration
.build();
}
}
}
return INSTANCE;
}
}

View File

@ -0,0 +1,38 @@
package com.example.myapplication.DataBase;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import com.example.myapplication.model.AudioEntity;
import java.util.List;
@Dao
public interface AudioDao {
@Insert
void insert(AudioEntity audioEntity);
@Update
void update(AudioEntity audioEntity);
@Delete
void delete(AudioEntity audioEntity);
@Query("SELECT * FROM audio_entities ORDER BY createdAt DESC")
List<AudioEntity> getAllAudios();
@Query("SELECT * FROM audio_entities WHERE id = :id")
AudioEntity getAudioById(int id);
@Query("SELECT * FROM audio_entities WHERE AudioPath = :path")
AudioEntity getAudioByPath(String path);
@Query("DELETE FROM audio_entities WHERE AudioPath = :path")
void deleteByPath(String path);
}

View File

@ -0,0 +1,237 @@
package com.example.myapplication.DataBase;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.example.myapplication.model.Project;
import java.util.ArrayList;
import java.util.List;
public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "UserDB";
private static final int DATABASE_VERSION = 3;
private static final String TABLE_USERS = "users";
private static final String TABLE_PROJECTS = "projects";
// 用户表列名
public static final String COLUMN_ID = "_id";
public static final String COLUMN_USERNAME = "username";
public static final String COLUMN_PASSWORD = "password";
public static final String COLUMN_USER_PROJECT_ID = "user_project_id";
public static final String COLUMN_USER_PROJECT_NAME = "user_project_name";
// 项目表列名
public static final String COLUMN_PROJECT_ID = "project_id";
public static final String COLUMN_PROJECT_NAME = "project_name";
public static final String COLUMN_LAST_UPDATED = "last_updated";
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// 创建用户表
String CREATE_USERS_TABLE = "CREATE TABLE " + TABLE_USERS + "("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ COLUMN_USERNAME + " TEXT UNIQUE,"
+ COLUMN_PASSWORD + " TEXT,"
+ COLUMN_USER_PROJECT_ID + " TEXT,"
+ COLUMN_USER_PROJECT_NAME + " TEXT" + ")";
db.execSQL(CREATE_USERS_TABLE);
// 创建项目表
String CREATE_PROJECTS_TABLE = "CREATE TABLE " + TABLE_PROJECTS + "("
+ COLUMN_PROJECT_ID + " TEXT PRIMARY KEY,"
+ COLUMN_PROJECT_NAME + " TEXT,"
+ COLUMN_LAST_UPDATED + " INTEGER" + ")";
db.execSQL(CREATE_PROJECTS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
// 版本1到版本2的迁移
db.execSQL("CREATE TABLE " + TABLE_USERS + "_temp" + "("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ COLUMN_USERNAME + " TEXT UNIQUE,"
+ COLUMN_PASSWORD + " TEXT,"
+ COLUMN_USER_PROJECT_ID + " TEXT" + ")");
db.execSQL("INSERT INTO " + TABLE_USERS + "_temp ("
+ COLUMN_USERNAME + ", " + COLUMN_PASSWORD + ", " + COLUMN_USER_PROJECT_ID + ") "
+ "SELECT username, password, project FROM " + TABLE_USERS);
db.execSQL("DROP TABLE " + TABLE_USERS);
db.execSQL("ALTER TABLE " + TABLE_USERS + "_temp RENAME TO " + TABLE_USERS);
}
if (oldVersion < 3) {
// 版本2到版本3的迁移添加项目名称列
db.execSQL("ALTER TABLE " + TABLE_USERS + " ADD COLUMN " + COLUMN_USER_PROJECT_NAME + " TEXT");
}
}
// 用户操作方法
public boolean addUser(String username, String password, String projectId, String projectName) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_USERNAME, username);
values.put(COLUMN_PASSWORD, password);
values.put(COLUMN_USER_PROJECT_ID, projectId);
values.put(COLUMN_USER_PROJECT_NAME, projectName);
long result = db.insert(TABLE_USERS, null, values);
db.close();
return result != -1;
}
public boolean updateUserProject(String username, String newProjectId, String newProjectName) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_USER_PROJECT_ID, newProjectId);
values.put(COLUMN_USER_PROJECT_NAME, newProjectName);
int result = db.update(TABLE_USERS, values,
COLUMN_USERNAME + " = ?",
new String[]{username});
db.close();
return result > 0;
}
public Cursor getAllUsers() {
SQLiteDatabase db = this.getReadableDatabase();
return db.query(TABLE_USERS,
new String[]{COLUMN_ID, COLUMN_USERNAME, COLUMN_PASSWORD,
COLUMN_USER_PROJECT_ID, COLUMN_USER_PROJECT_NAME},
null, null, null, null, null);
}
public boolean deleteUser(String username) {
SQLiteDatabase db = this.getWritableDatabase();
int result = db.delete(TABLE_USERS,
COLUMN_USERNAME + " = ?",
new String[]{username});
db.close();
return result > 0;
}
public boolean checkUser(String username, String password) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {COLUMN_ID};
String selection = COLUMN_USERNAME + " = ? AND " + COLUMN_PASSWORD + " = ?";
String[] selectionArgs = {username, password};
Cursor cursor = db.query(TABLE_USERS, columns, selection, selectionArgs,
null, null, null);
int count = cursor.getCount();
cursor.close();
db.close();
return count > 0;
}
public boolean userExists(String username) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {COLUMN_ID};
String selection = COLUMN_USERNAME + " = ?";
String[] selectionArgs = {username};
Cursor cursor = db.query(TABLE_USERS, columns, selection, selectionArgs,
null, null, null);
int count = cursor.getCount();
cursor.close();
db.close();
return count > 0;
}
// 项目操作方法
public void saveProjects(List<Project> projects) {
SQLiteDatabase db = this.getWritableDatabase();
db.beginTransaction();
try {
db.delete(TABLE_PROJECTS, null, null);
for (Project project : projects) {
ContentValues values = new ContentValues();
values.put(COLUMN_PROJECT_ID, project.getProjectId());
values.put(COLUMN_PROJECT_NAME, project.getProjectName());
values.put(COLUMN_LAST_UPDATED, System.currentTimeMillis());
db.insert(TABLE_PROJECTS, null, values);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public List<Project> getAllProjects() {
List<Project> projects = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(TABLE_PROJECTS,
new String[]{COLUMN_PROJECT_ID, COLUMN_PROJECT_NAME},
null, null, null, null, COLUMN_PROJECT_NAME + " ASC");
if (cursor.moveToFirst()) {
do {
Project project = new Project(
cursor.getString(0),
cursor.getString(1)
);
projects.add(project);
} while (cursor.moveToNext());
}
cursor.close();
return projects;
}
public List<Project> searchProjects(String query) {
List<Project> projects = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
String selection = COLUMN_PROJECT_NAME + " LIKE ? OR " + COLUMN_PROJECT_ID + " LIKE ?";
String[] selectionArgs = new String[]{"%" + query + "%", "%" + query + "%"};
Cursor cursor = db.query(TABLE_PROJECTS,
new String[]{COLUMN_PROJECT_ID, COLUMN_PROJECT_NAME},
selection, selectionArgs, null, null, COLUMN_PROJECT_NAME + " ASC");
if (cursor.moveToFirst()) {
do {
Project project = new Project(
cursor.getString(0),
cursor.getString(1)
);
projects.add(project);
} while (cursor.moveToNext());
}
cursor.close();
return projects;
}
public void updateLastUpdateTime(long time) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("last_update_time", time);
// 更新或插入最后更新时间
db.insertWithOnConflict("app_settings", null, values, SQLiteDatabase.CONFLICT_REPLACE);
db.close();
}
// 添加这个方法
public long getLastUpdateTime() {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(TABLE_PROJECTS,
new String[]{"MAX(" + COLUMN_LAST_UPDATED + ")"},
null, null, null, null, null);
long lastUpdated = 0;
if (cursor.moveToFirst()) {
lastUpdated = cursor.getLong(0);
}
cursor.close();
return lastUpdated;
}
}

View File

@ -0,0 +1,43 @@
package com.example.myapplication.DataBase;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import com.example.myapplication.model.ImageEntity;
import java.util.List;
@Dao
public interface ImageDao {
@Insert
void insert(ImageEntity image);
@Query("SELECT * FROM images ORDER BY time DESC")
List<ImageEntity> getAll();
@Delete
void delete(ImageEntity item);
@Query("DELETE FROM images")
void deleteAll();
// 在ImageEntity类中添加查询方法
@Query("SELECT * FROM images ORDER BY time DESC LIMIT 1")
ImageEntity getLatestImage();
// 定时间删除根据 ID 删除特定记录
@Query("DELETE FROM images WHERE time = :time")
void deleteBytime(long time);
@Query("SELECT * FROM images WHERE id = :id")
ImageEntity getById(long id);
@Update
void update(ImageEntity latestImage);
@Query("DELETE FROM images WHERE id IN (:ids)")
void deleteByIds(List<Integer> ids);
@Query("DELETE FROM images WHERE time BETWEEN :startTime AND :endTime")
int deleteByTimeRange(long startTime, long endTime);
}

View File

@ -0,0 +1,24 @@
package com.example.myapplication.DataBase;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.example.myapplication.model.Turbine;
import java.util.List;
@Dao
public interface TurbineDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<Turbine> turbines);
@Query("SELECT * FROM turbines WHERE projectId = :projectId")
List<Turbine> getTurbinesByProject(String projectId);
@Query("DELETE FROM turbines WHERE projectId = :projectId")
void deleteByProjectId(String projectId);
@Query("SELECT * FROM turbines WHERE projectId = :projectId AND " +
"(turbineId LIKE :query OR turbineName LIKE :query)")
List<Turbine> searchTurbines(String projectId, String query);
}

View File

@ -0,0 +1,536 @@
package com.example.myapplication;
import static android.content.ContentValues.TAG;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Filter;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.example.myapplication.DataBase.DatabaseHelper;
import com.example.myapplication.api.ProjectApi;
import com.example.myapplication.model.ApiResponse;
import com.example.myapplication.model.Project;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
public class LoginActivity extends AppCompatActivity {
private EditText etUsername, etPassword;
private CheckBox cbRemember;
private Button btnClearProject;
private Button btnLogin;
private DatabaseHelper dbHelper;
private SharedPreferences sharedPreferences;
private static final String PREFS_NAME = "LoginPrefs";
private static final String PREF_USERNAME = "username";
private static final String PREF_PASSWORD = "password";
private static final String PREF_REMEMBER = "remember";
private static final String PREF_PROJECT = "project"; // 新增项目键名
private AutoCompleteTextView actvProject;
private ArrayAdapter<Project> projectAdapter;
private TextView tvSelectedProjectName;
private TextView tvSelectedProjectId;
private List<Project> projectList = new ArrayList<>();
@SuppressLint("SuspiciousIndentation")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
dbHelper = new DatabaseHelper(this);
// 使用 Context.MODE_PRIVATE 替代 MODE_PRIVATE 常量
sharedPreferences = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
initViews();
setupListeners();
loadSavedPreferences();
loadProjects();
}
private void initViews() {
etUsername = findViewById(R.id.etUsername);
etPassword = findViewById(R.id.etPassword);
actvProject = findViewById(R.id.etProject);
cbRemember = findViewById(R.id.cbRemember);
btnLogin = findViewById(R.id.btnLogin);
btnClearProject = findViewById(R.id.btnClearProject);
tvSelectedProjectName = findViewById(R.id.tvSelectedProjectName);
tvSelectedProjectId = findViewById(R.id.tvSelectedProjectId);
// 初始化项目下拉框
initProjectDropdown();
}
private void setupListeners() {
// 清空项目按钮点击事件
btnClearProject.setOnClickListener(v -> {
actvProject.setText("");
tvSelectedProjectName.setText("项目名称:未选择");
tvSelectedProjectId.setText("项目ID未选择");
loadProjects();
projectAdapter.getFilter().filter("");
actvProject.showDropDown();
});
btnLogin.setOnClickListener(v -> attemptLogin());
}
private void loadSavedPreferences() {
boolean remember = sharedPreferences.getBoolean(PREF_REMEMBER, false);
if (remember) {
String savedUsername = sharedPreferences.getString(PREF_USERNAME, "");
String savedPassword = sharedPreferences.getString(PREF_PASSWORD, "");
String savedProject = sharedPreferences.getString(PREF_PROJECT, "");
etUsername.setText(savedUsername);
etPassword.setText(savedPassword);
actvProject.setText(savedProject);
if (!savedProject.isEmpty()) {
try {
int lastBracketIndex = savedProject.lastIndexOf("(");
int closingBracketIndex = savedProject.lastIndexOf(")");
if (lastBracketIndex != -1 && closingBracketIndex != -1 && closingBracketIndex > lastBracketIndex) {
String projectName = savedProject.substring(0, lastBracketIndex).trim();
String projectId = savedProject.substring(lastBracketIndex + 1, closingBracketIndex).trim();
tvSelectedProjectName.setText("项目名称:" + projectName);
tvSelectedProjectId.setText("项目ID:" + projectId);
}
} catch (Exception e) {
e.printStackTrace();
tvSelectedProjectName.setText("解析错误");
tvSelectedProjectId.setText("");
}
}
cbRemember.setChecked(true);
}
}
private void attemptLogin() {
String username = etUsername.getText().toString().trim();
String password = etPassword.getText().toString().trim();
String projectInput = actvProject.getText().toString().trim();
if (username.isEmpty() || password.isEmpty() || projectInput.isEmpty()) {
Toast.makeText(LoginActivity.this, "请填写所有字段", Toast.LENGTH_SHORT).show();
return;
}
// 在后台线程执行数据库操作
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
// 解析项目ID和名称
Pair<String, String> projectInfo = parseProjectInfo(projectInput);
String projectId = projectInfo.first;
String projectName = projectInfo.second;
// 保存登录信息
saveLoginPreferences(username, password, projectName, projectId);
// 检查用户是否存在
boolean userExists = dbHelper.userExists(username);
runOnUiThread(() -> {
if (userExists) {
// 用户存在验证密码
if (dbHelper.checkUser(username, password)) {
handleLoginSuccess(username, projectId, projectName);
} else {
Toast.makeText(LoginActivity.this, "密码错误", Toast.LENGTH_SHORT).show();
}
} else {
// 用户不存在自动注册
if (dbHelper.addUser(username, password, projectId, projectName)) {
handleLoginSuccess(username, projectId, projectName);
} else {
Toast.makeText(LoginActivity.this, "注册失败", Toast.LENGTH_SHORT).show();
}
}
});
} catch (Exception e) {
runOnUiThread(() ->
Toast.makeText(LoginActivity.this, "登录出错: " + e.getMessage(), Toast.LENGTH_SHORT).show());
}
});
}
private Pair<String, String> parseProjectInfo(String projectInput) {
String projectId = "";
String projectName = "";
Matcher matcher = Pattern.compile(".*\\((.*)\\)").matcher(projectInput);
if (matcher.find()) {
projectId = matcher.group(1).trim();
projectName = projectInput.replace("(" + projectId + ")", "").trim();
} else {
for (Project project : projectList) {
if (project.getProjectName().equals(projectInput) ||
project.getProjectId().equals(projectInput)) {
projectId = project.getProjectId();
projectName = project.getProjectName();
break;
}
}
if (projectId.isEmpty()) {
projectName = projectInput;
}
}
return new Pair<>(projectId, projectName);
}
private void saveLoginPreferences(String username, String password, String projectName, String projectId) {
SharedPreferences.Editor editor = sharedPreferences.edit();
if (cbRemember.isChecked()) {
editor.putString(PREF_USERNAME, username);
editor.putString(PREF_PASSWORD, password);
String projectDisplayText = projectId.isEmpty() ? projectName :
(projectName + " (" + projectId + ")");
editor.putString(PREF_PROJECT, projectDisplayText);
editor.putBoolean(PREF_REMEMBER, true);
} else {
editor.clear();
}
editor.apply();
}
private void handleLoginSuccess(String username, String projectId, String projectName) {
dbHelper.updateUserProject(username, projectId, projectName);
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.putExtra("username", username);
intent.putExtra("projectId", projectId);
intent.putExtra("projectName", projectName);
startActivity(intent);
finish();
}
private void initProjectDropdown() {
projectAdapter = new ArrayAdapter<Project>(this,
R.layout.custom_dropdown_item,
projectList) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getView(position, convertView, parent);
TextView textView = view.findViewById(android.R.id.text1);
Project project = getItem(position);
if (project != null) {
textView.setText(project.getProjectName() + " (" + project.getProjectId() + ")");
textView.setSingleLine(false);
textView.setMaxLines(3);
textView.setEllipsize(null);
}
return view;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
return getView(position, convertView, parent);
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
List<Project> filteredList = new ArrayList<>();
if (constraint != null && constraint.length() > 0) {
String filterPattern = constraint.toString().toLowerCase().trim();
DatabaseHelper dbHelper = new DatabaseHelper(LoginActivity.this);
filteredList = dbHelper.searchProjects(filterPattern);
} else {
filteredList.addAll(projectList);
}
results.values = filteredList;
results.count = filteredList.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
clear();
if (results.values != null) {
addAll((List<Project>) results.values);
}
notifyDataSetChanged();
}
};
}
};
actvProject.setAdapter(projectAdapter);
actvProject.setThreshold(0);
actvProject.setDropDownBackgroundResource(android.R.color.white);
actvProject.setOnItemClickListener((parent, view, position, id) -> {
Object item = parent.getItemAtPosition(position);
if (item instanceof Project) {
Project selectedProject = (Project) item;
tvSelectedProjectName.setText("项目名称:" + selectedProject.getProjectName());
tvSelectedProjectId.setText("项目ID" + selectedProject.getProjectId());
actvProject.setText(selectedProject.getProjectName() + " (" + selectedProject.getProjectId() + ")");
} else {
Log.e("TYPE_ERROR", "Expected Project but got: " + item.getClass());
Toast.makeText(this, "数据格式错误", Toast.LENGTH_SHORT).show();
}
});
}
private void loadProjects() {
String BASE_URL = "http://pms.dtyx.net:9158/";
try {
// 1. 验证接口定义
if (!validateApiDefinition()) {
return;
}
// 2. 初始化Retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
.connectTimeout(30, TimeUnit.SECONDS)
.build())
.build();
// 3. 创建API服务
ProjectApi projectApi = retrofit.create(ProjectApi.class);
// 4. 执行网络请求
executeProjectRequest(projectApi);
} catch (Exception e) {
showErrorOnUI("初始化失败", getErrorMessage(e));
logErrorToFile(e);
}
}
private boolean validateApiDefinition() {
try {
Method method = ProjectApi.class.getMethod("getProjectList");
Type returnType = method.getGenericReturnType();
// 预期的完整类型签名
String expectedType = "retrofit2.Call<com.example.myapplication.model.ApiResponse<java.util.List<com.example.myapplication.model.Project>>>";
if (!returnType.toString().equals(expectedType)) {
String errorMsg = "接口定义错误!\n\n当前定义:\n" + returnType +
"\n\n应修改为:\n@GET(\"project/list\")\nCall<ApiResponse<List<Project>>> getProjectList();";
new Handler(Looper.getMainLooper()).post(() -> {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("API接口定义不合法")
.setMessage(errorMsg)
.setPositiveButton("复制定义", (d, w) -> copyToClipboard(errorMsg))
.setNegativeButton("关闭", null)
.show();
});
return false;
}
return true;
} catch (Exception e) {
showErrorOnUI("接口验证失败", e.getMessage());
return false;
}
}
private void executeProjectRequest(ProjectApi projectApi) {
Call<ApiResponse<List<Project>>> call = projectApi.getProjectList();
call.enqueue(new Callback<ApiResponse<List<Project>>>() {
@Override
public void onResponse(Call<ApiResponse<List<Project>>> call, Response<ApiResponse<List<Project>>> response) {
if (response.isSuccessful() && response.body() != null) {
handleSuccessResponse(response.body().getData());
} else {
handleApiError(response);
}
}
@Override
public void onFailure(Call<ApiResponse<List<Project>>> call, Throwable t) {
handleNetworkError(t);
}
});
}
// === 错误处理方法 ===
private void handleSuccessResponse(List<Project> projects) {
runOnUiThread(() -> {
try {
dbHelper.saveProjects(projects);
projectList.clear();
projectList.addAll(projects);
projectAdapter.notifyDataSetChanged();
} catch (Exception e) {
showErrorOnUI("数据处理错误", e.getMessage());
}
});
}
private void handleApiError(Response<?> response) {
String errorMsg = "服务器响应错误: " + response.code();
try {
if (response.errorBody() != null) {
errorMsg += "\n" + response.errorBody().string();
}
} catch (IOException e) {
errorMsg += "\n无法读取错误详情";
}
showErrorOnUI("API请求失败", errorMsg);
loadCachedProjects();
}
private void handleNetworkError(Throwable t) {
String errorMsg = "网络错误: " + t.getMessage();
if (t instanceof SocketTimeoutException) {
errorMsg = "连接超时,请检查网络";
} else if (t instanceof UnknownHostException) {
errorMsg = "无法解析主机请检查URL";
}
showErrorOnUI("网络连接失败", errorMsg);
loadCachedProjects();
}
// === 工具方法 ===
private void showErrorOnUI(String title, String message) {
runOnUiThread(() -> {
// 在界面底部显示错误面板
ViewGroup rootView = findViewById(android.R.id.content);
LinearLayout errorPanel = new LinearLayout(this);
errorPanel.setOrientation(LinearLayout.VERTICAL);
errorPanel.setBackgroundColor(0x33FF0000);
errorPanel.setPadding(32, 16, 32, 16);
TextView tvError = new TextView(this);
tvError.setTextColor(Color.RED);
tvError.setText(title);
TextView tvMsg = new TextView(this);
tvMsg.setText(message);
errorPanel.addView(tvError);
errorPanel.addView(tvMsg);
rootView.addView(errorPanel);
// 5秒后自动消失
new Handler().postDelayed(() -> rootView.removeView(errorPanel), 5000);
});
}
private void copyToClipboard(String text) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText("错误定义", text));
Toast.makeText(this, "定义已复制", Toast.LENGTH_SHORT).show();
}
private String getErrorMessage(Exception e) {
if (e instanceof IllegalArgumentException) {
return "参数错误: " + e.getMessage();
} else if (e instanceof NullPointerException) {
return "空指针异常: " + e.getMessage();
} else {
return "未知错误: " + e.getClass().getSimpleName();
}
}
private void logErrorToFile(Exception e) {
String log = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) +
"\nError: " + e.getClass().getName() +
"\nMessage: " + e.getMessage() +
"\nStack Trace:\n" + Log.getStackTraceString(e);
try {
File file = new File(getExternalFilesDir(null), "error_log.txt");
FileWriter writer = new FileWriter(file, true);
writer.append(log).append("\n\n");
writer.close();
} catch (IOException ioException) {
Log.e("FileLog", "无法写入错误日志", ioException);
}
}
private void loadCachedProjects() {
new Thread(() -> {
List<Project> cachedProjects = dbHelper.getAllProjects();
runOnUiThread(() -> {
projectList.clear();
projectList.addAll(cachedProjects);
projectAdapter.notifyDataSetChanged();
Toast.makeText(this, "已加载本地缓存数据", Toast.LENGTH_LONG).show();
});
}).start();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,514 @@
package com.example.myapplication.Service;
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.content.pm.ServiceInfo;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Filter;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.example.myapplication.DataBase.AppDatabase;
import com.example.myapplication.DataBase.TurbineDao;
import com.example.myapplication.R;
import com.example.myapplication.api.TurbineApiService;
import com.example.myapplication.model.ApiResponse;
import com.example.myapplication.model.Turbine;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class FloatingWindowService extends Service {
private static final String CHANNEL_ID = "FloatingWindowChannel";
private static final int NOTIFICATION_ID = 1;
private Handler mainHandler = new Handler(Looper.getMainLooper());
private WindowManager windowManager;
private View floatingView;
private RadioGroup radioGroup;
private static final String EXTRA_PROJECT_ID = "PROJECT_ID";
private String lastUnit = "";
private int lastBlade = -1;
private String lastUnitName = "";
private AppDatabase database;
private TurbineDao turbineDao;
private AutoCompleteTextView actvUnit; // 替换原来的EditText
private ArrayAdapter<Turbine> turbineAdapter;
private String projectID;
@SuppressLint("SetTextI18n")
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
database = AppDatabase.getDatabase(this);
turbineDao = database.turbineDao();
createNotificationChannel();
showFloatingWindow();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && intent.hasExtra(EXTRA_PROJECT_ID)) {
projectID = intent.getStringExtra(EXTRA_PROJECT_ID);
// 你可以在这里使用projectID或者在其他方法中使用这个成员变量
}
return super.onStartCommand(intent, flags, startId);
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"悬浮窗服务通道",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
}
private void showFloatingWindow() {
// 创建前台服务通知
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("悬浮窗服务运行中")
.setContentText("正在显示悬浮提示框")
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 需要指定服务类型
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
// Android 8.0-9.0
startForeground(NOTIFICATION_ID, notification);
}
// 创建悬浮窗视图
floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null);
// 设置窗口参数
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.TOP | Gravity.START;
params.x = 100;
params.y = 100;
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
windowManager.addView(floatingView, params);
// 初始化视图
actvUnit = floatingView.findViewById(R.id.actvUnit);
setupTurbineDropdown();
radioGroup = floatingView.findViewById(R.id.radioGroup);
setupDragListener(params);
setupValueChangeListeners();
setupAutoCompleteBehavior();
}
private void setupAutoCompleteBehavior() {
actvUnit.setOnClickListener(v -> {
if (turbineAdapter.getCount() > 0) {
actvUnit.showDropDown();
} else {
actvUnit.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(actvUnit, InputMethodManager.SHOW_IMPLICIT);
}
});
actvUnit.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus && !TextUtils.isEmpty(actvUnit.getText()) && turbineAdapter.getCount() > 0) {
actvUnit.postDelayed(() -> actvUnit.showDropDown(), 100);
}
});
actvUnit.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
if (turbineAdapter.getCount() > 0) {
actvUnit.showDropDown();
} else {
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(actvUnit.getWindowToken(), 0);
}
return true;
}
return false;
});
}
// 移除 setupInputMethod() 方法因为功能已整合到上面的方法中
private void setupDragListener(WindowManager.LayoutParams params) {
floatingView.findViewById(R.id.floatingContainer).setOnTouchListener(new View.OnTouchListener() {
private int initialX, initialY;
private float initialTouchX, initialTouchY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect rect = new Rect();
actvUnit.getGlobalVisibleRect(rect);
if (rect.contains((int)event.getRawX(), (int)event.getRawY())) {
return false; // 让事件继续传递
}
initialX = params.x;
initialY = params.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
return true;
case MotionEvent.ACTION_MOVE:
params.x = initialX + (int)(event.getRawX() - initialTouchX);
params.y = initialY + (int)(event.getRawY() - initialTouchY);
windowManager.updateViewLayout(floatingView, params);
return true;
}
return false;
}
});
}
private void setupTurbineDropdown() {
turbineAdapter = new ArrayAdapter<Turbine>(this,
R.layout.dropdown_item, R.id.text1) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getView(position, convertView, parent);
TextView textView = view.findViewById(R.id.text1);
Turbine turbine = getItem(position);
if (turbine != null) {
// 显示更详细的信息
textView.setText(String.format(turbine.turbineName));
textView.setSingleLine(false);
textView.setMaxLines(3);
}
return view;
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
List<Turbine> filtered;
if (TextUtils.isEmpty(constraint)) {
// 无输入条件时显示全部数据
filtered = turbineDao.getTurbinesByProject(projectID);
} else {
// 有输入条件时执行搜索
filtered = turbineDao.searchTurbines(projectID, "%" + constraint + "%");
}
results.values = filtered;
results.count = filtered.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
clear();
if (results.count > 0) {
addAll((List<Turbine>) results.values);
notifyDataSetChanged();
if (actvUnit.hasFocus() && !TextUtils.isEmpty(constraint)) {
actvUnit.post(() -> actvUnit.showDropDown());
}
} else {
notifyDataSetInvalidated();
}
}
};
}
};
actvUnit.setAdapter(turbineAdapter);
actvUnit.setThreshold(0); // 输入1个字符后开始搜索
actvUnit.setDropDownHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
actvUnit.setDropDownVerticalOffset(10); // 下拉框与输入框的垂直偏移
// 设置输入监听
actvUnit.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void afterTextChanged(Editable s) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String input=s.toString().trim();
lastUnit = input;
lastUnitName = input; // 如果没有匹配项使用原始输入作为名称
if (turbineAdapter.getCount() > 0 && !TextUtils.isEmpty(input)) {
// 尝试查找完全匹配的项
for (int i = 0; i < turbineAdapter.getCount(); i++) {
Turbine turbine = turbineAdapter.getItem(i);
if (turbine != null && turbine.turbineName.equalsIgnoreCase(input)) {
lastUnit = turbine.turbineId;
lastUnitName = turbine.turbineName;
break;
}
}
}
checkAndSendUpdate(); // 实时触发检查
}
});
loadTurbines();
}
private void loadTurbines() {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
// 先检查本地是否有数据
List<Turbine> localTurbines = turbineDao.getTurbinesByProject(projectID);
if (localTurbines.isEmpty() || isNetworkAvailable()) {
// 如果本地无数据或有网络尝试从API获取
fetchTurbinesFromApi();
} else {
// 使用本地数据更新UI
mainHandler.post(() -> updateTurbineList(localTurbines));
}
} catch (Exception e) {
Log.e("TurbineLoad", "加载机组数据失败", e);
}
});
}
private void updateTurbineList(List<Turbine> turbines) {
turbineAdapter.clear();
if (!turbines.isEmpty()) {
turbineAdapter.addAll(turbines);
}
// 如果输入框有内容重新过滤
if (!TextUtils.isEmpty(actvUnit.getText())) {
turbineAdapter.getFilter().filter(actvUnit.getText());
}
}
private boolean isNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) return false;
NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
return capabilities != null &&
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET));
}
private void fetchTurbinesFromApi() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://pms.dtyx.net:9158/")
.addConverterFactory(GsonConverterFactory.create())
.build();
TurbineApiService service = retrofit.create(TurbineApiService.class);
Call<ApiResponse<List<Turbine>>> call = service.getTurbineList(projectID);
call.enqueue(new Callback<ApiResponse<List<Turbine>>>() {
@Override
public void onResponse(Call<ApiResponse<List<Turbine>>> call, Response<ApiResponse<List<Turbine>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<Turbine>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<Turbine> turbines = apiResponse.getData();
Log.d("API_SUCCESS", "Fetched " + turbines.size() + " turbines");
// 保存到数据库子线程
new Thread(() -> {
try {
turbineDao.deleteByProjectId(projectID);
for (Turbine turbine : turbines) {
turbine.projectId = projectID; // 确保 projectId 正确
}
turbineDao.insertAll(turbines);
// 使用 mainHandler 更新 UI主线程
mainHandler.post(() -> {
updateTurbineList(turbines);
showToast("数据加载成功");
});
} catch (Exception e) {
Log.e("DB_ERROR", "保存数据失败", e);
mainHandler.post(() -> showToast("保存数据失败: " + e.getMessage()));
}
}).start();
} else {
// API 返回错误 code != 200
String errorMsg = "服务器错误: " + apiResponse.getCode() + " - " + apiResponse.getMsg();
Log.e("API_ERROR", errorMsg);
mainHandler.post(() -> showToast(errorMsg));
}
} else {
// HTTP 状态码非 200 404500
String errorMsg = "请求失败: HTTP " + response.code();
if (response.errorBody() != null) {
try {
errorMsg += "\n" + response.errorBody().string();
} catch (IOException e) {
Log.e("API_ERROR", "解析错误信息失败", e);
}
}
Log.e("API_ERROR", errorMsg);
String finalErrorMsg = errorMsg;
mainHandler.post(() -> showToast(finalErrorMsg));
}
}
@Override
public void onFailure(Call<ApiResponse<List<Turbine>>> call, Throwable t) {
// 网络请求失败如无网络超时
String errorMsg = "网络错误: " + t.getMessage();
Log.e("NETWORK_ERROR", errorMsg, t);
mainHandler.post(() -> showToast(errorMsg));
}
});
}
// 显示 Toast 的辅助方法
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
private void setupValueChangeListeners() {
// 监听机组号输入框变化
actvUnit.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
}
});
// 添加下拉框项点击监听
actvUnit.setOnItemClickListener((parent, view, position, id) -> {
Turbine selected = turbineAdapter.getItem(position);
if (selected != null) {
lastUnit = selected.turbineId;
lastUnitName = selected.turbineName;
actvUnit.setText(selected.turbineName); // 显示名称
checkAndSendUpdate();
}
});
// 监听叶片号选择变化
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
int currentBlade = 0;
if (checkedId != -1) {
RadioButton radioButton = floatingView.findViewById(checkedId);
currentBlade = Integer.parseInt(radioButton.getText().toString());
}
if (currentBlade != lastBlade) {
lastBlade = currentBlade;
checkAndSendUpdate();
}
});
}
private void checkAndSendUpdate() {
// 只有当两个值都有有效变化时才发送广播
if (!lastUnit.isEmpty() && lastBlade != -1) {
sendUpdateBroadcast();
highlightChanges();
}
}
private void sendUpdateBroadcast() {
Intent intent = new Intent("com.example.myapplication.UPDATE_VALUES");
intent.putExtra("unit", lastUnit);
intent.putExtra("blade", lastBlade);
intent.putExtra("unitName", lastUnitName); // 发送机组名称
intent.setPackage(getPackageName());
sendBroadcast(intent);
}
private void highlightChanges() {
// 高亮效果 - 改变背景色然后恢复
int originalColor = actvUnit.getSolidColor();
actvUnit.setBackgroundColor(Color.YELLOW);
radioGroup.setBackgroundColor(Color.YELLOW);
new Handler().postDelayed(() -> {
actvUnit.setBackgroundColor(originalColor);
radioGroup.setBackgroundColor(Color.TRANSPARENT);
}, 300); // 0.5秒后恢复
}
@Override
public void onDestroy() {
super.onDestroy();
if (floatingView != null && windowManager != null) {
windowManager.removeView(floatingView);
}
mainHandler.removeCallbacksAndMessages(null);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@ -0,0 +1,54 @@
package com.example.myapplication.Service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.example.myapplication.R;
public class ForegroundService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(1, createNotification());
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
private Notification createNotification() {
NotificationChannel channel = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
channel = new NotificationChannel(
"channel_id",
"Foreground Service",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
return new NotificationCompat.Builder(this, "channel_id")
.setContentTitle("录音服务运行中")
.setContentText("悬浮球正在运行")
.setSmallIcon(R.drawable.ic_mic_off)
.build();
}
}

View File

@ -0,0 +1,95 @@
package com.example.myapplication.Service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.example.myapplication.R;
public class RecordingService extends Service {
private static final String CHANNEL_ID = "recording_channel";
private static final int NOTIFICATION_ID = 1;
@RequiresApi(api = Build.VERSION_CODES.R)
@Override
public void onCreate() {
super.onCreate();
try {
createNotificationChannel();
// 启动前台服务必须5秒内调用startForeground
Notification notification = buildNotification("录音服务运行中");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 需要指定类型
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else {
// Android 8.0-9.0
startForeground(NOTIFICATION_ID, notification);
}
} catch (Exception e) {
Toast.makeText(this,"错误:"+e,Toast.LENGTH_LONG).show();
stopSelf();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
"recording_channel",
"Recording Service",
NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
}
}
private Notification buildNotification(String text) {
// 确保图标有效R.drawable.ic_mic_on必须存在
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_mic_on)
.setContentTitle("录音服务")
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build();
}
public void updateNotification(String text) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
// 无权限时不显示通知避免崩溃
return;
}
}
NotificationManagerCompat.from(this)
.notify(NOTIFICATION_ID, buildNotification(text));
}
}

View File

@ -0,0 +1,28 @@
package com.example.myapplication.Tool;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
public class BackgroundToast {
private static Handler handler;
private static Toast currentToast;
public static void show(final Context context, final String message) {
if (handler == null) {
handler = new Handler(Looper.getMainLooper());
}
handler.post(() -> {
// 取消之前的Toast
if (currentToast != null) {
currentToast.cancel();
}
// 创建新的Toast
currentToast = Toast.makeText(context.getApplicationContext(), message, Toast.LENGTH_SHORT);
currentToast.show();
});
}
}

View File

@ -0,0 +1,70 @@
package com.example.myapplication.Tool;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
//测试时使用
public class ErrorDisplayUtil {
// 显示全量错误信息的对话框
public static void showErrorDialog(Activity activity, String title, String errorDetail) {
activity.runOnUiThread(() -> {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title)
.setMessage(errorDetail)
.setPositiveButton("复制错误", (dialog, which) -> {
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("错误详情", errorDetail);
clipboard.setPrimaryClip(clip);
Toast.makeText(activity, "已复制到剪贴板", Toast.LENGTH_SHORT).show();
})
.setNegativeButton("关闭", null)
.show();
});
}
// 在界面上固定位置显示错误面板建议放在登录按钮下方
public static void showErrorPanel(Activity activity, String errorType, String solution) {
activity.runOnUiThread(() -> {
ViewGroup rootView = activity.findViewById(android.R.id.content);
// 创建错误面板
LinearLayout errorPanel = new LinearLayout(activity);
errorPanel.setOrientation(LinearLayout.VERTICAL);
errorPanel.setBackgroundColor(0x22FF0000); // 半透明红色背景
errorPanel.setPadding(16, 16, 16, 16);
TextView errorView = new TextView(activity);
errorView.setTextColor(Color.RED);
errorView.setText("⚠️ 错误类型: " + errorType);
TextView solutionView = new TextView(activity);
solutionView.setTextColor(Color.BLACK);
solutionView.setText("💡 解决方案: " + solution);
Button detailBtn = new Button(activity);
detailBtn.setText("查看技术详情");
detailBtn.setOnClickListener(v -> showErrorDialog(activity, "技术详情", errorType + "\n\n" + solution));
errorPanel.addView(errorView);
errorPanel.addView(solutionView);
errorPanel.addView(detailBtn);
// 添加到界面底部
rootView.addView(errorPanel);
// 5秒后自动隐藏
new Handler().postDelayed(() -> rootView.removeView(errorPanel), 5000);
});
}
}

View File

@ -0,0 +1,87 @@
package com.example.myapplication.Tool;
import android.content.Context;
import android.graphics.Color;
import android.util.SparseBooleanArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import com.example.myapplication.model.ImageInfo;
import java.util.ArrayList;
import java.util.List;
public class ImageAdapter extends BaseAdapter {
private final Context context;
private final List<ImageInfo> imageList;
private final SparseBooleanArray selectedItems;
public ImageAdapter(Context context, List<ImageInfo> imageList) {
this.context = context;
this.imageList = imageList;
this.selectedItems = new SparseBooleanArray();
}
@Override
public int getCount() {
return imageList.size();
}
@Override
public Object getItem(int position) {
return imageList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView;
if (convertView == null) {
imageView = new ImageView(context);
imageView.setLayoutParams(new GridView.LayoutParams(200, 200));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setPadding(5, 5, 5, 5);
} else {
imageView = (ImageView) convertView;
}
ImageInfo imageInfo = imageList.get(position);
imageView.setImageBitmap(imageInfo.thumbnail);
// 设置选中状态
if (selectedItems.get(position, false)) {
imageView.setBackgroundColor(Color.BLUE);
} else {
imageView.setBackgroundColor(Color.TRANSPARENT);
}
imageView.setOnClickListener(v -> {
if (selectedItems.get(position, false)) {
selectedItems.delete(position);
imageView.setBackgroundColor(Color.TRANSPARENT);
} else {
selectedItems.put(position, true);
imageView.setBackgroundColor(Color.BLUE);
}
});
return imageView;
}
public List<ImageInfo> getSelectedImages() {
List<ImageInfo> selected = new ArrayList<>();
for (int i = 0; i < imageList.size(); i++) {
if (selectedItems.get(i, false)) {
selected.add(imageList.get(i));
}
}
return selected;
}
}

View File

@ -0,0 +1,19 @@
package com.example.myapplication.api;
import com.example.myapplication.model.ApiResponse;
import com.example.myapplication.model.Project;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
// ApiService.java
public interface ProjectApi {
@GET("project/list")
Call<ApiResponse<List<Project>>> getProjectList();
}

View File

@ -0,0 +1,17 @@
package com.example.myapplication.api;
import com.example.myapplication.model.ApiResponse;
import com.example.myapplication.model.Turbine;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface TurbineApiService {
@GET("turbine/list")
Call<ApiResponse<List<Turbine>>> getTurbineList(@Query("projectId") String projectId);
}

View File

@ -0,0 +1,61 @@
package com.example.myapplication.model;
import com.google.gson.annotations.SerializedName;
public class ApiResponse<T> {
@SerializedName("code")
private int code;
@SerializedName("msg")
private String msg; // 严格匹配服务器字段名 "msg"
@SerializedName("data")
private T data;
@SerializedName("status")
private int status;
@SerializedName("success")
private boolean success;
// Getters
public int getCode() {
return code;
}
public String getMsg() { // 方法名改为 getMsg() 与字段名一致
return msg;
}
public T getData() {
return data;
}
public int getStatus() {
return status;
}
public boolean isSuccess() {
return success;
}
// Setters按需添加
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "ApiResponse{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
", status=" + status +
", success=" + success +
'}';
}
}

View File

@ -0,0 +1,98 @@
package com.example.myapplication.model;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "audio_entities")
public class AudioEntity {
@PrimaryKey(autoGenerate = true)
private int id;
private String fileName;
public String AudioPath;
public AudioEntity() {
}
public String ImagePath;
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
private long fileSize;
public String time;
public AudioEntity(String audioPath, String imagePath,String time) {
this.AudioPath = audioPath;
this.ImagePath = imagePath;
this.time=time;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getAudioPath() {
return AudioPath;
}
public void setAudioPath(String audioPath) {
AudioPath = audioPath;
}
public String getImagePath() {
return ImagePath;
}
public void setImagePath(String imagePath) {
ImagePath = imagePath;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
public long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(long createdAt) {
this.createdAt = createdAt;
}
private long duration; // 音频时长(毫秒)
private long createdAt; // 创建时间戳
// 构造方法getter setter
}

View File

@ -0,0 +1,47 @@
package com.example.myapplication.model;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "images")
public class ImageEntity {
@PrimaryKey(autoGenerate = true)
public int id;
public String path;
public long time;
public double latitude;
public double longitude;
public double altitude;
public String user;
public String audioPath;
public int blade;
public String unit;
public String project;
public boolean b;
public ImageEntity(String path, long time, double latitude, double longitude, double altitude, String user, String audioPath, String project, String unit, int blade, boolean b) {
this.path = path;
this.time = time;
this.latitude = latitude;
this.longitude = longitude;
this.altitude = altitude;
this.user = user;
this.audioPath = audioPath;
this.project=project;
this.unit=unit;
this.blade=blade;
this.b=b;
}
// 构造方法
}
// ImageDao.java

View File

@ -0,0 +1,169 @@
package com.example.myapplication.model;
import android.graphics.Bitmap;
public class ImageInfo {
private String path; // 图片路径
private long time; // 时间戳单位
private Bitmap bitmap; // 图片缩略图
private double latitude;
private double longitude;
private double altitude;
private String user;
public boolean isB() {
return b;
}
public void setB(boolean b) {
this.b = b;
}
public ImageInfo(String path, long time, Bitmap bitmap, double latitude, double longitude, double altitude, String user, String audioPath, boolean b, Bitmap thumbnail, String unit, String project, int blade) {
this.path = path;
this.time = time;
this.bitmap = bitmap;
this.latitude = latitude;
this.longitude = longitude;
this.altitude = altitude;
this.user = user;
this.audioPath = audioPath;
this.b = b;
this.thumbnail = thumbnail;
this.unit = unit;
this.project = project;
this.blade = blade;
}
boolean b;
public Bitmap getThumbnail() {
return thumbnail;
}
public void setThumbnail(Bitmap thumbnail) {
this.thumbnail = thumbnail;
}
public String getProject() {
return project;
}
public void setProject(String project) {
this.project = project;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public int getBlade() {
return blade;
}
public void setBlade(int blade) {
this.blade = blade;
}
private String audioPath;
public Bitmap thumbnail;
private String project;
private String unit;
private int blade;
public void setPath(String path) {
this.path = path;
}
public void setTime(long time) {
this.time = time;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
public void setAltitude(double altitude) {
this.altitude = altitude;
}
public void setUser(String user) {
this.user = user;
}
public String getAudioPath() {
return audioPath;
}
public void setAudioPath(String audioPath) {
this.audioPath = audioPath;
}
// 修改构造函数
public ImageInfo(String path, long time, Bitmap thumbnail, double latitude, double longitude, double altitude, String user, String audioPath) {
this.path = path;
this.time = time;
this.bitmap = bitmap;
this.latitude = latitude;
this.longitude = longitude;
this.altitude=altitude;
this.user=user;
this.audioPath=audioPath;
}
public ImageInfo(String path, long time, Bitmap bitmap, double latitude, double longitude, double altitude, String user, String audioPath, Bitmap thumbnail, String project, String unit, int blade) {
this.path = path;
this.time = time;
this.bitmap = bitmap;
this.latitude = latitude;
this.longitude = longitude;
this.altitude = altitude;
this.user = user;
this.audioPath = audioPath;
this.thumbnail = thumbnail;
this.project = project;
this.unit = unit;
this.blade = blade;
}
public double getLongitude() {
return longitude;
}
public double getLatitude() {
return latitude;
}
public String getUser() {
return user;
}
public double getAltitude() {
return altitude;
}
public ImageInfo(String path, long time, Bitmap bitmap) {
this.path = path;
this.time = time;
this.bitmap = bitmap;
}
// Getter 方法
public String getPath() { return path; }
public long getTime() { return time; }
public Bitmap getBitmap() { return bitmap; }
}

View File

@ -0,0 +1,239 @@
package com.example.myapplication.model;
import com.google.gson.annotations.SerializedName;
// Project.java
public class Project {
@SerializedName("projectId")
private String projectId;
@SerializedName("projectName")
private String projectName;
@SerializedName("status")
private int status;
@SerializedName("client")
private String client;
@SerializedName("clientContact")
private String clientContact;
@SerializedName("clientPhone")
private String clientPhone;
@SerializedName("constructorIds")
private String constructorIds;
@SerializedName("constructorName")
private String constructorName;
@SerializedName("coverUrl")
private String coverUrl;
@SerializedName("createTime")
private String createTime;
@SerializedName("farmAddress")
private String farmAddress;
@SerializedName("farmName")
private String farmName;
@SerializedName("inspectionContact")
private String inspectionContact;
@SerializedName("inspectionPhone")
private String inspectionPhone;
@SerializedName("inspectionUnit")
private String inspectionUnit;
@SerializedName("projectManagerId")
private String projectManagerId;
@SerializedName("projectManagerName")
private String projectManagerName;
@SerializedName("scale")
private String scale;
@SerializedName("statusLabel")
private String statusLabel;
@SerializedName("turbineModel")
private String turbineModel;
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getClient() {
return client;
}
public void setClient(String client) {
this.client = client;
}
public String getClientPhone() {
return clientPhone;
}
public void setClientPhone(String clientPhone) {
this.clientPhone = clientPhone;
}
public String getConstructorIds() {
return constructorIds;
}
public void setConstructorIds(String constructorIds) {
this.constructorIds = constructorIds;
}
public String getClientContact() {
return clientContact;
}
public void setClientContact(String clientContact) {
this.clientContact = clientContact;
}
public String getCoverUrl() {
return coverUrl;
}
public void setCoverUrl(String coverUrl) {
this.coverUrl = coverUrl;
}
public String getConstructorName() {
return constructorName;
}
public void setConstructorName(String constructorName) {
this.constructorName = constructorName;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
public String getFarmName() {
return farmName;
}
public void setFarmName(String farmName) {
this.farmName = farmName;
}
public String getFarmAddress() {
return farmAddress;
}
public void setFarmAddress(String farmAddress) {
this.farmAddress = farmAddress;
}
public String getInspectionPhone() {
return inspectionPhone;
}
public void setInspectionPhone(String inspectionPhone) {
this.inspectionPhone = inspectionPhone;
}
public String getInspectionContact() {
return inspectionContact;
}
public void setInspectionContact(String inspectionContact) {
this.inspectionContact = inspectionContact;
}
public String getInspectionUnit() {
return inspectionUnit;
}
public void setInspectionUnit(String inspectionUnit) {
this.inspectionUnit = inspectionUnit;
}
public String getProjectManagerId() {
return projectManagerId;
}
public void setProjectManagerId(String projectManagerId) {
this.projectManagerId = projectManagerId;
}
public String getScale() {
return scale;
}
public void setScale(String scale) {
this.scale = scale;
}
public String getProjectManagerName() {
return projectManagerName;
}
public void setProjectManagerName(String projectManagerName) {
this.projectManagerName = projectManagerName;
}
public String getStatusLabel() {
return statusLabel;
}
public void setStatusLabel(String statusLabel) {
this.statusLabel = statusLabel;
}
public String getTurbineModel() {
return turbineModel;
}
public void setTurbineModel(String turbineModel) {
this.turbineModel = turbineModel;
}
// Getters and Setters
public String getProjectId() {
return projectId;
}
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public Project(String projectId, String projectName) {
this.projectId = projectId;
this.projectName = projectName;
}
// 其他字段的 getter/setter 按需添加...
// 建议使用 Android Studio "Generate" -> "Getter and Setter" 自动生成
@Override
public String toString() {
return projectName + " (" + projectId + ")";
}
}

View File

@ -0,0 +1,25 @@
package com.example.myapplication.model;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "turbines")
public class Turbine {
@PrimaryKey
@NonNull
public String turbineId;
public String turbineName;
public String projectId;
@Override
public String toString() {
return
turbineName +'('+ turbineId + ')';
}
// 构造方法getter/setter省略
}

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent"/>
<stroke android:width="1dp" android:color="#BDBDBD"/>
<corners android:radius="8dp"/>
</shape>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2a3,3 0,0 1,3 3v6a3,3 0,0 1,-3 3,3 3,0 0,1 -3,-3V5a3,3 0,0 1,3 -3z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M19,10v2a7,7 0,0 1,-7 7,7 7,0 0,1 -7,-7v-2h2v2a5,5 0,0 0,5 5,5 5,0 0,0 5,-5v-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12,15a1,1 0,1 0,0 2,1 1,0 0,0 0,-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M3.28,2.22a0.75,0.75 0,1 0,-1.06 1.06l18.5,18.5a0.75,0.75 0,0 0,1.06 -1.06z"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2a3,3 0,0 1,3 3v6a3,3 0,0 1,-3 3,3 3,0 0,1 -3,-3V5a3,3 0,0 1,3 -3z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M19,10v2a7,7 0,0 1,-7 7,7 7,0 0,1 -7,-7v-2h2v2a5,5 0,0 0,5 5,5 5,0 0,0 5,-5v-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12,15a1,1 0,1 0,0 2,1 1,0 0,0 0,-2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4081"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5z"/>
</vector>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户管理"
android:textSize="24sp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"/>
<ListView
android:id="@+id/lvUsers"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/btnBack"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="返回"/>
</LinearLayout>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".LoginActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登录"
android:textSize="24sp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"/>
<EditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户名"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<EditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码"
android:inputType="textPassword"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp"
android:gravity="center_vertical">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:hint="项目">
<AutoCompleteTextView
android:id="@+id/etProject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:completionThreshold="1"
android:maxLines="3"
android:ellipsize="none"
android:singleLine="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:imeOptions="actionDone"
android:dropDownWidth="match_parent"
android:dropDownHeight="wrap_content"
android:dropDownVerticalOffset="4dp"
android:dropDownAnchor="@id/etProject"
android:dropDownSelector="@android:color/darker_gray"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnClearProject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清除"
android:textSize="14sp"
android:textColor="#FF0000"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_marginStart="8dp"/>
</LinearLayout>
<CheckBox
android:id="@+id/cbRemember"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="记住登录信息"
android:layout_marginBottom="24dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:background="@android:color/darker_gray"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="已选项目信息:"
android:textSize="14sp"
android:textColor="@android:color/white"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvSelectedProjectName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:padding="4dp"
android:text="项目名称:未选择"
android:textColor="@android:color/white"
android:background="@android:color/transparent"/>
<TextView
android:id="@+id/tvSelectedProjectId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:padding="4dp"
android:text="项目ID未选择"
android:textColor="@android:color/white"
android:background="@android:color/transparent"/>
</LinearLayout>
<Button
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登录"/>
</LinearLayout>

View File

@ -0,0 +1,592 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFF8E1"
android:orientation="vertical"
tools:context=".MainActivity">
<!-- 项目信息固定区域 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardBackgroundColor="@color/card_background"
app:cardCornerRadius="12dp"
app:cardElevation="6dp"
app:strokeColor="@color/card_stroke"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 用户信息(独占一行) -->
<TextView
android:id="@+id/tv_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="👤 本人信息: 未设置"
android:textSize="16sp"
android:textColor="@color/text_primary"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"
android:paddingBottom="8dp"/>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"
android:layout_marginVertical="8dp"/>
<!-- 项目信息(垂直排列,确保长文本可读) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="4dp">
<!-- 项目名称(带图标和固定标签宽度) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📁 项目名称:"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginRight="8dp"/>
<TextView
android:id="@+id/tv_project"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未设置"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:maxLines="2"/>
</LinearLayout>
<!-- 项目ID带图标和固定标签宽度 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🆔 项目ID:"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginRight="8dp"/>
<TextView
android:id="@+id/tv_projectId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未设置"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:maxLines="2"/>
</LinearLayout>
</LinearLayout>
<!-- 机组信息(垂直排列) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 机组ID -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⚙️ 机组ID:"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginRight="8dp"/>
<TextView
android:id="@+id/tvUnit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未设置"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:maxLines="1"/>
</LinearLayout>
<!-- 机组名称 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🏷️ 机组名字:"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginRight="8dp"/>
<TextView
android:id="@+id/tvUnitName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未设置"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:maxLines="1"/>
</LinearLayout>
</LinearLayout>
<!-- 叶片信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🍃 叶片号:"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginRight="8dp"/>
<TextView
android:id="@+id/tvBlade"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未设置"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:maxLines="1"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 主内容区域(可滚动) -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<!-- 状态卡置顶 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="#E3F2FD"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/location_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="当前位置: 未获取"
android:textColor="#0D47A1"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="当前状态:未监听"
android:textColor="#212121"
android:textSize="14sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 视频管理区 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="#FFFFFF"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="视频管理区"
android:textColor="#2196F3"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<!-- 双按钮布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginBottom="8dp">
<Button
android:id="@+id/btn_upload_videos2"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="手动选择上传视频"
app:strokeColor="#2196F3"
app:strokeWidth="1dp"/>
<Button
android:id="@+id/btn_upload_videos"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="选择时间段上传视频"
app:strokeColor="#2196F3"
app:strokeWidth="1dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 图片录音管理区 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="#FFFFFF"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="图片管理区"
android:textColor="#FF9800"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<!-- 第一行双按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<Button
android:id="@+id/selectTimeButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="选择时间段补传图片"
app:strokeColor="#2196F3"
app:strokeWidth="1dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_history"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="查看成功照片"
android:textColor="#2196F3"
app:cornerRadius="8dp"
app:strokeColor="#2196F3" />
</LinearLayout>
<!-- 第二行双按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_history2"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="查看失败照片"
android:textColor="#4CAF50"
app:cornerRadius="8dp"
app:strokeColor="#4CAF50" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_upload_queue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="一键上传失败照片"
android:backgroundTint="#FFC107"
android:textColor="#212121"
app:cornerRadius="8dp"/>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_stop_upload"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="终止上传"
android:backgroundTint="#F44336"
android:textColor="#FFFFFF"
android:enabled="false"
app:cornerRadius="8dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 用户管理区 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="#FFFFFF"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="用户管理"
android:textColor="#607D8B"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<Button
android:id="@+id/btnAdmin"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="用户管理"
app:strokeColor="#607D8B"
app:strokeWidth="1dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 时间设置放在主内容区最下方 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="#E3F2FD"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/startTimeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="按时间段上传照片或视频的开始时间:未选择"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/endTimeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="按时间段上传照片或视频的结束时间:未选择"
android:textSize="14sp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/btn_failed_audios"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查看失败录音"
android:layout_margin="8dp"/>
<!-- 定时上传图片设置(移动到最下方) -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="#FFFFFF"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="定时上传图片设置"
android:textColor="#673AB7"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="上传间隔(分钟)"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxStrokeColor="#E65100"
app:hintTextColor="#E65100">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_upload_interval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number|text"
android:textColor="#E65100"
android:imeOptions="actionDone"
android:text="30"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_set_interval"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置"
android:textColor="#673AB7"
android:layout_marginStart="8dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_timed_upload"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="开始定时"
android:textColor="#4CAF50"
app:cornerRadius="8dp"
app:strokeColor="#4CAF50" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_stop_timed_upload"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="停止定时"
android:textColor="#F44336"
app:cornerRadius="8dp"
app:strokeColor="#F44336" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
<!-- 其他功能区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<Button
android:id="@+id/btnShowFloating"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="显示悬浮窗"
app:strokeColor="#9C27B0"
app:strokeWidth="1dp"/>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_monitor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="监听相册变化"
android:textColor="#673AB7"
android:textSize="16sp"
app:thumbTint="#673AB7"
app:trackTint="#D1C4E9"
app:useMaterialThemeColors="false" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textSize="16sp"
android:textColor="@android:color/black"
android:singleLine="false"
android:maxLines="3"
android:ellipsize="none"
android:background="?android:attr/selectableItemBackground"/>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black"
android:ellipsize="end"
android:maxLines="2"/>
</LinearLayout>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/floatingContainer"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#88F5F5F5"
android:padding="8dp">
<!-- 机组号行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="机组号"
android:textSize="14sp"
android:textColor="#FF000000"/>
<AutoCompleteTextView
android:id="@+id/actvUnit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入机组号或名称"
android:inputType="text"
android:imeOptions="actionDone"/>
</LinearLayout>
<!-- 叶片号行 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="叶片号"
android:layout_marginTop="6dp"
android:textSize="14sp"
android:textColor="#FF000000"/>
<RadioGroup
android:id="@+id/radioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:layout_weight="1"
android:textColor="#FF000000"
android:buttonTint="#88000000"/>
<RadioButton
android:id="@+id/rb2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2"
android:layout_weight="1"
android:textColor="#FF000000"
android:buttonTint="#88000000"/>
<RadioButton
android:id="@+id/rb3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3"
android:layout_weight="1"
android:textColor="#FF000000"
android:buttonTint="#88000000"/>
</RadioGroup>
</LinearLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toTopOf="@id/textview_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textview_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/lorem_ipsum"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_first" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Button
android:id="@+id/button_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toTopOf="@id/textview_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textview_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/lorem_ipsum"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_second" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/item_layout"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/iv_thumbnail"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textSize="14sp"/>
</LinearLayout>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<!-- 用户名 -->
<TextView
android:id="@+id/tvUsername"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"/>
<!-- 密码 -->
<TextView
android:id="@+id/tvPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@android:color/darker_gray"/>
<!-- 项目名称 - 默认显示1行点击后展开 -->
<TextView
android:id="@+id/tvProjectName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"
android:clickable="true"
android:focusable="true"/>
<!-- 项目ID -->
<TextView
android:id="@+id/tvProjectId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@android:color/darker_gray"/>
<!-- 删除按钮 -->
<Button
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:layout_marginStart="8dp"/>
</LinearLayout>

View File

@ -0,0 +1,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.example.myapplication.MainActivity">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="com.example.myapplication.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.example.myapplication.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.MyApplication" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.MyApplication" parent="Base.Theme.MyApplication">
<!-- Transparent system bars for edge-to-edge. -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
</style>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="card_background">#F5F9FF</color>
<color name="card_stroke">#BBDEFB</color>
<color name="text_primary">#212121</color>
<color name="text_secondary">#424242</color>
<color name="divider">#E0E0E0</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 应用名称 -->
<string name="app_name">My Application</string>
<!-- 解决错误所需的字符串 -->
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="lorem_ipsum">Lorem ipsum dolor sit amet...</string>
<string name="action_settings">Settings</string>
<string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<!-- 之前登录功能需要的字符串 -->
<string name="login">Login</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="project">Project Name</string>
<string name="remember_me">Remember me</string>
<string name="logout">Logout</string>
<string name="welcome">Welcome, %1$s!\nProject: %2$s</string>
</resources>

View File

@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.MyApplication" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.MyApplication" parent="Base.Theme.MyApplication" />
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="." />
<!-- 应用内部存储目录 -->
<files-path name="internal_files" path="." />
<!-- 缓存目录 -->
<cache-path name="cache_files" path="." />
<!-- 外部存储中应用专属目录 -->
<external-files-path name="external_app_files" path="." />
<!-- 外部存储中应用专属缓存目录 -->
<external-cache-path name="external_app_cache" path="." />
</paths>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 允许访问 localhost -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<domain includeSubdomains="true">10.0.2.2</domain> <!-- 模拟器专用 -->
</domain-config>
</network-security-config>

View File

@ -0,0 +1,17 @@
package com.example.myapplication;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

4
build.gradle.kts Normal file
View File

@ -0,0 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}

21
gradle.properties Normal file
View File

@ -0,0 +1,21 @@
# 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
# 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

29
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,29 @@
[versions]
agp = "8.9.2"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
constraintlayout = "2.1.4"
navigationFragment = "2.6.0"
navigationUi = "2.6.0"
okhttp = "4.12.0"
room = "2.6.1"
firebaseCrashlyticsBuildtools = "3.0.3"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Mon Apr 28 15:36:39 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Normal file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# 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
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# 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"
which java >/dev/null 2>&1 || 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
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@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
@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=.
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%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="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!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

23
settings.gradle.kts Normal file
View File

@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "My Application"
include(":app")