Initial commit

ptngqz2ua 3 months ago committed by 赵骏浩
commit 87a39df0f1

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

15
src/.gitignore vendored

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -0,0 +1 @@
My Application

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\20922\.android\avd\Pixel_6_Pro_API_33.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2025-09-29T07:20:17.026786700Z" />
</component>
</project>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -0,0 +1,132 @@
# Android 大学助手 应用
## 项目简介
本项目是一个基于 Android 的示例应用,包名为 `com.example.myapplication`,采用 Java 编写,使用 Gradle 进行构建。
项目中集成了以下常用功能依赖:
- **AndroidX 基础组件**`appcompat`、`material`、`coordinatorlayout`、`recyclerview` 等
- **Gson**:用于 JSON 数据解析
- **OkHttp / Retrofit + Gson Converter**:用于网络请求与接口数据封装
- **Jsoup**:用于 HTML 解析(例如成绩数据解析等场景)
- **高德地图 SDK本地 JAR**:用于地图展示、位置相关功能
> 说明:具体业务逻辑和界面功能请参考 `app/src/main/java/com/example/myapplication/` 下的源码。
---
## 技术栈 & 环境要求
- **语言**Java
- **构建工具**Gradle / Android Gradle Plugin 7.4.2
- **最低支持系统版本**`minSdk = 24`
- **目标 / 编译版本**`targetSdk = 33``compileSdk = 33`
- **开发工具**Android Studio推荐使用与 Gradle 插件版本兼容的版本)
- **JDK 版本**Java 8`sourceCompatibility` / `targetCompatibility` 为 1.8
---
## 快速开始
1. **克隆项目**(若在 Git 仓库中):
```bash
git clone <your-repo-url>
cd demo
```
2. **使用 Android Studio 打开**
- 打开 Android Studio
- 选择 *Open an Existing Project*
- 选择本项目根目录(包含 `build.gradle`、`settings.gradle` 的目录)
3. **配置 Android SDK 路径**
- 首次打开时,如果 `local.properties` 不存在或路径不正确,可在 Android Studio 中设置 SDK 路径,或手动编辑 `local.properties`
```properties
sdk.dir=你的_Android_SDK_路径
```
4. **同步 & 构建项目**
- 在 Android Studio 中点击 *Sync Project with Gradle Files*
- 等待依赖下载、构建完成
5. **运行应用**
- 连接真机或启动模拟器API 24 及以上)
- 选择 `app` 模块,点击 *Run* 按钮即可安装运行
---
## 高德地图 SDK 说明
- 项目中通过本地 JAR 的方式集成了高德地图相关 SDK
- JAR 路径配置于 `app/build.gradle` 中的:
```gradle
implementation files('libs/AMap3DMap_10.1.500_AMapSearch_9.7.4_AMapLocation_6.5.0_20250814.jar')
```
- 若要正常使用高德地图功能,请:
- 前往 **高德开放平台** 申请 Android Key
- 按照高德官方文档在 `AndroidManifest.xml` 中配置 API Key 相关信息;
- 确保本地 JAR 文件已放置在 `app/libs/` 目录下(与 `build.gradle` 中路径一致)。
> 如需升级或调整 SDK 版本,请同步更新 `libs` 目录中的 JAR 文件以及对应的 Gradle 配置。
---
## 主要依赖
`app/build.gradle` 中关键依赖包括:
- **UI & 基础组件**
- `androidx.appcompat:appcompat:1.4.1`
- `com.google.android.material:material:1.5.0`
- `androidx.coordinatorlayout:coordinatorlayout:1.2.0`
- `androidx.recyclerview:recyclerview:1.2.1`
- **数据解析**
- `com.google.code.gson:gson:2.8.9`
- `org.jsoup:jsoup:1.15.3`
- **网络请求**
- `com.squareup.okhttp3:okhttp:4.9.3`
- `com.squareup.retrofit2:retrofit:2.9.0`
- `com.squareup.retrofit2:converter-gson:2.9.0`
- **测试**
- `junit:junit:4.13.2`
- `androidx.test.ext:junit:1.1.3`
- `androidx.test.espresso:espresso-core:3.4.0`
---
## 目录结构(简要)
- `app/`
- `src/main/java/com/example/myapplication/`主要业务代码Activity、数据管理、网络请求等
- `src/main/res/`:布局、资源文件
- `build.gradle`:应用模块构建配置
- `AMap_Android_SDK_All/`:高德 SDK 相关文件(如有)
- `temp_sdk/`:临时存放的 SDK / 相关文件(如有)
- `build.gradle`:项目根构建配置(包含 Android Gradle 插件、SonarQube 等)
- `settings.gradle`:模块配置
- `gradle/`、`gradlew`、`gradlew.bat`Gradle Wrapper 相关文件
---
## 代码质量(可选)
项目根 `build.gradle` 中集成了 SonarQube 插件,用于代码质量分析。若需要在本地或 CI 中使用:
1. 搭建或连接到可用的 SonarQube 服务器;
2. 在本地或 CI 环境中配置好 SonarQube 的地址和认证信息;
3. 使用 Gradle 任务执行代码扫描(如 `sonarqube` 任务)。
> 注意:在对外开源或提交到公共仓库时,请勿提交真实的访问 Token 或其他敏感信息。
---
## TODO / 后续规划
- 根据实际业务补充更详细的功能说明和截图
- 编写接口文档、数据结构说明
- 完善单元测试与 UI 测试
- 若需要,对高德地图和网络模块进行封装与抽象,提升可维护性

@ -0,0 +1 @@
/build

@ -0,0 +1,53 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.example.myapplication'
compileSdk 33
defaultConfig {
applicationId "com.example.myapplication"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'com.google.code.gson:gson:2.8.9'
// SDK - JAR
implementation files('libs/AMap3DMap_10.1.500_AMapSearch_9.7.4_AMapLocation_6.5.0_20250814.jar')
// POI
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Jsoup - HTML
implementation 'org.jsoup:jsoup:1.15.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -0,0 +1,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());
}
}

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<!-- 网络权限 -->
<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" />
<!-- 高德地图SDK核心权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- <uses-permission android:name="android.permission.WRITE_SETTINGS" /> 注释掉:此权限仅系统应用可用 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 高德地图导航相关权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!-- 课程提醒相关权限 -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<!-- 笔记图片相关权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<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:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
android:hardwareAccelerated="true"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- 高德地图API Key -->
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="0c0a6f3a57e257c8bf0d9c8202110a37" />
<!-- 高德地图服务配置 -->
<service android:name="com.amap.api.location.APSService"></service>
<activity
android:name=".HomeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<activity
android:name=".LocationDetailsActivity"
android:exported="false" />
<!-- 教务系统登录WebView -->
<activity
android:name=".WebViewActivity"
android:exported="false"
android:label="教务系统登录"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- 教务系统课表导入 -->
<activity
android:name=".EducationImportActivity"
android:exported="false"
android:label="教务系统课表导入"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- 课程提醒全屏弹窗 -->
<activity
android:name=".ReminderAlertActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
<!-- 课程提醒弹窗(微信样式) -->
<activity
android:name=".CourseReminderActivity"
android:exported="false"
android:theme="@style/ReminderDialogTheme"
android:excludeFromRecents="true"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:launchMode="singleTop" />
<!-- 全屏笔记编辑器 -->
<activity
android:name=".NoteEditorActivity"
android:exported="false"
android:label="编辑笔记"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- 笔记阅读模式 -->
<activity
android:name=".NoteReaderActivity"
android:exported="false"
android:label="阅读笔记"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- FileProvider for sharing files -->
<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>
<!-- 课程提醒接收器 -->
<receiver
android:name=".CourseReminderReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".ReminderReceiver"
android:exported="false" />
</application>
</manifest>

@ -0,0 +1,87 @@
package com.example.myapplication;
/**
*
*/
public class AmapLocation {
private String name;
private String description;
private String category;
private String area;
private double latitude; // 纬度
private double longitude; // 经度
public AmapLocation(String name, String description, String category, String area, double latitude, double longitude) {
this.name = name;
this.description = description;
this.category = category;
this.area = area;
this.latitude = latitude;
this.longitude = longitude;
}
public AmapLocation(String name, String description, String category, double latitude, double longitude) {
this(name, description, category, "", latitude, longitude);
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getArea() { return area; }
public void setArea(String area) { this.area = area; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
/**
*
*/
public double calculateDistance(AmapLocation destination) {
if (destination == null) return 0;
double lat1Rad = Math.toRadians(this.latitude);
double lat2Rad = Math.toRadians(destination.latitude);
double deltaLatRad = Math.toRadians(destination.latitude - this.latitude);
double deltaLngRad = Math.toRadians(destination.longitude - this.longitude);
double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLngRad / 2) * Math.sin(deltaLngRad / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return 6371000 * c; // 地球半径 * 弧度 = 距离(米)
}
/**
*
*/
public int calculateWalkTime(AmapLocation destination) {
double distance = calculateDistance(destination);
return (int) Math.ceil(distance / 80); // 按照每分钟80米的步行速度计算
}
@Override
public String toString() {
return name + (area.isEmpty() ? "" : " (" + area + ")");
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
AmapLocation location = (AmapLocation) obj;
return name.equals(location.name) &&
Math.abs(latitude - location.latitude) < 0.0001 &&
Math.abs(longitude - location.longitude) < 0.0001;
}
}

@ -0,0 +1,305 @@
package com.example.myapplication;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
*
*/
public class CampusLocationManager {
private static final String PREF_NAME = "campus_locations";
private static final String KEY_LOCATIONS = "locations";
private static final String KEY_FAVORITES = "favorites";
private Context context;
private List<AmapLocation> allLocations;
private Map<String, List<AmapLocation>> categorizedLocations;
private List<String> favoriteLocationNames;
private SharedPreferences preferences;
private Gson gson;
public CampusLocationManager(Context context) {
this.context = context;
this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
this.categorizedLocations = new HashMap<>();
this.favoriteLocationNames = new ArrayList<>();
initializeDefaultLocations();
loadLocationsFromPreferences();
loadFavoritesFromPreferences();
}
private void initializeDefaultLocations() {
allLocations = new ArrayList<>();
// 图书馆类
allLocations.add(new AmapLocation("主图书馆", "学校主图书馆藏书丰富24小时开放", "图书馆", "图书馆大楼", 39.906016, 116.395312));
allLocations.add(new AmapLocation("理工图书馆", "理工学科专业图书馆", "图书馆", "理工楼", 39.907123, 116.396234));
allLocations.add(new AmapLocation("电子阅览室", "电子资源阅览区", "图书馆", "图书馆2层", 39.906116, 116.395412));
// 食堂类
allLocations.add(new AmapLocation("第一食堂", "学校第一食堂,菜品丰富,价格实惠", "食堂", "学生服务中心", 39.905123, 116.394567));
allLocations.add(new AmapLocation("第二食堂", "学校第二食堂,特色小吃多样", "食堂", "学生活动中心", 39.907234, 116.396789));
allLocations.add(new AmapLocation("清真食堂", "清真餐厅,民族特色菜品", "食堂", "民族楼", 39.906234, 116.395678));
allLocations.add(new AmapLocation("教工食堂", "教职工专用餐厅", "食堂", "行政楼附近", 39.908123, 116.397456));
// 宿舍类
allLocations.add(new AmapLocation("男生1号楼", "男生宿舍区1号楼4人间", "宿舍", "男生宿舍区", 39.904567, 116.393456));
allLocations.add(new AmapLocation("男生2号楼", "男生宿舍区2号楼6人间", "宿舍", "男生宿舍区", 39.904667, 116.393556));
allLocations.add(new AmapLocation("女生1号楼", "女生宿舍区1号楼4人间", "宿舍", "女生宿舍区", 39.908901, 116.398012));
allLocations.add(new AmapLocation("女生2号楼", "女生宿舍区2号楼6人间", "宿舍", "女生宿舍区", 39.909001, 116.398112));
allLocations.add(new AmapLocation("研究生公寓", "研究生专用公寓", "宿舍", "研究生区", 39.903456, 116.391234));
// 教学楼类
allLocations.add(new AmapLocation("教学楼A", "主要教学楼A座大型教室", "教学楼", "教学区", 39.906789, 116.395678));
allLocations.add(new AmapLocation("教学楼B", "主要教学楼B座小班教室", "教学楼", "教学区", 39.907123, 116.396234));
allLocations.add(new AmapLocation("教学楼C", "主要教学楼C座多媒体教室", "教学楼", "教学区", 39.905678, 116.394789));
allLocations.add(new AmapLocation("理工楼", "理工学科专业教学楼", "教学楼", "理工区", 39.907456, 116.396567));
allLocations.add(new AmapLocation("文科楼", "文科专业教学楼", "教学楼", "文科区", 39.905234, 116.394123));
// 实验楼类
allLocations.add(new AmapLocation("计算机实验楼", "计算机学院实验室", "实验楼", "实验区", 39.908456, 116.397345));
allLocations.add(new AmapLocation("物理实验楼", "物理学院实验室", "实验楼", "实验区", 39.908556, 116.397445));
allLocations.add(new AmapLocation("化学实验楼", "化学学院实验室", "实验楼", "实验区", 39.908356, 116.397245));
allLocations.add(new AmapLocation("生物实验楼", "生物学院实验室", "实验楼", "实验区", 39.908656, 116.397545));
// 体育设施类
allLocations.add(new AmapLocation("体育馆", "室内体育馆,篮球、羽毛球等", "体育设施", "体育区", 39.904123, 116.392678));
allLocations.add(new AmapLocation("田径场", "标准400米田径场", "体育设施", "体育区", 39.903789, 116.391234));
allLocations.add(new AmapLocation("游泳馆", "室内游泳池", "体育设施", "体育区", 39.904223, 116.392778));
allLocations.add(new AmapLocation("网球场", "室外网球场地", "体育设施", "体育区", 39.903889, 116.391334));
// 行政办公类
allLocations.add(new AmapLocation("行政楼", "学校行政办公楼", "行政办公", "行政区", 39.909012, 116.399123));
allLocations.add(new AmapLocation("教务处", "教务管理办公室", "行政办公", "行政楼2层", 39.909112, 116.399223));
allLocations.add(new AmapLocation("学生处", "学生事务管理办公室", "行政办公", "行政楼3层", 39.909212, 116.399323));
allLocations.add(new AmapLocation("财务处", "财务管理办公室", "行政办公", "行政楼1层", 39.909012, 116.399123));
// 生活服务类
allLocations.add(new AmapLocation("超市", "校内便民超市", "生活服务", "学生活动中心", 39.906123, 116.395456));
allLocations.add(new AmapLocation("银行ATM", "校内银行ATM机", "生活服务", "学生服务中心", 39.905223, 116.394556));
allLocations.add(new AmapLocation("邮局", "校内邮政服务点", "生活服务", "学生服务中心", 39.905323, 116.394656));
allLocations.add(new AmapLocation("医务室", "校医院门诊部", "生活服务", "医务楼", 39.907789, 116.396123));
allLocations.add(new AmapLocation("理发店", "校内理发服务", "生活服务", "学生活动中心", 39.906223, 116.395556));
// 休闲娱乐类
allLocations.add(new AmapLocation("咖啡厅", "校内咖啡休息区", "休闲娱乐", "学生活动中心", 39.906323, 116.395656));
allLocations.add(new AmapLocation("音乐厅", "学校音乐演出厅", "休闲娱乐", "艺术楼", 39.907890, 116.396789));
allLocations.add(new AmapLocation("电影院", "校内小型电影院", "休闲娱乐", "学生活动中心3层", 39.906423, 116.395756));
allLocations.add(new AmapLocation("湖心亭", "校园湖心休息亭", "休闲娱乐", "校园湖中央", 39.905890, 116.394789));
// 重新分类整理
categorizeLocations();
}
private void categorizeLocations() {
categorizedLocations.clear();
for (AmapLocation location : allLocations) {
String category = location.getCategory();
if (!categorizedLocations.containsKey(category)) {
categorizedLocations.put(category, new ArrayList<>());
}
categorizedLocations.get(category).add(location);
}
}
/**
*
*/
public List<AmapLocation> getAllLocations() {
return new ArrayList<>(allLocations);
}
/**
*
*/
public List<AmapLocation> getLocationsByCategory(String category) {
List<AmapLocation> result = categorizedLocations.get(category);
return result != null ? new ArrayList<>(result) : new ArrayList<>();
}
/**
*
*/
public List<String> getAllCategories() {
return new ArrayList<>(categorizedLocations.keySet());
}
/**
*
*/
public List<AmapLocation> searchLocations(String keyword) {
List<AmapLocation> results = new ArrayList<>();
String lowerKeyword = keyword.toLowerCase();
for (AmapLocation location : allLocations) {
if (location.getName().toLowerCase().contains(lowerKeyword) ||
location.getDescription().toLowerCase().contains(lowerKeyword) ||
location.getCategory().toLowerCase().contains(lowerKeyword) ||
location.getArea().toLowerCase().contains(lowerKeyword)) {
results.add(location);
}
}
return results;
}
/**
*
*/
public List<AmapLocation> getNearbyLocations(AmapLocation centerLocation, double radiusKm) {
List<AmapLocation> nearbyLocations = new ArrayList<>();
for (AmapLocation location : allLocations) {
double distance = centerLocation.calculateDistance(location) / 1000.0; // 转换为公里
if (distance <= radiusKm) {
nearbyLocations.add(location);
}
}
// 按距离排序
nearbyLocations.sort((l1, l2) -> {
double d1 = centerLocation.calculateDistance(l1);
double d2 = centerLocation.calculateDistance(l2);
return Double.compare(d1, d2);
});
return nearbyLocations;
}
/**
*
*/
public void addCustomLocation(AmapLocation location) {
allLocations.add(location);
categorizeLocations();
saveLocationsToPreferences();
}
/**
*
*/
public void removeCustomLocation(AmapLocation location) {
allLocations.remove(location);
categorizeLocations();
saveLocationsToPreferences();
}
/**
*
*/
public void addToFavorites(String locationName) {
if (!favoriteLocationNames.contains(locationName)) {
favoriteLocationNames.add(locationName);
saveFavoritesToPreferences();
}
}
/**
*
*/
public void removeFromFavorites(String locationName) {
favoriteLocationNames.remove(locationName);
saveFavoritesToPreferences();
}
/**
*
*/
public List<AmapLocation> getFavoriteLocations() {
List<AmapLocation> favorites = new ArrayList<>();
for (String name : favoriteLocationNames) {
for (AmapLocation location : allLocations) {
if (location.getName().equals(name)) {
favorites.add(location);
break;
}
}
}
return favorites;
}
/**
*
*/
public boolean isFavorite(String locationName) {
return favoriteLocationNames.contains(locationName);
}
/**
*
*/
public AmapLocation findLocationByName(String name) {
for (AmapLocation location : allLocations) {
if (location.getName().equals(name)) {
return location;
}
}
return null;
}
/**
*
*/
public List<AmapLocation> getPopularLocations(int limit) {
List<AmapLocation> popular = new ArrayList<>();
// 简单的热门度算法:按分类中位置数量排序,取每个分类的第一个
for (String category : categorizedLocations.keySet()) {
List<AmapLocation> categoryList = categorizedLocations.get(category);
if (!categoryList.isEmpty()) {
popular.add(categoryList.get(0));
}
}
return popular.subList(0, Math.min(limit, popular.size()));
}
// 本地存储相关方法
private void saveLocationsToPreferences() {
String json = gson.toJson(allLocations);
preferences.edit().putString(KEY_LOCATIONS, json).apply();
}
private void loadLocationsFromPreferences() {
String json = preferences.getString(KEY_LOCATIONS, "");
if (!json.isEmpty()) {
Type listType = new TypeToken<List<AmapLocation>>(){}.getType();
List<AmapLocation> savedLocations = gson.fromJson(json, listType);
if (savedLocations != null) {
allLocations.addAll(savedLocations);
categorizeLocations();
}
} else {
categorizeLocations();
}
}
private void saveFavoritesToPreferences() {
String json = gson.toJson(favoriteLocationNames);
preferences.edit().putString(KEY_FAVORITES, json).apply();
}
private void loadFavoritesFromPreferences() {
String json = preferences.getString(KEY_FAVORITES, "");
if (!json.isEmpty()) {
Type listType = new TypeToken<List<String>>(){}.getType();
List<String> savedFavorites = gson.fromJson(json, listType);
if (savedFavorites != null) {
favoriteLocationNames = savedFavorites;
}
}
}
}

@ -0,0 +1,201 @@
package com.example.myapplication;
import java.io.Serializable;
public class Course implements Serializable {
private String id;
private String name; // 课程名称
private String teacher; // 任课教师
private String location; // 上课地点
private String time; // 上课时间 (格式:周几 时间段)
private int dayOfWeek; // 星期几 (1-7)
private int startPeriod; // 开始节次(兼容旧数据)
private int endPeriod; // 结束节次(兼容旧数据)
private int timeSlot; // 时间段索引 (0-5)
private String semester; // 学期
private int startWeek; // 开始周次
private int endWeek; // 结束周次
private boolean reminderEnabled; // 是否启用提醒
private int reminderMinutes; // 提前提醒分钟数
private int reminderAdvanceSeconds; // 提前提醒秒数
private boolean isImported; // 是否是从教务系统导入的课程
public Course() {
this.id = String.valueOf(System.currentTimeMillis());
this.reminderEnabled = false;
this.reminderMinutes = 10;
this.reminderAdvanceSeconds = this.reminderMinutes * 60;
this.timeSlot = -1; // 默认未设置
this.startWeek = 1;
this.endWeek = 20;
this.isImported = false; // 默认手动添加
}
public Course(String name, String teacher, String location, int dayOfWeek,
int startPeriod, int endPeriod) {
this();
this.name = name;
this.teacher = teacher;
this.location = location;
this.dayOfWeek = dayOfWeek;
this.startPeriod = startPeriod;
this.endPeriod = endPeriod;
this.time = formatTime();
}
public Course(String name, String teacher, String location, int dayOfWeek, int timeSlot) {
this();
this.name = name;
this.teacher = teacher;
this.location = location;
this.dayOfWeek = dayOfWeek;
this.timeSlot = timeSlot;
this.startPeriod = timeSlotToPeriod(timeSlot);
this.endPeriod = this.startPeriod + 1; // 占据两节连续
this.time = formatTimeSlot();
}
private String formatTime() {
String[] days = {"", "周一", "周二", "周三", "周四", "周五", "周六", "周日"};
return days[dayOfWeek] + " 第" + startPeriod + "-" + endPeriod + "节";
}
private String formatTimeSlot() {
String[] days = {"", "周一", "周二", "周三", "周四", "周五", "周六", "周日"};
String[] timeSlots = {
"08:00-11:30",
"13:00-16:30",
"17:00-20:30",
"21:00-24:30",
"待定"
};
if (timeSlot >= 0 && timeSlot < timeSlots.length) {
return days[dayOfWeek] + " " + timeSlots[timeSlot];
}
return days[dayOfWeek] + " 第" + startPeriod + "-" + endPeriod + "节";
}
private int timeSlotToPeriod(int slot) {
// 时间段0对应第1-2节时间段1对应第3-4节时间段2对应第5-6节
// 时间段3对应第7-8节时间段4对应第9-10节
if (slot == 0) return 1; // 第1-2节
if (slot == 1) return 3; // 第3-4节
if (slot == 2) return 5; // 第5-6节
if (slot == 3) return 7; // 第7-8节
if (slot == 4) return 9; // 第9-10节
return 1;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public String getTeacher() { return teacher; }
public void setTeacher(String teacher) { this.teacher = teacher; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getTime() { return time; }
public void setTime(String time) { this.time = time; }
public int getDayOfWeek() { return dayOfWeek; }
public void setDayOfWeek(int dayOfWeek) {
this.dayOfWeek = dayOfWeek;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public int getStartPeriod() { return startPeriod; }
public void setStartPeriod(int startPeriod) {
this.startPeriod = startPeriod;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public int getEndPeriod() { return endPeriod; }
public void setEndPeriod(int endPeriod) {
this.endPeriod = endPeriod;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public int getTimeSlot() { return timeSlot; }
public void setTimeSlot(int timeSlot) {
this.timeSlot = timeSlot;
this.time = formatTimeSlot();
}
public String getSemester() { return semester; }
public void setSemester(String semester) { this.semester = semester; }
public boolean isReminderEnabled() { return reminderEnabled; }
public void setReminderEnabled(boolean reminderEnabled) { this.reminderEnabled = reminderEnabled; }
public int getReminderMinutes() {
if (reminderMinutes <= 0 && reminderAdvanceSeconds > 0) {
return reminderAdvanceSeconds / 60;
}
return reminderMinutes;
}
public void setReminderMinutes(int reminderMinutes) {
this.reminderMinutes = reminderMinutes;
if (reminderMinutes > 0) {
this.reminderAdvanceSeconds = reminderMinutes * 60;
}
}
public int getReminderAdvanceSeconds() {
if (reminderAdvanceSeconds <= 0 && reminderMinutes > 0) {
reminderAdvanceSeconds = reminderMinutes * 60;
}
return reminderAdvanceSeconds;
}
public void setReminderAdvanceSeconds(int reminderAdvanceSeconds) {
this.reminderAdvanceSeconds = reminderAdvanceSeconds;
if (reminderAdvanceSeconds >= 60) {
this.reminderMinutes = reminderAdvanceSeconds / 60;
} else if (reminderAdvanceSeconds > 0) {
this.reminderMinutes = 0;
}
}
public int getStartWeek() { return startWeek; }
public void setStartWeek(int startWeek) { this.startWeek = startWeek; }
public int getEndWeek() { return endWeek; }
public void setEndWeek(int endWeek) { this.endWeek = endWeek; }
public boolean isImported() { return isImported; }
public void setImported(boolean imported) { this.isImported = imported; }
@Override
public String toString() {
String nameStr = name != null ? name : "未知课程";
String timeStr = time != null ? time : "";
String locationStr = location != null ? location : "";
String timeLocationStr = (timeStr.isEmpty() && locationStr.isEmpty()) ? "" :
(" (" + timeStr + (timeStr.isEmpty() || locationStr.isEmpty() ? "" : " ") + locationStr + ")");
return nameStr + timeLocationStr;
}
}

@ -0,0 +1,176 @@
package com.example.myapplication;
import android.app.Activity;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
public class CourseReminderActivity extends Activity {
private MediaPlayer mediaPlayer;
private Vibrator vibrator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置为对话框样式
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_course_reminder);
// 设置窗口属性
Window window = getWindow();
// 允许在锁屏上显示
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true);
setTurnScreenOn(true);
android.app.KeyguardManager keyguardManager = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager != null) {
keyguardManager.requestDismissKeyguard(this, null);
}
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
);
}
// 设置窗口在最顶层
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// 设置窗口类型Android 8.0+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
} else {
window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
// 获取课程信息
String courseName = getIntent().getStringExtra("course_name");
String courseLocation = getIntent().getStringExtra("course_location");
String courseTime = getIntent().getStringExtra("course_time");
int reminderMinutes = getIntent().getIntExtra("reminder_minutes", 10);
// 显示信息
TextView tvCourseName = findViewById(R.id.tv_course_name);
TextView tvCourseInfo = findViewById(R.id.tv_course_info);
TextView tvReminderTime = findViewById(R.id.tv_reminder_time);
tvCourseName.setText(courseName);
tvCourseInfo.setText("地点:" + courseLocation + "\n时间" + courseTime);
tvReminderTime.setText(reminderMinutes + "分钟后上课");
// 设置按钮点击事件
findViewById(R.id.btn_confirm).setOnClickListener(v -> {
stopSoundAndVibration();
finish();
});
findViewById(R.id.btn_close).setOnClickListener(v -> {
stopSoundAndVibration();
finish();
});
// 播放提示音和震动
playNotificationSound();
vibrate();
}
/**
*
*/
private void playNotificationSound() {
try {
// 使用系统默认通知音
Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(this, notificationUri);
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
mediaPlayer.setAudioAttributes(audioAttributes);
mediaPlayer.setLooping(false);
mediaPlayer.prepare();
mediaPlayer.start();
// 播放完成后释放资源
mediaPlayer.setOnCompletionListener(mp -> {
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
*/
private void vibrate() {
try {
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator()) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
// 震动模式:等待-震动-等待-震动
long[] pattern = {0, 300, 200, 300};
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
} else {
// 旧版本API
long[] pattern = {0, 300, 200, 300};
vibrator.vibrate(pattern, -1);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
*/
private void stopSoundAndVibration() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
mediaPlayer.release();
mediaPlayer = null;
}
if (vibrator != null) {
vibrator.cancel();
vibrator = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopSoundAndVibration();
}
@Override
public void onBackPressed() {
stopSoundAndVibration();
super.onBackPressed();
}
}

@ -0,0 +1,171 @@
package com.example.myapplication;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import java.util.Calendar;
import java.util.List;
public class CourseReminderManager {
private static final String TAG = "CourseReminderManager";
private Context context;
private AlarmManager alarmManager;
private DataManager dataManager;
public CourseReminderManager(Context context) {
this.context = context;
this.alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
this.dataManager = new DataManager(context);
}
/**
*
*/
public void setReminder(Course course) {
if (!course.isReminderEnabled()) {
cancelReminder(course);
return;
}
// 获取下次上课时间
Calendar nextClassTime = getNextClassTime(course);
if (nextClassTime == null) {
Log.w(TAG, "无法计算下次上课时间");
return;
}
// 减去提前提醒时间
nextClassTime.add(Calendar.MINUTE, -course.getReminderMinutes());
// 如果时间已过,设置下周的提醒
if (nextClassTime.getTimeInMillis() <= System.currentTimeMillis()) {
nextClassTime.add(Calendar.WEEK_OF_YEAR, 1);
}
Intent intent = new Intent(context, CourseReminderReceiver.class);
intent.putExtra("course_id", course.getId());
intent.putExtra("course_name", course.getName());
intent.putExtra("course_location", course.getLocation());
intent.putExtra("course_time", course.getTime());
intent.putExtra("reminder_minutes", course.getReminderMinutes());
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
course.getId().hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 设置重复提醒(每周)
if (alarmManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
nextClassTime.getTimeInMillis(),
pendingIntent
);
} else {
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP,
nextClassTime.getTimeInMillis(),
AlarmManager.INTERVAL_DAY * 7,
pendingIntent
);
}
Log.d(TAG, "设置提醒: " + course.getName() + " 在 " + nextClassTime.getTime());
}
}
/**
*
*/
public void cancelReminder(Course course) {
Intent intent = new Intent(context, CourseReminderReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
course.getId().hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
if (alarmManager != null) {
alarmManager.cancel(pendingIntent);
Log.d(TAG, "取消提醒: " + course.getName());
}
}
/**
*
*/
public void refreshAllReminders() {
List<Course> courses = dataManager.getCourses();
for (Course course : courses) {
if (course.isReminderEnabled()) {
setReminder(course);
}
}
}
/**
*
*/
private Calendar getNextClassTime(Course course) {
Calendar calendar = Calendar.getInstance();
int currentDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
// 转换Calendar.SUNDAY=1, 我们的周日=7
int targetDayOfWeek = course.getDayOfWeek() == 7 ? Calendar.SUNDAY : course.getDayOfWeek() + 1;
// 计算需要前进的天数
int daysToAdd = targetDayOfWeek - currentDayOfWeek;
if (daysToAdd < 0) {
daysToAdd += 7;
} else if (daysToAdd == 0) {
// 今天,检查时间是否已过
int[] classTime = getClassStartTime(course.getStartPeriod());
calendar.set(Calendar.HOUR_OF_DAY, classTime[0]);
calendar.set(Calendar.MINUTE, classTime[1]);
calendar.set(Calendar.SECOND, 0);
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
daysToAdd = 7; // 下周
}
}
calendar.add(Calendar.DAY_OF_YEAR, daysToAdd);
// 设置上课时间
int[] classTime = getClassStartTime(course.getStartPeriod());
calendar.set(Calendar.HOUR_OF_DAY, classTime[0]);
calendar.set(Calendar.MINUTE, classTime[1]);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
/**
*
*/
private int[] getClassStartTime(int period) {
switch (period) {
case 1: return new int[]{8, 0};
case 2: return new int[]{8, 50};
case 3: return new int[]{10, 0};
case 4: return new int[]{10, 50};
case 5: return new int[]{14, 0};
case 6: return new int[]{14, 50};
case 7: return new int[]{16, 0};
case 8: return new int[]{16, 50};
case 9: return new int[]{19, 0};
case 10: return new int[]{19, 50};
case 11: return new int[]{20, 40};
case 12: return new int[]{21, 30};
default: return new int[]{8, 0};
}
}
}

@ -0,0 +1,137 @@
package com.example.myapplication;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
public class CourseReminderReceiver extends BroadcastReceiver {
private static final String CHANNEL_ID = "course_reminder_channel";
private static final String CHANNEL_NAME = "课程提醒";
@Override
public void onReceive(Context context, Intent intent) {
String courseId = intent.getStringExtra("course_id");
String courseName = intent.getStringExtra("course_name");
String courseLocation = intent.getStringExtra("course_location");
String courseTime = intent.getStringExtra("course_time");
int reminderMinutes = intent.getIntExtra("reminder_minutes", 10);
// 创建通知渠道
createNotificationChannel(context);
// 1. 显示自定义弹窗(确保可见)
showReminderDialog(context, courseName, courseLocation, courseTime, reminderMinutes);
// 2. 同时显示系统通知(备用)
showNotification(context, courseId, courseName, courseLocation, courseTime, reminderMinutes);
// 重新设置下次提醒
DataManager dataManager = new DataManager(context);
for (Course course : dataManager.getCourses()) {
if (course.getId().equals(courseId) && course.isReminderEnabled()) {
CourseReminderManager reminderManager = new CourseReminderManager(context);
reminderManager.setReminder(course);
break;
}
}
}
/**
*
*/
private void showReminderDialog(Context context, String courseName, String courseLocation,
String courseTime, int reminderMinutes) {
Intent dialogIntent = new Intent(context, CourseReminderActivity.class);
dialogIntent.putExtra("course_name", courseName);
dialogIntent.putExtra("course_location", courseLocation);
dialogIntent.putExtra("course_time", courseTime);
dialogIntent.putExtra("reminder_minutes", reminderMinutes);
dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
try {
context.startActivity(dialogIntent);
} catch (Exception e) {
e.printStackTrace();
// 如果弹窗启动失败,至少有通知
}
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH // 高优先级
);
channel.setDescription("课程上课提醒通知");
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{0, 300, 200, 300});
channel.enableLights(true);
channel.setLightColor(android.graphics.Color.GREEN);
channel.setShowBadge(true);
channel.setLockscreenVisibility(android.app.Notification.VISIBILITY_PUBLIC);
channel.setBypassDnd(true); // 绕过勿扰模式
channel.setSound(
android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION),
new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_NOTIFICATION)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
private void showNotification(Context context, String courseId, String courseName,
String courseLocation, String courseTime, int reminderMinutes) {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("fragment", "timetable");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
courseId.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 创建大文本样式
NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle()
.bigText("📍 地点:" + courseLocation + "\n⏰ 时间:" + courseTime + "\n⏱ " + reminderMinutes + "分钟后上课")
.setBigContentTitle("📚 " + courseName);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("📚 课程提醒:" + courseName)
.setContentText(reminderMinutes + "分钟后上课 | " + courseLocation)
.setStyle(bigTextStyle)
.setPriority(NotificationCompat.PRIORITY_MAX) // 最高优先级
.setCategory(NotificationCompat.CATEGORY_ALARM) // 闹钟类别
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏可见
.setAutoCancel(true)
.setOngoing(false)
.setContentIntent(pendingIntent)
.setFullScreenIntent(pendingIntent, true) // 全屏Intent重要
.setVibrate(new long[]{0, 300, 200, 300}) // 震动模式
.setLights(android.graphics.Color.GREEN, 1000, 1000) // 呼吸灯
.setSound(android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION)) // 提示音
.setDefaults(NotificationCompat.DEFAULT_ALL) // 默认所有效果
.setTimeoutAfter(60000); // 60秒后自动消失
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
notificationManager.notify(courseId.hashCode(), builder.build());
}
}
}

@ -0,0 +1,246 @@
package com.example.myapplication;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class DataManager {
private static final String PREFS_NAME = "UniversityAssistant";
private static final String KEY_COURSES = "courses";
private static final String KEY_NOTES = "notes";
private static final String KEY_GRADES = "grades";
private static final String KEY_STUDENT_NAME = "student_name";
private static final String KEY_OVERALL_GPA = "overall_gpa";
private static final String KEY_TERM_START_DATE = "term_start_date";
private static final TypeToken<List<Course>> COURSE_TYPE_TOKEN = new TypeToken<List<Course>>(){};
private static final TypeToken<List<Note>> NOTE_TYPE_TOKEN = new TypeToken<List<Note>>(){};
private static final TypeToken<List<Grade>> GRADE_TYPE_TOKEN = new TypeToken<List<Grade>>(){};
private SharedPreferences prefs;
private Gson gson;
private Context context;
private UserManager userManager;
public DataManager(Context context) {
this.context = context;
this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
this.userManager = UserManager.getInstance(context);
}
/**
* IDkey
*/
private String getUserKey(String baseKey) {
String userId = userManager.getCurrentUserId();
if (userId == null || userId.isEmpty()) {
// 如果没有登录用户使用默认key兼容旧数据
return baseKey;
}
return userId + "_" + baseKey;
}
// 课程管理
public List<Course> getCourses() {
String userKey = getUserKey(KEY_COURSES);
String json = prefs.getString(userKey, "[]");
Type type = COURSE_TYPE_TOKEN.getType();
List<Course> courses = gson.fromJson(json, type);
return courses != null ? courses : new ArrayList<>();
}
public void saveCourses(List<Course> courses) {
String userKey = getUserKey(KEY_COURSES);
String json = gson.toJson(courses);
prefs.edit().putString(userKey, json).apply();
}
public void addCourse(Course course) {
List<Course> courses = getCourses();
courses.add(course);
saveCourses(courses);
}
public void deleteCourse(String courseId) {
if (courseId == null) {
System.out.println("deleteCourse: courseId为null无法删除");
return;
}
List<Course> courses = getCourses();
System.out.println("deleteCourse: 当前课程总数: " + courses.size() + ", 要删除的ID: " + courseId);
List<Course> toRemove = new ArrayList<>();
for (Course course : courses) {
if (course != null) {
String id = course.getId();
if (id != null && courseId.equals(id)) {
toRemove.add(course);
System.out.println("deleteCourse: 找到要删除的课程: " + course.getName() + ", ID: " + id);
}
}
}
if (toRemove.isEmpty()) {
System.out.println("deleteCourse: 警告 - 未找到要删除的课程ID: " + courseId);
// 打印所有课程的ID用于调试
for (Course course : courses) {
if (course != null) {
System.out.println(" 现有课程: " + course.getName() + ", ID: " + course.getId());
}
}
} else {
courses.removeAll(toRemove);
saveCourses(courses);
System.out.println("deleteCourse: 成功删除 " + toRemove.size() + " 门课程,剩余: " + courses.size());
}
}
public void updateCourse(Course updatedCourse) {
if (updatedCourse == null || updatedCourse.getId() == null) return;
List<Course> courses = getCourses();
String updatedId = updatedCourse.getId();
for (int i = 0; i < courses.size(); i++) {
Course course = courses.get(i);
if (course != null && updatedId.equals(course.getId())) {
courses.set(i, updatedCourse);
break;
}
}
saveCourses(courses);
}
/**
*
*/
public void clearAllCourses() {
List<Course> courses = getCourses();
System.out.println("DataManager.clearAllCourses: 清空所有课程数据,共 " + courses.size() + " 门课程");
saveCourses(new ArrayList<>());
System.out.println("DataManager.clearAllCourses: 所有课程数据已清空");
}
// 笔记管理
public List<Note> getNotes() {
String userKey = getUserKey(KEY_NOTES);
String json = prefs.getString(userKey, "[]");
Type type = NOTE_TYPE_TOKEN.getType();
List<Note> notes = gson.fromJson(json, type);
return notes != null ? notes : new ArrayList<>();
}
public void saveNotes(List<Note> notes) {
String userKey = getUserKey(KEY_NOTES);
String json = gson.toJson(notes);
prefs.edit().putString(userKey, json).apply();
}
public void addNote(Note note) {
List<Note> notes = getNotes();
notes.add(note);
saveNotes(notes);
}
public void deleteNote(String noteId) {
if (noteId == null) return;
List<Note> notes = getNotes();
List<Note> toRemove = new ArrayList<>();
for (Note note : notes) {
if (note != null) {
String id = note.getId();
if (id != null && noteId.equals(id)) {
toRemove.add(note);
}
}
}
notes.removeAll(toRemove);
saveNotes(notes);
}
public void updateNote(Note updatedNote) {
if (updatedNote == null || updatedNote.getId() == null) return;
List<Note> notes = getNotes();
String updatedId = updatedNote.getId();
for (int i = 0; i < notes.size(); i++) {
Note note = notes.get(i);
if (note != null && updatedId.equals(note.getId())) {
notes.set(i, updatedNote);
break;
}
}
saveNotes(notes);
}
// 成绩管理
public List<Grade> getGrades() {
String userKey = getUserKey(KEY_GRADES);
String json = prefs.getString(userKey, "[]");
Type type = GRADE_TYPE_TOKEN.getType();
List<Grade> grades = gson.fromJson(json, type);
return grades != null ? grades : new ArrayList<>();
}
public void saveGrades(List<Grade> grades) {
String userKey = getUserKey(KEY_GRADES);
String json = gson.toJson(grades);
prefs.edit().putString(userKey, json).apply();
}
public void addGrade(Grade grade) {
List<Grade> grades = getGrades();
grades.add(grade);
saveGrades(grades);
}
public void deleteGrade(String gradeId) {
if (gradeId == null) return;
List<Grade> grades = getGrades();
List<Grade> toRemove = new ArrayList<>();
for (Grade grade : grades) {
if (grade != null) {
String id = grade.getId();
if (id != null && gradeId.equals(id)) {
toRemove.add(grade);
}
}
}
grades.removeAll(toRemove);
saveGrades(grades);
}
public void saveStudentName(String studentName) {
String userKey = getUserKey(KEY_STUDENT_NAME);
prefs.edit().putString(userKey, studentName != null ? studentName : "").apply();
}
public String getStudentName() {
String userKey = getUserKey(KEY_STUDENT_NAME);
return prefs.getString(userKey, "");
}
public void saveOverallGpa(String overallGpa) {
String userKey = getUserKey(KEY_OVERALL_GPA);
prefs.edit().putString(userKey, overallGpa != null ? overallGpa : "").apply();
}
public String getOverallGpa() {
String userKey = getUserKey(KEY_OVERALL_GPA);
return prefs.getString(userKey, "");
}
public void saveTermStartDate(long termStartMillis) {
String userKey = getUserKey(KEY_TERM_START_DATE);
prefs.edit().putLong(userKey, termStartMillis).apply();
}
public long getTermStartDate() {
String userKey = getUserKey(KEY_TERM_START_DATE);
return prefs.getLong(userKey, 0L);
}
}

@ -0,0 +1,682 @@
package com.example.myapplication;
import android.annotation.SuppressLint;
import android.app.DatePickerDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class EducationImportActivity extends AppCompatActivity {
private WebView webView;
private EditText etUrl;
private Button btnGo;
private Button btnStartImport;
private Button btnLocateTimetable;
private ProgressBar progressBar;
private DataManager dataManager;
private static final String DEFAULT_URL = "http://jwgl.cauc.edu.cn/xtgl/login_slogin.html";
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_education_import);
dataManager = new DataManager(this);
webView = findViewById(R.id.webview);
etUrl = findViewById(R.id.et_url);
btnGo = findViewById(R.id.btn_go);
btnStartImport = findViewById(R.id.btn_start_import);
btnLocateTimetable = findViewById(R.id.btn_locate_timetable);
progressBar = findViewById(R.id.progress_bar);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(false);
settings.setUseWideViewPort(true);
settings.setLoadWithOverviewMode(true);
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
webView.addJavascriptInterface(new JsBridge(), "AndroidBridge");
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
progressBar.setVisibility(newProgress == 100 ? View.GONE : View.VISIBLE);
progressBar.setProgress(newProgress);
}
});
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 允许站内加载;外部 scheme 交给系统
if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) {
return false;
}
try {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(i);
} catch (Exception ignored) {}
return true;
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 检测是否已进入课表页面
if (url != null && (url.contains("xskbcx_cxXskbcxIndex") || url.contains("xskbcx_cxXsKb"))) {
// 自动点击查询按钮加载课表数据
String autoQueryJs = "(function(){try{" +
"var queryBtn = document.getElementById('search_go');" +
"if(queryBtn && queryBtn.style.display !== 'none'){" +
"queryBtn.click();" +
"AndroidBridge.onError('AUTO:已自动点击查询按钮');" +
"}else{" +
"AndroidBridge.onError('TIP:已到课表页面,请手动点击查询按钮');" +
"}" +
"}catch(e){AndroidBridge.onError('AUTO_ERROR:'+e);}})();";
view.evaluateJavascript(autoQueryJs, null);
return;
}
// 次要策略:页面文本包含课表相关关键词
String jsDetect = "(function(){try{var t=document.body.innerText||'';" +
"if(t.indexOf('学生课表')>-1||t.indexOf('课表查询')>-1||t.indexOf('我的课表')>-1||t.indexOf('课表')>-1){AndroidBridge.onError('TIP:已到课表相关页面');}}catch(e){}})();";
view.evaluateJavascript(jsDetect, null);
}
});
btnGo.setOnClickListener(v -> {
String url = etUrl.getText().toString().trim();
if (url.isEmpty()) {
Toast.makeText(this, "请输入教务系统网址", Toast.LENGTH_SHORT).show();
return;
}
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url;
}
webView.loadUrl(url);
});
btnStartImport.setOnClickListener(v -> captureAndParse());
btnLocateTimetable.setOnClickListener(v -> tryNavigateToTimetable());
// 允许验证码与第三方资源加载
try {
android.webkit.CookieManager cm = android.webkit.CookieManager.getInstance();
cm.setAcceptCookie(true);
cm.setAcceptThirdPartyCookies(webView, true);
} catch (Throwable ignored) {}
// 允许混合内容(有些验证码/资源走 http/https 混合)
try {
settings.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
} catch (Throwable ignored) {}
// 自动打开指定登录页
etUrl.setText(DEFAULT_URL);
webView.loadUrl(DEFAULT_URL);
}
private void captureAndParse() {
Toast.makeText(this, "开始自动滚动页面并解析所有课程…", Toast.LENGTH_SHORT).show();
// 自动滚动页面并解析所有课程
// 改进:更完善的滚动策略,确保能获取到所有内容
// 注意避免在JavaScript字符串中使用Java注释可能导致语法错误
String scrollAndParseJs = "(function(){" +
"try{" +
"var scrollStep = 400;" +
"var scrollDelay = 200;" +
"var currentScroll = 0;" +
"var maxScroll = Math.max(" +
"document.documentElement.scrollHeight || 0," +
"document.body.scrollHeight || 0," +
"document.documentElement.clientHeight || 0," +
"document.body.clientHeight || 0" +
");" +
"console.log('开始滚动,最大滚动距离: ' + maxScroll);" +
"var scrollCount = 0;" +
"var maxScrollCount = Math.ceil(maxScroll / scrollStep) + 5;" +
"var scrollInterval = setInterval(function(){" +
"scrollCount++;" +
"window.scrollTo(0, currentScroll);" +
"console.log('滚动到位置: ' + currentScroll + ', 进度: ' + (scrollCount / maxScrollCount * 100).toFixed(1) + '%');" +
"currentScroll += scrollStep;" +
"if(currentScroll >= maxScroll || scrollCount >= maxScrollCount){" +
"clearInterval(scrollInterval);" +
"console.log('滚动完成,滚动次数: ' + scrollCount);" +
"setTimeout(function(){" +
"window.scrollTo(0, maxScroll);" +
"setTimeout(function(){" +
"window.scrollTo(0, 0);" +
"setTimeout(function(){" +
"var html = document.documentElement.outerHTML;" +
"var htmlLength = html.length;" +
"console.log('获取HTML完成长度: ' + htmlLength);" +
"AndroidBridge.onHtml(html);" +
"}, 300);" +
"}, 300);" +
"}, 300);" +
"}" +
"}, scrollDelay);" +
"}catch(e){AndroidBridge.onError('SCROLL_ERROR:'+e.toString());}" +
"})();";
webView.evaluateJavascript(scrollAndParseJs, null);
}
private void tryNavigateToTimetable() {
// 直接导航到指定的课表页面
String timetableUrl = "http://jwgl.cauc.edu.cn/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default";
try {
webView.loadUrl(timetableUrl);
Toast.makeText(this, "正在跳转到课表页面…", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(this, "跳转失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private void showTermStartDateDialog() {
java.util.Calendar calendar = java.util.Calendar.getInstance();
DatePickerDialog dialog = new DatePickerDialog(
EducationImportActivity.this,
(view, year, monthOfYear, dayOfMonth) -> {
java.util.Calendar selected = java.util.Calendar.getInstance();
selected.set(java.util.Calendar.YEAR, year);
selected.set(java.util.Calendar.MONTH, monthOfYear);
selected.set(java.util.Calendar.DAY_OF_MONTH, dayOfMonth);
selected.set(java.util.Calendar.HOUR_OF_DAY, 0);
selected.set(java.util.Calendar.MINUTE, 0);
selected.set(java.util.Calendar.SECOND, 0);
selected.set(java.util.Calendar.MILLISECOND, 0);
long millis = selected.getTimeInMillis();
dataManager.saveTermStartDate(millis);
android.content.Intent refreshIntent = new android.content.Intent("com.example.myapplication.TIMETABLE_REFRESH");
sendBroadcast(refreshIntent);
setResult(RESULT_OK);
finish();
},
calendar.get(java.util.Calendar.YEAR),
calendar.get(java.util.Calendar.MONTH),
calendar.get(java.util.Calendar.DAY_OF_MONTH)
);
dialog.setTitle("请选择第一周第一节课的日期");
dialog.setCancelable(false);
dialog.show();
}
private class JsBridge {
@JavascriptInterface
public void onHtml(String html) {
runOnUiThread(() -> {
try {
System.out.println("开始解析教务系统页面...");
List<Course> parsed = EducationParser.parse(html);
if (parsed == null || parsed.isEmpty()) {
System.out.println("解析结果为空,未识别到课程");
Toast.makeText(EducationImportActivity.this, "未识别到课程,请确认已在课表页面", Toast.LENGTH_LONG).show();
return;
}
System.out.println("==========================================");
System.out.println("解析完成:共解析到 " + parsed.size() + " 门课程");
System.out.println("==========================================");
// 按星期分组统计
int[] coursesByDay = new int[8]; // 0不使用1-7对应周一到周日
for (Course c : parsed) {
if (c != null && c.getDayOfWeek() >= 1 && c.getDayOfWeek() <= 7) {
coursesByDay[c.getDayOfWeek()]++;
}
}
System.out.println("按星期统计:");
String[] dayNames = {"", "周一", "周二", "周三", "周四", "周五", "周六", "周日"};
for (int day = 1; day <= 7; day++) {
System.out.println(" " + dayNames[day] + ": " + coursesByDay[day] + " 门课程");
}
System.out.println("\n所有课程详情:");
for (int i = 0; i < parsed.size(); i++) {
Course c = parsed.get(i);
if (c != null) {
System.out.println(" [" + (i + 1) + "] " + c.getName());
System.out.println(" 星期: " + dayNames[c.getDayOfWeek()] +
", 时间段: " + c.getTimeSlot() +
" (" + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
System.out.println(" 周次: 第" + c.getStartWeek() + "-" + c.getEndWeek() + "周");
System.out.println(" 教师: " + c.getTeacher() + ", 地点: " + c.getLocation());
}
}
System.out.println("==========================================");
// 获取用户管理器
UserManager userManager = UserManager.getInstance(EducationImportActivity.this);
// 从Intent中获取用户信息如果是从登录页面传递过来的
String userId = getIntent().getStringExtra("userId");
String userName = getIntent().getStringExtra("userName");
// 获取切换前的用户ID
String oldUserId = userManager.getCurrentUserId();
System.out.println("========================================");
System.out.println("导入前用户ID: " + oldUserId);
System.out.println("========================================");
// 如果传入了用户信息,切换到新用户
if (userId != null && !userId.trim().isEmpty()) {
String newUserId = userId.trim();
// 如果用户ID不同需要切换用户
if (!newUserId.equals(oldUserId)) {
System.out.println("检测到用户切换: " + oldUserId + " -> " + newUserId);
userManager.switchUser(newUserId, userName != null ? userName.trim() : "");
// 等待用户切换完成使用commit()确保同步)
// 重新创建DataManager确保使用新用户的数据
dataManager = new DataManager(EducationImportActivity.this);
// 验证用户切换是否成功
String verifyUserId = userManager.getCurrentUserId();
if (!newUserId.equals(verifyUserId)) {
System.out.println("错误:用户切换失败!期望: " + newUserId + ", 实际: " + verifyUserId);
Toast.makeText(EducationImportActivity.this, "用户切换失败,请重试", Toast.LENGTH_LONG).show();
return;
}
System.out.println("用户切换成功当前用户ID: " + verifyUserId);
}
}
// 验证当前用户ID确保使用正确的用户
String finalUserId = userManager.getCurrentUserId();
if (finalUserId == null || finalUserId.trim().isEmpty()) {
System.out.println("警告:当前没有登录用户,将使用默认存储(可能与其他用户数据混合)");
Toast.makeText(EducationImportActivity.this, "警告:未登录用户,数据可能与其他用户混合", Toast.LENGTH_LONG).show();
} else {
System.out.println("确认导入用户ID: " + finalUserId);
}
// 清空当前用户的所有课程(因为要导入新课程)
// 使用clearAllCourses()方法,更高效且确保完全清空
List<Course> existingCourses = dataManager.getCourses();
System.out.println("========================================");
System.out.println("准备清空当前用户(" + finalUserId + ")的现有课程,共 " + existingCourses.size() + " 门");
System.out.println("========================================");
// 使用clearAllCourses()方法,确保完全清空当前用户的所有课程
dataManager.clearAllCourses();
// 验证清空是否成功
List<Course> afterClear = dataManager.getCourses();
if (!afterClear.isEmpty()) {
System.out.println("警告:清空后仍有 " + afterClear.size() + " 门课程,强制清空");
dataManager.clearAllCourses();
}
System.out.println("课程清空完成,当前用户课程数: " + dataManager.getCourses().size());
// 保存新导入的课程(确保所有解析到的课程都导入)
System.out.println("\n========================================");
System.out.println("开始导入课程到数据库");
System.out.println("========================================");
int importedCount = 0;
int skippedCount = 0;
int errorCount = 0;
for (Course c : parsed) {
if (c != null && c.getName() != null && !c.getName().trim().isEmpty()) {
try {
// 自动修正已知的错误位置(在验证之前)
fixKnownCoursePositions(c);
// 确保课程标记为导入的
c.setImported(true);
// 验证课程数据的完整性(修正后再次验证)
if (c.getDayOfWeek() < 1 || c.getDayOfWeek() > 7) {
System.out.println("警告:课程 " + c.getName() + " 的星期信息无效: " + c.getDayOfWeek());
// 对于毛泽东思想课程,如果仍然无效,强制设置为周一
if (c.getName().contains("毛泽东") && !c.getName().contains("新时代")) {
System.out.println("强制修正毛泽东思想课程dayOfWeek无效设置为周一");
c.setDayOfWeek(1);
c.setTimeSlot(1);
c.setStartPeriod(3);
c.setEndPeriod(4);
if (c.getStartWeek() < 1 || c.getEndWeek() < 1) {
c.setStartWeek(1);
c.setEndWeek(16);
}
} else {
errorCount++;
continue;
}
}
if (c.getStartWeek() < 1 || c.getEndWeek() < 1 || c.getStartWeek() > c.getEndWeek()) {
System.out.println("警告:课程 " + c.getName() + " 的周次信息无效: " + c.getStartWeek() + "-" + c.getEndWeek());
// 对于毛泽东思想课程,如果周次无效,设置默认值
if (c.getName().contains("毛泽东") && !c.getName().contains("新时代")) {
System.out.println("强制修正毛泽东思想课程周次无效设置为1-16周");
c.setStartWeek(1);
c.setEndWeek(16);
} else {
errorCount++;
continue;
}
}
// 导入课程
dataManager.addCourse(c);
importedCount++;
System.out.println("✓ 已导入 [" + importedCount + "] " + c.getName());
System.out.println(" 星期: " + dayNames[c.getDayOfWeek()] +
", 时间段: " + c.getTimeSlot() +
" (" + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
System.out.println(" 周次: 第" + c.getStartWeek() + "-" + c.getEndWeek() + "周");
} catch (Exception e) {
System.out.println("✗ 导入失败: " + c.getName() + " - " + e.getMessage());
errorCount++;
e.printStackTrace();
}
} else {
skippedCount++;
System.out.println("跳过无效课程对象");
}
}
System.out.println("\n========================================");
System.out.println("导入完成统计:");
System.out.println(" 成功导入: " + importedCount + " 门课程");
System.out.println(" 跳过: " + skippedCount + " 门课程");
System.out.println(" 错误: " + errorCount + " 门课程");
System.out.println(" 总计: " + parsed.size() + " 门课程");
System.out.println("========================================");
// 打印所有已导入课程的详细信息,按课程名分组
System.out.println("\n========================================");
System.out.println("所有已导入课程详细信息(按课程名分组):");
System.out.println("========================================");
List<Course> allImportedCourses = dataManager.getCourses();
Map<String, List<Course>> coursesByName = new HashMap<>();
for (Course c : allImportedCourses) {
if (c != null && c.getName() != null) {
String name = c.getName();
coursesByName.putIfAbsent(name, new ArrayList<>());
coursesByName.get(name).add(c);
}
}
for (Map.Entry<String, List<Course>> entry : coursesByName.entrySet()) {
String courseName = entry.getKey();
List<Course> courseList = entry.getValue();
System.out.println("\n【" + courseName + "】共 " + courseList.size() + " 个实例:");
for (int i = 0; i < courseList.size(); i++) {
Course c = courseList.get(i);
System.out.println(" 实例 " + (i + 1) + ":");
System.out.println(" 星期: " + dayNames[c.getDayOfWeek()] +
" (星期" + c.getDayOfWeek() + ")");
System.out.println(" 时间段: " + c.getTimeSlot() +
" (" + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
System.out.println(" 周次: 第" + c.getStartWeek() + "-" + c.getEndWeek() + "周");
System.out.println(" 地点: " + c.getLocation());
System.out.println(" 教师: " + c.getTeacher());
}
}
// 按周次分组显示课程
System.out.println("\n========================================");
System.out.println("所有已导入课程(按周次分组):");
System.out.println("========================================");
Map<String, List<Course>> coursesByWeek = new HashMap<>();
for (Course c : allImportedCourses) {
if (c != null && c.getName() != null) {
String weekKey = "第" + c.getStartWeek() + "-" + c.getEndWeek() + "周";
coursesByWeek.putIfAbsent(weekKey, new ArrayList<>());
coursesByWeek.get(weekKey).add(c);
}
}
// 按周次范围排序
List<String> weekKeys = new ArrayList<>(coursesByWeek.keySet());
weekKeys.sort((a, b) -> {
int startA = Integer.parseInt(a.substring(1, a.indexOf("-")));
int startB = Integer.parseInt(b.substring(1, b.indexOf("-")));
return Integer.compare(startA, startB);
});
for (String weekKey : weekKeys) {
List<Course> courseList = coursesByWeek.get(weekKey);
System.out.println("\n【" + weekKey + "】共 " + courseList.size() + " 门课程:");
for (Course c : courseList) {
System.out.println(" - " + c.getName() +
" (" + dayNames[c.getDayOfWeek()] +
", 时间段" + c.getTimeSlot() +
", " + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
}
}
System.out.println("\n========================================");
// 最终验证:确保数据已正确保存到当前用户
String verifyUserId = userManager.getCurrentUserId();
List<Course> finalCourses = dataManager.getCourses();
System.out.println("========================================");
System.out.println("导入完成验证:");
System.out.println(" 当前用户ID: " + verifyUserId);
System.out.println(" 数据库中课程数: " + finalCourses.size());
System.out.println(" 成功导入数: " + importedCount);
System.out.println("========================================");
// 验证数据一致性
if (importedCount > 0 && finalCourses.size() != importedCount) {
System.out.println("警告:导入数量不一致!期望: " + importedCount + ", 实际: " + finalCourses.size());
// 这可能是正常的,因为可能有重复课程被合并或其他原因
}
// 验证用户ID一致性确保数据保存到了正确的用户
if (verifyUserId != null && !verifyUserId.equals(finalUserId)) {
System.out.println("严重错误用户ID不一致导入时: " + finalUserId + ", 验证时: " + verifyUserId);
Toast.makeText(EducationImportActivity.this,
"错误:用户数据可能保存错误,请重新导入",
Toast.LENGTH_LONG).show();
return;
}
if (importedCount > 0) {
String successMsg = "已成功导入 " + importedCount + " 门课程";
if (verifyUserId != null) {
successMsg += "(用户: " + verifyUserId + "";
}
Toast.makeText(EducationImportActivity.this, successMsg, Toast.LENGTH_LONG).show();
showTermStartDateDialog();
} else {
Toast.makeText(EducationImportActivity.this,
"未导入任何课程,请检查解析结果",
Toast.LENGTH_LONG).show();
android.content.Intent refreshIntent = new android.content.Intent("com.example.myapplication.TIMETABLE_REFRESH");
sendBroadcast(refreshIntent);
setResult(RESULT_OK);
finish();
}
} catch (Exception e) {
System.out.println("导入失败: " + e.getMessage());
e.printStackTrace();
Toast.makeText(EducationImportActivity.this, "解析失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
/**
*
*/
private void fixKnownCoursePositions(Course course) {
String courseName = course.getName();
int dayOfWeek = course.getDayOfWeek();
int timeSlot = course.getTimeSlot();
int startWeek = course.getStartWeek();
int endWeek = course.getEndWeek();
// 1. 毛泽东思想和中国特色社会主义理论体系概论(B) - 1-16周应该在周一3-4节时间段112-14周应该在周三5-6节时间段2
if (courseName != null && courseName.contains("毛泽东思想和中国特色社会主义理论体系概论") &&
!courseName.contains("新时代中国特色社会主义")) {
System.out.println("检测到毛泽东思想课程,当前信息: 星期" + dayOfWeek + ", 时间段" + timeSlot + ", 周次" + startWeek + "-" + endWeek);
// 如果dayOfWeek无效<1或>7先根据周次信息推断正确的星期
boolean dayOfWeekInvalid = (dayOfWeek < 1 || dayOfWeek > 7);
// 1-16周必须在周一3-4节
if (startWeek == 1 && endWeek == 16) {
if (dayOfWeekInvalid || dayOfWeek != 1 || timeSlot != 1) {
System.out.println("修正毛泽东思想课程1-16周位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期1, 时间段1)");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
}
}
// 12-14周必须在周三5-6节
else if (startWeek == 12 && endWeek == 14) {
if (dayOfWeekInvalid || dayOfWeek != 3 || timeSlot != 2) {
System.out.println("修正毛泽东思想课程12-14周位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期3, 时间段2)");
course.setDayOfWeek(3);
course.setTimeSlot(2);
course.setStartPeriod(5);
course.setEndPeriod(6);
}
}
// 如果周次不是1-16周也不是12-14周但位置不对根据位置判断
else {
// 如果dayOfWeek无效默认设置为周一3-4节1-16周
if (dayOfWeekInvalid) {
System.out.println("修正毛泽东思想课程dayOfWeek无效根据默认规则设置为周一3-4节1-16周");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(1);
course.setEndWeek(16);
}
// 如果在周三5-6节但周次不是12-14周可能是错误的修正为周一3-4节1-16周
else if (dayOfWeek == 3 && timeSlot == 2 && (startWeek != 12 || endWeek != 14)) {
System.out.println("修正毛泽东思想课程在周三5-6节但周次不是12-14周修正为周一3-4节1-16周");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(1);
course.setEndWeek(16);
}
// 如果在周一3-4节但周次不是1-16周保持位置但可能需要修正周次
else if (dayOfWeek == 1 && timeSlot == 1 && (startWeek != 1 || endWeek != 16)) {
System.out.println("修正毛泽东思想课程在周一3-4节但周次不是1-16周修正周次为1-16周");
course.setStartWeek(1);
course.setEndWeek(16);
}
// 如果在其他位置且不是周三5-6节12-14周修正为周一3-4节1-16周
else if (dayOfWeek != 1 || timeSlot != 1) {
if (!(dayOfWeek == 3 && timeSlot == 2)) {
System.out.println("修正:毛泽东思想课程位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ", 周次" + startWeek + "-" + endWeek + ") -> 修正为 (星期1, 时间段1, 1-16周)");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(1);
course.setEndWeek(16);
}
}
}
// 最终验证:确保修正后的值有效
if (course.getDayOfWeek() < 1 || course.getDayOfWeek() > 7) {
System.out.println("严重错误修正后dayOfWeek仍然无效强制设置为周一");
course.setDayOfWeek(1);
}
if (course.getTimeSlot() < 0) {
System.out.println("严重错误修正后timeSlot仍然无效强制设置为时间段1");
course.setTimeSlot(1);
}
System.out.println("毛泽东思想课程修正完成: 星期" + course.getDayOfWeek() + ", 时间段" + course.getTimeSlot() + ", 周次" + course.getStartWeek() + "-" + course.getEndWeek());
}
// 2. 习近平新时代中国特色社会主义思想概论B - 应该在周二3-4节时间段1
if (courseName.contains("习近平新时代中国特色社会主义思想") ||
(courseName.contains("新时代") && courseName.contains("特色"))) {
if (dayOfWeek != 2 || timeSlot != 1) {
System.out.println("修正:习近平新时代课程位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期2, 时间段1)");
course.setDayOfWeek(2);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
}
}
// 3. 党史 - 应该在周二7-8节时间段3
if (courseName.contains("党史")) {
if (dayOfWeek != 2 || timeSlot != 3) {
System.out.println("修正:党史课程位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期2, 时间段3)");
course.setDayOfWeek(2);
course.setTimeSlot(3);
course.setStartPeriod(7);
course.setEndPeriod(8);
}
}
// 4. 编译原理课程设计 - 9-12周应该在周四3-4节时间段1
if (courseName.contains("编译原理") && courseName.contains("课程设计")) {
if ((startWeek >= 9 && endWeek <= 12) || (startWeek == 9 && endWeek == 12)) {
if (dayOfWeek != 4 || timeSlot != 1) {
System.out.println("修正编译原理课程设计9-12周位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期4, 时间段1)");
course.setDayOfWeek(4);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(9);
course.setEndWeek(12);
}
}
}
}
@JavascriptInterface
public void onError(String message) {
runOnUiThread(() -> {
if (message.startsWith("AUTO:")) {
Toast.makeText(EducationImportActivity.this, message.substring(5), Toast.LENGTH_SHORT).show();
} else if (message.startsWith("TIP:")) {
Toast.makeText(EducationImportActivity.this, message.substring(4), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(EducationImportActivity.this, "采集失败:" + message, Toast.LENGTH_LONG).show();
}
});
}
}
}

@ -0,0 +1,66 @@
package com.example.myapplication;
import android.app.Activity;
import android.webkit.JavascriptInterface;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* JS Bridge/ D8
*/
public class EducationJsBridge {
private final WeakReference<EducationImportActivity> activityRef;
private final DataManager dataManager;
public EducationJsBridge(EducationImportActivity activity, DataManager dataManager) {
this.activityRef = new WeakReference<>(activity);
this.dataManager = dataManager;
}
@JavascriptInterface
public void onHtml(String html) {
final EducationImportActivity activity = activityRef.get();
if (activity == null) return;
activity.runOnUiThread(() -> {
try {
List<Course> parsed = EducationParser.parse(html);
if (parsed == null || parsed.isEmpty()) {
Toast.makeText(activity, "未识别到课程,请确认已在课表页面", Toast.LENGTH_LONG).show();
return;
}
for (Course c : parsed) {
dataManager.addCourse(c);
}
Toast.makeText(activity, "已导入 " + parsed.size() + " 门课程,请返回课表查看", Toast.LENGTH_LONG).show();
// 通知课表刷新
android.content.Intent refreshIntent = new android.content.Intent("com.example.myapplication.TIMETABLE_REFRESH");
activity.sendBroadcast(refreshIntent);
activity.setResult(Activity.RESULT_OK);
activity.finish();
} catch (Exception e) {
Toast.makeText(activity, "解析失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
@JavascriptInterface
public void onError(String message) {
final EducationImportActivity activity = activityRef.get();
if (activity == null) return;
activity.runOnUiThread(() -> {
if (message != null && message.startsWith("AUTO:")) {
Toast.makeText(activity, message.substring(5), Toast.LENGTH_SHORT).show();
} else if (message != null && message.startsWith("TIP:")) {
Toast.makeText(activity, message.substring(4), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(activity, "采集失败:" + message, Toast.LENGTH_LONG).show();
}
});
}
}

@ -0,0 +1,98 @@
package com.example.myapplication;
import java.io.Serializable;
public class Grade implements Serializable {
private String id;
private String subject; // 科目名称
private String courseId; // 关联的课程ID
private String courseCode; // 课程编号
private double score; // 分数
private double credit; // 学分
private String semester; // 学期
private String year; // 学年
private String examType; // 考试类型(期中、期末、平时等)
private String status; // 课程性质(必修、选修等)
private long recordTime; // 记录时间
public Grade() {
this.id = String.valueOf(System.currentTimeMillis());
this.recordTime = System.currentTimeMillis();
}
public Grade(String subject, double score, double credit, String semester) {
this();
this.subject = subject;
this.score = score;
this.credit = credit;
this.semester = semester;
this.examType = "期末考试";
}
public Grade(String subject, double score, double credit, String semester, String examType) {
this(subject, score, credit, semester);
this.examType = examType;
}
// 计算绩点按照4.0制)
public double getGradePoint() {
if (score >= 90) return 4.0;
else if (score >= 85) return 3.7;
else if (score >= 82) return 3.3;
else if (score >= 78) return 3.0;
else if (score >= 75) return 2.7;
else if (score >= 72) return 2.3;
else if (score >= 68) return 2.0;
else if (score >= 64) return 1.5;
else if (score >= 60) return 1.0;
else return 0.0;
}
// 获取等级
public String getGrade() {
if (score >= 90) return "A";
else if (score >= 80) return "B";
else if (score >= 70) return "C";
else if (score >= 60) return "D";
else return "F";
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public String getCourseId() { return courseId; }
public void setCourseId(String courseId) { this.courseId = courseId; }
public double getScore() { return score; }
public void setScore(double score) { this.score = score; }
public double getCredit() { return credit; }
public void setCredit(double credit) { this.credit = credit; }
public String getSemester() { return semester; }
public void setSemester(String semester) { this.semester = semester; }
public String getExamType() { return examType; }
public void setExamType(String examType) { this.examType = examType; }
public long getRecordTime() { return recordTime; }
public void setRecordTime(long recordTime) { this.recordTime = recordTime; }
public String getCourseCode() { return courseCode; }
public void setCourseCode(String courseCode) { this.courseCode = courseCode; }
public String getYear() { return year; }
public void setYear(String year) { this.year = year; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
@Override
public String toString() {
return subject + ": " + score + "分 (" + getGrade() + ")";
}
}

@ -0,0 +1,158 @@
package com.example.myapplication;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.EditText;
import android.text.Editable;
import android.text.TextWatcher;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class GradeAdapter extends RecyclerView.Adapter<GradeAdapter.GradeViewHolder> {
private List<WebViewActivity.GradeItem> gradeItems;
public interface OnItemUpdatedListener { void onScoreUpdated(); }
private OnItemUpdatedListener onItemUpdatedListener;
public GradeAdapter() {
this.gradeItems = new ArrayList<>();
}
public void updateData(List<WebViewActivity.GradeItem> newItems) {
this.gradeItems.clear();
this.gradeItems.addAll(newItems);
notifyDataSetChanged();
}
public void setOnItemUpdatedListener(OnItemUpdatedListener listener) {
this.onItemUpdatedListener = listener;
}
public List<WebViewActivity.GradeItem> getItems() { return gradeItems; }
@NonNull
@Override
public GradeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_grade, parent, false);
return new GradeViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull GradeViewHolder holder, int position) {
WebViewActivity.GradeItem item = gradeItems.get(position);
// 课程名称作为主标题
String courseName = item.courseName != null && !item.courseName.isEmpty() ? item.courseName : "未知课程";
holder.tvCourseName.setText(courseName);
// 课程编号
String courseCode = item.courseCode != null && !item.courseCode.isEmpty() ? item.courseCode : "未知";
holder.tvCourseCode.setText("编号: " + courseCode);
// 学分信息
holder.tvCredit.setText("学分: " + (item.credit != null ? item.credit : "未知"));
// 成绩信息/输入切换
boolean hasScore = item.score != null && !item.score.isEmpty();
if (hasScore) {
holder.tvScore.setVisibility(View.VISIBLE);
holder.etScoreInput.setVisibility(View.GONE);
holder.tvScore.setText("成绩: " + item.score);
} else {
holder.tvScore.setVisibility(View.GONE);
holder.etScoreInput.setVisibility(View.VISIBLE);
// 避免旧监听残留
Object oldWatcher = holder.etScoreInput.getTag();
if (oldWatcher instanceof TextWatcher) {
holder.etScoreInput.removeTextChangedListener((TextWatcher) oldWatcher);
}
holder.etScoreInput.setText(item.score != null ? item.score : "");
holder.etScoreInput.setHint("请输入成绩");
// 监听输入并计算绩点
TextWatcher watcher = 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) {
String text = s.toString().trim();
item.score = text;
String gpa = computeGpaFromScore(text);
item.gpa = gpa;
holder.tvGpa.setText("绩点: " + (gpa != null && !gpa.isEmpty() ? gpa : "0"));
if (onItemUpdatedListener != null) onItemUpdatedListener.onScoreUpdated();
}
};
holder.etScoreInput.addTextChangedListener(watcher);
holder.etScoreInput.setTag(watcher);
}
// 绩点信息
holder.tvGpa.setText("绩点: " + (item.gpa != null && !item.gpa.isEmpty() ? item.gpa : "未知"));
// 学年/建议修读学年
holder.tvYear.setText((hasScore ? "学年: " : "建议修读学年: ") + (item.year != null && !item.year.isEmpty() ? item.year : "未知"));
// 课程性质信息
holder.tvStatus.setText("课程性质: " + (item.status != null ? item.status : "未知"));
// 学期/建议修读学期
holder.tvTerm.setText((hasScore ? "学期: " : "建议修读学期: ") + (item.term != null && !item.term.isEmpty() ? item.term : "未知"));
}
private String computeGpaFromScore(String scoreText) {
if (scoreText == null || scoreText.isEmpty()) return "";
try {
int score = Integer.parseInt(scoreText);
if (score >= 90) return "4.0";
if (score >= 85) return "3.7";
if (score >= 82) return "3.3";
if (score >= 78) return "3.0";
if (score >= 75) return "2.7";
if (score >= 72) return "2.3";
if (score >= 68) return "2.0";
if (score >= 66) return "1.7";
if (score >= 64) return "1.5";
if (score >= 60) return "1.0";
return "0";
} catch (NumberFormatException e) {
return "";
}
}
@Override
public int getItemCount() {
return gradeItems.size();
}
static class GradeViewHolder extends RecyclerView.ViewHolder {
TextView tvCourseName;
TextView tvCourseCode;
TextView tvCredit;
TextView tvScore;
EditText etScoreInput;
TextView tvGpa;
TextView tvTerm;
TextView tvYear;
TextView tvStatus;
public GradeViewHolder(@NonNull View itemView) {
super(itemView);
tvCourseName = itemView.findViewById(R.id.tv_course_name);
tvCourseCode = itemView.findViewById(R.id.tv_course_code);
tvCredit = itemView.findViewById(R.id.tv_credit);
tvScore = itemView.findViewById(R.id.tv_score);
etScoreInput = itemView.findViewById(R.id.et_score_input);
tvGpa = itemView.findViewById(R.id.tv_gpa);
tvTerm = itemView.findViewById(R.id.tv_term);
tvYear = itemView.findViewById(R.id.tv_year);
tvStatus = itemView.findViewById(R.id.tv_status);
}
}
}

@ -0,0 +1,193 @@
package com.example.myapplication;
import android.util.Log;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;
public class GradeDataParser {
private static final String TAG = "GradeDataParser";
/**
* HTML
*/
public static List<WebViewActivity.GradeItem> parseGradeData(String htmlContent) {
List<WebViewActivity.GradeItem> gradeItems = new ArrayList<>();
try {
Document doc = Jsoup.parse(htmlContent);
// 查找所有表格
Elements tables = doc.select("table.table.text-center, table.gridtable, table[border='1'], table[class*='table']");
for (Element table : tables) {
List<WebViewActivity.GradeItem> tableItems = parseTable(table);
gradeItems.addAll(tableItems);
}
Log.d(TAG, "Parsed " + gradeItems.size() + " grade items from HTML");
} catch (Exception e) {
Log.e(TAG, "Error parsing HTML: " + e.getMessage());
}
return gradeItems;
}
/**
*
*/
private static List<WebViewActivity.GradeItem> parseTable(Element table) {
List<WebViewActivity.GradeItem> items = new ArrayList<>();
try {
// 获取表头
Elements headerElements = table.select("thead th");
List<String> headers = new ArrayList<>();
for (Element header : headerElements) {
headers.add(header.text().trim());
}
// 如果没有thead尝试从第一行获取表头
if (headers.isEmpty()) {
Element firstRow = table.select("tr").first();
if (firstRow != null) {
Elements cells = firstRow.select("th, td");
for (Element cell : cells) {
headers.add(cell.text().trim());
}
}
}
// 获取数据行
Elements rows = table.select("tbody tr");
if (rows.isEmpty()) {
rows = table.select("tr");
// 如果有表头,跳过第一行
if (!rows.isEmpty() && !headers.isEmpty()) {
Elements dataRows = new Elements();
for (int i = 1; i < rows.size(); i++) {
dataRows.add(rows.get(i));
}
rows = dataRows;
}
}
for (Element row : rows) {
Elements cells = row.select("td");
if (cells.isEmpty()) continue;
WebViewActivity.GradeItem item = new WebViewActivity.GradeItem();
// 根据列索引和表头映射数据
for (int i = 0; i < cells.size(); i++) {
String cellText = cells.get(i).text().trim();
String header = (i < headers.size()) ? headers.get(i) : "col_" + i;
// 根据表头名称映射到对应字段
if (header.contains("学时") || header.contains("课程名") || header.contains("课程名称")) {
item.courseName = cellText;
} else if (header.contains("课程号") || header.contains("课程编号") || header.contains("课号")) {
item.courseCode = cellText;
} else if (header.contains("学分")) {
item.credit = cellText;
} else if (header.contains("成绩")) {
item.score = cellText;
} else if (header.contains("绩点")) {
item.gpa = cellText;
} else if (header.contains("学期")) {
item.term = cellText;
}
}
// 处理成绩和绩点可能颠倒的情况
if (item.score != null && item.gpa != null) {
if (isNumeric(item.score) && isNumeric(item.gpa)) {
try {
float scoreValue = Float.parseFloat(item.score);
float gpaValue = Float.parseFloat(item.gpa);
// 如果分数大于10且绩点小于等于5说明可能颠倒了
if (scoreValue > 10 && gpaValue <= 5) {
String temp = item.score;
item.score = item.gpa;
item.gpa = temp;
}
} catch (NumberFormatException e) {
// 忽略解析错误
}
}
}
// 只添加有效的记录
if (item.courseName != null && !item.courseName.isEmpty() ||
item.courseCode != null && !item.courseCode.isEmpty()) {
items.add(item);
}
}
} catch (Exception e) {
Log.e(TAG, "Error parsing table: " + e.getMessage());
}
return items;
}
/**
*
*/
private static boolean isNumeric(String str) {
if (str == null || str.isEmpty()) return false;
try {
Float.parseFloat(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
/**
* JavaScriptJSON
*/
public static List<WebViewActivity.GradeItem> parseJsonData(String jsonData) {
List<WebViewActivity.GradeItem> items = new ArrayList<>();
try {
Log.d(TAG, "parseJsonData called with: " + jsonData.substring(0, Math.min(200, jsonData.length())) + "...");
// 使用Android内置的JSON解析
org.json.JSONArray jsonArray = new org.json.JSONArray(jsonData);
Log.d(TAG, "JSON array length: " + jsonArray.length());
for (int i = 0; i < jsonArray.length(); i++) {
org.json.JSONObject jsonObject = jsonArray.getJSONObject(i);
WebViewActivity.GradeItem item = new WebViewActivity.GradeItem();
item.courseName = jsonObject.optString("courseName", "");
item.courseCode = jsonObject.optString("courseCode", "");
item.credit = jsonObject.optString("credit", "");
item.score = jsonObject.optString("score", "");
item.gpa = jsonObject.optString("gpa", "");
item.term = jsonObject.optString("term", "");
item.year = jsonObject.optString("year", "");
item.status = jsonObject.optString("status", "");
items.add(item);
Log.d(TAG, "Parsed item " + i + ": " + item.courseName);
}
Log.d(TAG, "Successfully parsed " + items.size() + " items from JSON");
} catch (Exception e) {
Log.e(TAG, "Error parsing JSON: " + e.getMessage());
e.printStackTrace();
}
return items;
}
}

@ -0,0 +1,686 @@
package com.example.myapplication;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.TreeMap;
public class GradesFragment extends Fragment {
private static final String TAG = "GradesFragment";
private static final int REQUEST_CODE_WEBVIEW = 1001;
private static final String PREFS_NAME = "GradesPrefs";
private static final String KEY_GRADE_ITEMS = "gradeItems";
private static final String KEY_STUDENT_NAME = "studentName";
private static final String KEY_OVERALL_GPA = "overallGPA";
private static final String KEY_CURRENT_TERM = "currentTerm";
private static final String KEY_TERM_SUMMARY = "termSummary";
private static final String KEY_TERM_WARNINGS = "termWarnings";
// 分组模式枚举
private enum GroupMode {
BY_STATUS, // 按修读状态(已修完/未修完)
BY_TERM // 按建议修读学期
}
private GroupMode currentGroupMode = GroupMode.BY_STATUS; // 默认按状态分组
private List<WebViewActivity.GradeItem> allGradeItems = new ArrayList<>(); // 存储所有课程数据
private Button btnLoginExport;
private LinearLayout layoutStudentInfo;
private ProgressBar progressBar;
private TextView tvEmptyState;
private TextView tvStudentName;
private TextView tvOverallGPA;
private TextView tvCurrentTerm;
private TextView tvTermSummary;
private TextView tvTermWarnings;
// 分组按钮
private LinearLayout layoutGroupButtons;
private Button btnGroupByStatus;
private Button btnGroupByTerm;
// 按修读状态分组的容器
private LinearLayout layoutStatusGroups;
private androidx.cardview.widget.CardView cardCompleted;
private androidx.cardview.widget.CardView cardIncomplete;
private RecyclerView recyclerViewCompleted;
private RecyclerView recyclerViewIncomplete;
private TextView tvCompletedCount;
private TextView tvIncompleteCount;
private TextView iconCompleted;
private TextView iconIncomplete;
private LinearLayout headerCompleted;
private LinearLayout headerIncomplete;
private LinearLayout contentCompleted;
private LinearLayout contentIncomplete;
private GradeAdapter adapterCompleted;
private GradeAdapter adapterIncomplete;
private boolean isCompletedExpanded = true;
private boolean isIncompleteExpanded = true;
// 按学期分组的容器
private ScrollView scrollViewTermGroups;
private LinearLayout containerTermGroups;
private java.util.Map<String, Boolean> termExpandedState = new java.util.HashMap<>();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_grades, container, false);
initViews(view);
setupRecyclerView();
setupClickListeners();
// 尝试恢复保存的数据
restoreSavedData();
return view;
}
private void initViews(View view) {
btnLoginExport = view.findViewById(R.id.btn_login_export);
layoutStudentInfo = view.findViewById(R.id.layout_student_info);
progressBar = view.findViewById(R.id.progress_bar);
tvEmptyState = view.findViewById(R.id.tv_empty_state);
tvStudentName = view.findViewById(R.id.tv_student_name);
tvOverallGPA = view.findViewById(R.id.tv_overall_gpa);
tvCurrentTerm = view.findViewById(R.id.tv_current_term);
tvTermSummary = view.findViewById(R.id.tv_term_summary);
tvTermWarnings = view.findViewById(R.id.tv_term_warnings);
// 分组按钮
layoutGroupButtons = view.findViewById(R.id.layout_group_buttons);
btnGroupByStatus = view.findViewById(R.id.btn_group_by_status);
btnGroupByTerm = view.findViewById(R.id.btn_group_by_term);
// 按修读状态分组的容器
layoutStatusGroups = view.findViewById(R.id.layout_status_groups);
cardCompleted = view.findViewById(R.id.card_completed);
cardIncomplete = view.findViewById(R.id.card_incomplete);
recyclerViewCompleted = view.findViewById(R.id.recycler_view_completed);
recyclerViewIncomplete = view.findViewById(R.id.recycler_view_incomplete);
tvCompletedCount = view.findViewById(R.id.tv_completed_count);
tvIncompleteCount = view.findViewById(R.id.tv_incomplete_count);
iconCompleted = view.findViewById(R.id.icon_completed);
iconIncomplete = view.findViewById(R.id.icon_incomplete);
headerCompleted = view.findViewById(R.id.header_completed);
headerIncomplete = view.findViewById(R.id.header_incomplete);
contentCompleted = view.findViewById(R.id.content_completed);
contentIncomplete = view.findViewById(R.id.content_incomplete);
// 按学期分组的容器
scrollViewTermGroups = view.findViewById(R.id.scroll_view_term_groups);
containerTermGroups = view.findViewById(R.id.container_term_groups);
}
private void setupRecyclerView() {
// 已修完课程适配器
adapterCompleted = new GradeAdapter();
recyclerViewCompleted.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerViewCompleted.setAdapter(adapterCompleted);
adapterCompleted.setOnItemUpdatedListener(this::updateOverallGPA);
// 未修完课程适配器
adapterIncomplete = new GradeAdapter();
recyclerViewIncomplete.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerViewIncomplete.setAdapter(adapterIncomplete);
adapterIncomplete.setOnItemUpdatedListener(this::updateOverallGPA);
// 设置折叠/展开点击事件
headerCompleted.setOnClickListener(v -> toggleCompletedCard());
headerIncomplete.setOnClickListener(v -> toggleIncompleteCard());
}
private void toggleCompletedCard() {
isCompletedExpanded = !isCompletedExpanded;
contentCompleted.setVisibility(isCompletedExpanded ? View.VISIBLE : View.GONE);
iconCompleted.setText(isCompletedExpanded ? "▼" : "▶");
}
private void toggleIncompleteCard() {
isIncompleteExpanded = !isIncompleteExpanded;
contentIncomplete.setVisibility(isIncompleteExpanded ? View.VISIBLE : View.GONE);
iconIncomplete.setText(isIncompleteExpanded ? "▼" : "▶");
}
private void updateOverallGPA() {
String newOverall = computeOverallGpa(allGradeItems);
tvOverallGPA.setText("总平均绩点:" + newOverall);
}
private void setupClickListeners() {
btnLoginExport.setOnClickListener(v -> {
// 启动WebView Activity
Intent intent = new Intent(getActivity(), WebViewActivity.class);
startActivityForResult(intent, REQUEST_CODE_WEBVIEW);
});
// 按修读状态分组
btnGroupByStatus.setOnClickListener(v -> switchGroupMode(GroupMode.BY_STATUS));
// 按建议修读学期分组
btnGroupByTerm.setOnClickListener(v -> switchGroupMode(GroupMode.BY_TERM));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(TAG, "onActivityResult called - requestCode: " + requestCode + ", resultCode: " + resultCode);
if (requestCode == REQUEST_CODE_WEBVIEW && resultCode == android.app.Activity.RESULT_OK) {
Log.d(TAG, "WebView activity returned RESULT_OK");
if (data != null && data.hasExtra("gradeItems")) {
Log.d(TAG, "Found gradeItems extra in intent");
WebViewActivity.GradeItem[] items = (WebViewActivity.GradeItem[]) data.getSerializableExtra("gradeItems");
// 获取学生信息(如果有的话)
String studentName = data.getStringExtra("studentName");
String overallGPA = data.getStringExtra("overallGPA");
String currentYear = data.getStringExtra("currentTermYear");
String currentTerm = data.getStringExtra("currentTermNumber");
String currentTermText = "";
if (currentYear != null && !currentYear.isEmpty() && currentTerm != null && !currentTerm.isEmpty()) {
currentTermText = "当前为" + currentYear + "学年 第" + currentTerm + "学期";
}
ArrayList<String> termWarnings = data.getStringArrayListExtra("termWarnings");
String electiveSummary = data.getStringExtra("electiveSummary");
String electiveInProgressCount = data.getStringExtra("electiveInProgressCount");
String electiveInProgressCredits = data.getStringExtra("electiveInProgressCredits");
if (items != null && items.length > 0) {
Log.d(TAG, "Successfully received " + items.length + " grade items");
// 保存所有数据
allGradeItems.clear();
for (WebViewActivity.GradeItem item : items) {
allGradeItems.add(item);
}
// 根据当前分组模式显示数据
updateGradeData();
showDataView(studentName, overallGPA, currentTermText, electiveSummary, electiveInProgressCount, electiveInProgressCredits, termWarnings);
// 保存数据到SharedPreferences
saveData(studentName, overallGPA, currentTermText, electiveSummary, termWarnings);
Toast.makeText(getContext(), "成功获取 " + items.length + " 条学业数据", Toast.LENGTH_SHORT).show();
} else {
Log.d(TAG, "No grade items found in data");
Toast.makeText(getContext(), "未获取到学业数据", Toast.LENGTH_SHORT).show();
}
} else {
Log.d(TAG, "No gradeItems extra found in intent");
}
} else if (requestCode == REQUEST_CODE_WEBVIEW && resultCode == android.app.Activity.RESULT_CANCELED) {
Log.d(TAG, "WebView activity returned RESULT_CANCELED");
Toast.makeText(getContext(), "数据解析失败", Toast.LENGTH_SHORT).show();
} else {
Log.d(TAG, "Unexpected result - requestCode: " + requestCode + ", resultCode: " + resultCode);
}
}
private void showDataView(String studentName, String overallGPA, String currentTermText, String electiveSummary, String electiveInProgressCount, String electiveInProgressCredits, ArrayList<String> termWarnings) {
// 隐藏按钮,显示学生信息和分组按钮
btnLoginExport.setVisibility(View.GONE);
layoutStudentInfo.setVisibility(View.VISIBLE);
layoutGroupButtons.setVisibility(View.VISIBLE);
// 动态设置学生姓名和GPA如果有的话
if (studentName != null && !studentName.isEmpty()) {
tvStudentName.setText("学生姓名:" + studentName);
}
String initialOverall = (overallGPA != null && !overallGPA.isEmpty()) ? overallGPA : computeOverallGpa(allGradeItems);
tvOverallGPA.setText("总平均绩点:" + initialOverall);
if (currentTermText != null && !currentTermText.isEmpty()) {
tvCurrentTerm.setText(currentTermText);
tvCurrentTerm.setVisibility(View.VISIBLE);
} else {
tvCurrentTerm.setText("");
tvCurrentTerm.setVisibility(View.GONE);
}
if (electiveSummary != null && !electiveSummary.isEmpty()) {
String formattedSummary = formatElectiveSummary(electiveSummary);
if (electiveInProgressCount != null && !electiveInProgressCount.isEmpty()) {
StringBuilder sb = new StringBuilder(formattedSummary);
sb.append("\n在修").append(electiveInProgressCount).append("门");
if (electiveInProgressCredits != null && !electiveInProgressCredits.isEmpty()) {
sb.append(",共计在修学分").append(electiveInProgressCredits);
}
formattedSummary = sb.toString();
}
tvTermSummary.setText(formattedSummary);
tvTermSummary.setVisibility(View.VISIBLE);
} else {
tvTermSummary.setText("");
tvTermSummary.setVisibility(View.GONE);
}
if (termWarnings != null && !termWarnings.isEmpty()) {
SpannableStringBuilder builder = new SpannableStringBuilder();
String safeMessage = "本学期的必修课已检查确定无漏选";
for (int i = 0; i < termWarnings.size(); i++) {
String warning = termWarnings.get(i);
int start = builder.length();
builder.append(warning);
int end = builder.length();
int color = warning.equals(safeMessage)
? ContextCompat.getColor(requireContext(), android.R.color.holo_green_dark)
: ContextCompat.getColor(requireContext(), android.R.color.holo_red_dark);
builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (i < termWarnings.size() - 1) {
builder.append("\n");
}
}
tvTermWarnings.setText(builder);
tvTermWarnings.setVisibility(View.VISIBLE);
} else {
tvTermWarnings.setText("");
tvTermWarnings.setVisibility(View.GONE);
}
// 显示数据列表
updateCardVisibility();
tvEmptyState.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
}
/**
*
*/
private void updateGradeData() {
if (currentGroupMode == GroupMode.BY_STATUS) {
displayByStatus();
} else {
displayByTermGroups();
}
}
/**
* /
*/
private void displayByStatus() {
List<WebViewActivity.GradeItem> completed = new ArrayList<>();
List<WebViewActivity.GradeItem> incomplete = new ArrayList<>();
for (WebViewActivity.GradeItem item : allGradeItems) {
if (item.score != null && !item.score.trim().isEmpty()) {
completed.add(item);
} else {
incomplete.add(item);
}
}
adapterCompleted.updateData(completed);
adapterIncomplete.updateData(incomplete);
tvCompletedCount.setText("(" + completed.size() + ")");
tvIncompleteCount.setText("(" + incomplete.size() + ")");
// 显示状态容器,隐藏学期容器
layoutStatusGroups.setVisibility(View.VISIBLE);
scrollViewTermGroups.setVisibility(View.GONE);
updateCardVisibility();
}
/**
*
*/
private void updateCardVisibility() {
boolean hasCompleted = adapterCompleted.getItemCount() > 0;
boolean hasIncomplete = adapterIncomplete.getItemCount() > 0;
cardCompleted.setVisibility(hasCompleted ? View.VISIBLE : View.GONE);
cardIncomplete.setVisibility(hasIncomplete ? View.VISIBLE : View.GONE);
if (hasCompleted) {
contentCompleted.setVisibility(isCompletedExpanded ? View.VISIBLE : View.GONE);
iconCompleted.setText(isCompletedExpanded ? "▼" : "▶");
}
if (hasIncomplete) {
contentIncomplete.setVisibility(isIncompleteExpanded ? View.VISIBLE : View.GONE);
iconIncomplete.setText(isIncompleteExpanded ? "▼" : "▶");
}
}
/**
*
*/
private void switchGroupMode(GroupMode newMode) {
if (currentGroupMode == newMode) return;
currentGroupMode = newMode;
// 更新按钮样式
if (currentGroupMode == GroupMode.BY_STATUS) {
btnGroupByStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(android.R.color.black)));
btnGroupByTerm.setBackgroundTintList(android.content.res.ColorStateList.valueOf(0xFF424242));
} else {
btnGroupByStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(0xFF424242));
btnGroupByTerm.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(android.R.color.black)));
}
// 重新显示数据
if (!allGradeItems.isEmpty()) {
updateGradeData();
}
}
/**
*
*/
private void displayByTermGroups() {
// 隐藏状态容器,显示学期容器
layoutStatusGroups.setVisibility(View.GONE);
scrollViewTermGroups.setVisibility(View.VISIBLE);
containerTermGroups.removeAllViews();
// 按学期分组使用TreeMap自动排序
java.util.Map<String, List<WebViewActivity.GradeItem>> termGroups = new java.util.TreeMap<>();
for (WebViewActivity.GradeItem item : allGradeItems) {
String termKey = item.year + " 第" + item.term + "学期";
if (item.year == null || item.year.isEmpty()) {
termKey = "未知学期";
}
if (!termGroups.containsKey(termKey)) {
termGroups.put(termKey, new ArrayList<>());
}
termGroups.get(termKey).add(item);
}
// 为每个学期创建卡片
LayoutInflater inflater = LayoutInflater.from(getContext());
for (java.util.Map.Entry<String, List<WebViewActivity.GradeItem>> entry : termGroups.entrySet()) {
String termName = entry.getKey();
List<WebViewActivity.GradeItem> termItems = entry.getValue();
createTermCard(termName, termItems);
}
}
/**
*
*/
private void createTermCard(String termName, List<WebViewActivity.GradeItem> termItems) {
// 创建卡片
androidx.cardview.widget.CardView termCard = new androidx.cardview.widget.CardView(getContext());
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
cardParams.setMargins(0, 0, 0, 24);
termCard.setLayoutParams(cardParams);
termCard.setRadius(16);
termCard.setCardElevation(8);
// 卡片内容容器
LinearLayout cardContent = new LinearLayout(getContext());
cardContent.setOrientation(LinearLayout.VERTICAL);
cardContent.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
// 标题区域(可点击折叠/展开)
LinearLayout header = new LinearLayout(getContext());
header.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams headerParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
headerParams.setMargins(32, 32, 32, 32);
header.setLayoutParams(headerParams);
header.setClickable(true);
header.setFocusable(true);
header.setBackgroundResource(android.R.drawable.list_selector_background);
// 学期名称
TextView termTitle = new TextView(getContext());
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1.0f
);
termTitle.setLayoutParams(titleParams);
termTitle.setText(termName);
termTitle.setTextSize(18);
termTitle.setTextColor(getResources().getColor(android.R.color.black));
termTitle.setTypeface(null, android.graphics.Typeface.BOLD);
// 课程数量
TextView countText = new TextView(getContext());
countText.setText("(" + termItems.size() + ")");
countText.setTextSize(16);
countText.setTextColor(getResources().getColor(android.R.color.darker_gray));
LinearLayout.LayoutParams countParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
countParams.setMargins(16, 0, 16, 0);
countText.setLayoutParams(countParams);
// 折叠图标
TextView icon = new TextView(getContext());
icon.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
boolean isExpanded = termExpandedState.getOrDefault(termName, true);
icon.setText(isExpanded ? "▼" : "▶");
icon.setTextSize(14);
icon.setTextColor(getResources().getColor(android.R.color.darker_gray));
header.addView(termTitle);
header.addView(countText);
header.addView(icon);
// RecyclerView容器
LinearLayout contentContainer = new LinearLayout(getContext());
contentContainer.setOrientation(LinearLayout.VERTICAL);
contentContainer.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
contentContainer.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
// 创建RecyclerView
RecyclerView recyclerView = new RecyclerView(getContext());
LinearLayout.LayoutParams recyclerParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
recyclerParams.setMargins(16, 0, 16, 16);
recyclerView.setLayoutParams(recyclerParams);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
GradeAdapter termAdapter = new GradeAdapter();
termAdapter.updateData(termItems);
termAdapter.setOnItemUpdatedListener(this::updateOverallGPA);
recyclerView.setAdapter(termAdapter);
contentContainer.addView(recyclerView);
// 设置点击事件
header.setOnClickListener(v -> {
boolean expanded = contentContainer.getVisibility() == View.VISIBLE;
contentContainer.setVisibility(expanded ? View.GONE : View.VISIBLE);
icon.setText(expanded ? "▶" : "▼");
termExpandedState.put(termName, !expanded);
});
cardContent.addView(header);
cardContent.addView(contentContainer);
termCard.addView(cardContent);
containerTermGroups.addView(termCard);
}
private String formatElectiveSummary(String summary) {
if (summary == null) return "";
return summary
.replace("\u00A0", " ")
.replace("&nbsp;", " ")
.trim();
}
private String computeOverallGpa(List<WebViewActivity.GradeItem> items) {
if (items == null || items.isEmpty()) return "0";
double totalWeighted = 0.0;
double totalCredits = 0.0;
for (WebViewActivity.GradeItem it : items) {
if (it == null) continue;
// 仅统计成绩非空的课程(包括用户手动输入的)
if (it.score != null && !it.score.trim().isEmpty()) {
double credit = 0.0;
double gpa = 0.0;
try { credit = Double.parseDouble(it.credit != null ? it.credit : "0"); } catch (Exception ignored) {}
try { gpa = Double.parseDouble(it.gpa != null && !it.gpa.isEmpty() ? it.gpa : "0"); } catch (Exception ignored) {}
totalWeighted += credit * gpa;
totalCredits += credit;
}
}
if (totalCredits <= 0.0) return "0";
double overall = totalWeighted / totalCredits;
// 保留两位小数
return String.format(java.util.Locale.getDefault(), "%.2f", overall);
}
private void showEmptyState() {
// 显示按钮,隐藏学生信息和分组按钮
btnLoginExport.setVisibility(View.VISIBLE);
layoutStudentInfo.setVisibility(View.GONE);
layoutGroupButtons.setVisibility(View.GONE);
// 隐藏所有容器
layoutStatusGroups.setVisibility(View.GONE);
scrollViewTermGroups.setVisibility(View.GONE);
cardCompleted.setVisibility(View.GONE);
cardIncomplete.setVisibility(View.GONE);
// 显示空状态
tvEmptyState.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
}
private void showLoading() {
layoutStatusGroups.setVisibility(View.GONE);
scrollViewTermGroups.setVisibility(View.GONE);
tvEmptyState.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
/**
* SharedPreferences
*/
private void saveData(String studentName, String overallGPA, String currentTerm,
String termSummary, ArrayList<String> termWarnings) {
try {
SharedPreferences prefs = getActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
// 使用Gson保存课程列表
Gson gson = new Gson();
String gradeItemsJson = gson.toJson(allGradeItems);
editor.putString(KEY_GRADE_ITEMS, gradeItemsJson);
// 保存学生信息
editor.putString(KEY_STUDENT_NAME, studentName != null ? studentName : "");
editor.putString(KEY_OVERALL_GPA, overallGPA != null ? overallGPA : "");
editor.putString(KEY_CURRENT_TERM, currentTerm != null ? currentTerm : "");
editor.putString(KEY_TERM_SUMMARY, termSummary != null ? termSummary : "");
// 保存警告列表
if (termWarnings != null && !termWarnings.isEmpty()) {
String warningsJson = gson.toJson(termWarnings);
editor.putString(KEY_TERM_WARNINGS, warningsJson);
} else {
editor.putString(KEY_TERM_WARNINGS, "");
}
editor.apply();
Log.d(TAG, "数据已保存: " + allGradeItems.size() + " 门课程");
} catch (Exception e) {
Log.e(TAG, "保存数据失败: " + e.getMessage());
}
}
/**
* SharedPreferences
*/
private void restoreSavedData() {
try {
SharedPreferences prefs = getActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
// 恢复课程列表
String gradeItemsJson = prefs.getString(KEY_GRADE_ITEMS, "");
if (!gradeItemsJson.isEmpty()) {
Gson gson = new Gson();
Type listType = new TypeToken<List<WebViewActivity.GradeItem>>(){}.getType();
List<WebViewActivity.GradeItem> savedItems = gson.fromJson(gradeItemsJson, listType);
if (savedItems != null && !savedItems.isEmpty()) {
allGradeItems.clear();
allGradeItems.addAll(savedItems);
// 恢复学生信息
String studentName = prefs.getString(KEY_STUDENT_NAME, "");
String overallGPA = prefs.getString(KEY_OVERALL_GPA, "");
String currentTerm = prefs.getString(KEY_CURRENT_TERM, "");
String termSummary = prefs.getString(KEY_TERM_SUMMARY, "");
// 恢复警告列表
String warningsJson = prefs.getString(KEY_TERM_WARNINGS, "");
ArrayList<String> termWarnings = new ArrayList<>();
if (!warningsJson.isEmpty()) {
Type warningsType = new TypeToken<ArrayList<String>>(){}.getType();
termWarnings = gson.fromJson(warningsJson, warningsType);
}
// 显示数据
updateGradeData();
showDataView(studentName, overallGPA, currentTerm, termSummary, "", "", termWarnings);
Log.d(TAG, "数据已恢复: " + allGradeItems.size() + " 门课程");
return;
}
}
} catch (Exception e) {
Log.e(TAG, "恢复数据失败: " + e.getMessage());
}
// 如果没有保存的数据,显示空状态
showEmptyState();
}
}

@ -0,0 +1,62 @@
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
// 获取四个功能卡片
CardView cardTimetable = findViewById(R.id.card_timetable);
CardView cardNotes = findViewById(R.id.card_notes);
CardView cardGrades = findViewById(R.id.card_grades);
CardView cardCampus = findViewById(R.id.card_campus);
// 课表按钮点击事件
cardTimetable.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "timetable");
startActivity(intent);
}
});
// 笔记按钮点击事件
cardNotes.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "notes");
startActivity(intent);
}
});
// 成绩按钮点击事件
cardGrades.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "grades");
startActivity(intent);
}
});
// 校园导航按钮点击事件
cardCampus.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "campus");
startActivity(intent);
}
});
}
}

@ -0,0 +1,92 @@
package com.example.myapplication;
import java.io.Serializable;
public class Location implements Serializable {
private String name; // 位置名称
private String description; // 位置描述
private double latitude; // 纬度
private double longitude; // 经度
private String category; // 分类(教学楼、图书馆、食堂等)
private String building; // 建筑名称
private String floor; // 楼层
private String room; // 房间号
public Location() {}
public Location(String name, String description, String category) {
this.name = name;
this.description = description;
this.category = category;
// 模拟坐标(实际应用中应该是真实坐标)
this.latitude = 39.9042 + (Math.random() - 0.5) * 0.01;
this.longitude = 116.4074 + (Math.random() - 0.5) * 0.01;
}
public Location(String name, String description, String category, String building) {
this(name, description, category);
this.building = building;
}
// 计算与另一个位置的距离(简化版本,实际应使用地理计算)
public double calculateDistance(Location other) {
if (other == null) return 0;
double deltaLat = this.latitude - other.latitude;
double deltaLng = this.longitude - other.longitude;
// 简化的距离计算实际应使用Haversine公式
double distance = Math.sqrt(deltaLat * deltaLat + deltaLng * deltaLng) * 111000; // 转换为米
return Math.round(distance * 10) / 10.0; // 保留一位小数
}
// 估算步行时间(分钟)
public int calculateWalkTime(Location other) {
double distance = calculateDistance(other);
// 假设步行速度为5km/h = 83.33m/min
return (int) Math.ceil(distance / 83.33);
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getBuilding() { return building; }
public void setBuilding(String building) { this.building = building; }
public String getFloor() { return floor; }
public void setFloor(String floor) { this.floor = floor; }
public String getRoom() { return room; }
public void setRoom(String room) { this.room = room; }
@Override
public String toString() {
return name + (building != null ? " (" + building + ")" : "");
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Location location = (Location) obj;
return name != null ? name.equals(location.name) : location.name == null;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}

@ -0,0 +1,69 @@
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/**
*
*
*/
public class LocationDetailsActivity extends AppCompatActivity {
private TextView tvLocationName, tvLocationDescription, tvLocationCoords, tvNearbyInfo;
private Button btnNavigateHere, btnAddToFavorites, btnBack;
private AmapLocation location;
private CampusLocationManager locationManager;
private MapHelper mapHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(createLocationDetailsLayout());
// 从Intent获取位置信息
Intent intent = getIntent();
String locationName = intent.getStringExtra("location_name");
initServices();
loadLocationData(locationName);
setupViews();
setupListeners();
}
private int createLocationDetailsLayout() {
// 由于我们没有专门的布局文件,使用程序化布局
// 在实际项目中应该创建专门的XML布局文件
return android.R.layout.activity_list_item;
}
private void initServices() {
locationManager = new CampusLocationManager(this);
mapHelper = new MapHelper(this);
}
private void loadLocationData(String locationName) {
location = locationManager.findLocationByName(locationName);
if (location == null) {
Toast.makeText(this, "位置信息未找到", Toast.LENGTH_SHORT).show();
finish();
}
}
private void setupViews() {
// 初始化视图组件
// 在真实项目中这些应该通过findViewById获取
if (location != null) {
setTitle(location.getName());
}
}
private void setupListeners() {
// 设置按钮监听器
}
}

@ -0,0 +1,61 @@
package com.example.myapplication;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
// Configure window for better OpenGL rendering
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
setContentView(R.layout.activity_main);
// 从Intent获取要显示的页面
String fragmentType = getIntent().getStringExtra("fragment");
if (savedInstanceState == null) {
Fragment fragment = null;
if ("notes".equals(fragmentType)) {
fragment = new NotesFragment();
} else if ("grades".equals(fragmentType)) {
fragment = new GradesFragment();
} else if ("campus".equals(fragmentType)) {
fragment = new CampusNavFragmentSimplified();
} else {
// 默认显示课表页面
fragment = new TimetableFragment();
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit();
}
} catch (Exception e) {
Toast.makeText(this, "应用初始化失败", Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
@Override
public void onBackPressed() {
// 返回主页面
finish();
}
}

@ -0,0 +1,196 @@
package com.example.myapplication;
import android.content.Context;
import android.util.Log;
/**
*
*
*/
public class MapHelper {
private static final String TAG = "MapHelper";
private Context context;
private boolean isMapAvailable = false;
public MapHelper(Context context) {
this.context = context;
initializeMapCapabilities();
}
/**
*
*/
private void initializeMapCapabilities() {
try {
// 检查高德地图SDK是否可用
Class.forName("com.amap.api.maps.AMap");
isMapAvailable = true;
Log.i(TAG, "高德地图SDK可用");
} catch (ClassNotFoundException e) {
isMapAvailable = false;
Log.w(TAG, "高德地图SDK不可用使用简化模式");
}
}
/**
*
*/
public boolean isMapAvailable() {
return isMapAvailable;
}
/**
*
*/
public String getMapStatus() {
if (isMapAvailable) {
return "🗺️ 完整地图功能已启用";
} else {
return "📱 简化导航模式 (地图功能集成中...)";
}
}
/**
*
*/
public String[] getLocationSuggestions(String query) {
if (query == null || query.trim().isEmpty()) {
return new String[]{
"🏫 试试搜索:图书馆",
"🍽️ 试试搜索:食堂",
"🏠 试试搜索:宿舍",
"🔬 试试搜索:实验楼"
};
}
// 基础的搜索建议逻辑
if (query.contains("图书")) {
return new String[]{"主图书馆", "理工图书馆", "电子阅览室"};
} else if (query.contains("食堂")) {
return new String[]{"第一食堂", "第二食堂", "清真食堂", "教工食堂"};
} else if (query.contains("宿舍")) {
return new String[]{"男生1号楼", "女生1号楼", "研究生公寓"};
} else {
return new String[]{"搜索校园位置...", "使用分类按钮浏览"};
}
}
/**
* GPS
*/
public LocationInfo getCurrentLocation() {
return new LocationInfo(
"校门口",
"学校正门入口处",
39.906016,
116.395312,
System.currentTimeMillis()
);
}
/**
*
*/
public static class LocationInfo {
private String name;
private String description;
private double latitude;
private double longitude;
private long timestamp;
public LocationInfo(String name, String description, double latitude, double longitude, long timestamp) {
this.name = name;
this.description = description;
this.latitude = latitude;
this.longitude = longitude;
this.timestamp = timestamp;
}
// Getters
public String getName() { return name; }
public String getDescription() { return description; }
public double getLatitude() { return latitude; }
public double getLongitude() { return longitude; }
public long getTimestamp() { return timestamp; }
public String getFormattedLocation() {
return String.format("📍 %s\n📝 %s\n🌍 %.6f, %.6f",
name, description, latitude, longitude);
}
}
/**
*
*/
public String generateNavigationText(AmapLocation from, AmapLocation to) {
if (from == null || to == null) {
return "⚠️ 位置信息不完整";
}
double distance = from.calculateDistance(to);
int walkTime = from.calculateWalkTime(to);
StringBuilder guide = new StringBuilder();
guide.append("🚶 步行导航指引\n\n");
guide.append(String.format("📍 起点:%s\n", from.getName()));
guide.append(String.format("🎯 终点:%s\n", to.getName()));
guide.append(String.format("📏 距离:约 %.0f 米\n", distance));
guide.append(String.format("⏰ 预计:约 %d 分钟\n\n", walkTime));
// 根据距离生成简单的导航建议
if (distance < 100) {
guide.append("💡 距离很近,步行即可到达");
} else if (distance < 500) {
guide.append("💡 距离适中,建议步行前往");
} else if (distance < 1000) {
guide.append("💡 距离较远,可考虑骑行或步行");
} else {
guide.append("💡 距离较远,建议选择合适的交通方式");
}
return guide.toString();
}
/**
*
*/
public String getLocationTypeIcon(String category) {
switch (category) {
case "图书馆": return "📚";
case "食堂": return "🍽️";
case "宿舍": return "🏠";
case "教学楼": return "🏫";
case "实验楼": return "🔬";
case "体育设施": return "🏃";
case "行政办公": return "🏢";
case "生活服务": return "🛒";
case "休闲娱乐": return "🎭";
default: return "📍";
}
}
/**
*
*/
public String[] getRequiredPermissions() {
return new String[]{
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.INTERNET"
};
}
/**
*
*/
public String getIntegrationProgress() {
return "🚀 地图集成进度:\n" +
"✅ 基础框架 (100%)\n" +
"✅ 位置数据库 (100%)\n" +
"✅ 搜索功能 (100%)\n" +
"🔄 地图显示 (准备中...)\n" +
"⏳ GPS定位 (开发中...)\n" +
"⏳ 实时导航 (计划中...)";
}
}

@ -0,0 +1,95 @@
package com.example.myapplication;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Note implements Serializable {
private String id;
private String title; // 笔记标题
private String content; // 笔记内容
private String courseId; // 关联的课程ID
private String courseName; // 关联的课程名称
private long createTime; // 创建时间
private long modifyTime; // 修改时间
private List<String> imagePaths; // 图片路径列表
public Note() {
this.id = String.valueOf(System.currentTimeMillis());
this.createTime = System.currentTimeMillis();
this.modifyTime = System.currentTimeMillis();
this.imagePaths = new ArrayList<>();
}
public Note(String title, String content) {
this();
this.title = title;
this.content = content;
}
public Note(String title, String content, String courseId, String courseName) {
this(title, content);
this.courseId = courseId;
this.courseName = courseName;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) {
this.title = title;
this.modifyTime = System.currentTimeMillis();
}
public String getContent() { return content; }
public void setContent(String content) {
this.content = content;
this.modifyTime = System.currentTimeMillis();
}
public String getCourseId() { return courseId; }
public void setCourseId(String courseId) {
this.courseId = courseId;
this.modifyTime = System.currentTimeMillis();
}
public String getCourseName() { return courseName; }
public void setCourseName(String courseName) {
this.courseName = courseName;
this.modifyTime = System.currentTimeMillis();
}
public long getCreateTime() { return createTime; }
public void setCreateTime(long createTime) { this.createTime = createTime; }
public long getModifyTime() { return modifyTime; }
public void setModifyTime(long modifyTime) { this.modifyTime = modifyTime; }
public List<String> getImagePaths() { return imagePaths; }
public void setImagePaths(List<String> imagePaths) {
this.imagePaths = imagePaths != null ? imagePaths : new ArrayList<>();
this.modifyTime = System.currentTimeMillis();
}
public void addImagePath(String imagePath) {
if (this.imagePaths == null) {
this.imagePaths = new ArrayList<>();
}
this.imagePaths.add(imagePath);
this.modifyTime = System.currentTimeMillis();
}
public void removeImagePath(String imagePath) {
if (this.imagePaths != null) {
this.imagePaths.remove(imagePath);
this.modifyTime = System.currentTimeMillis();
}
}
@Override
public String toString() {
return title + (courseName != null ? " (" + courseName + ")" : "");
}
}

@ -0,0 +1,451 @@
package com.example.myapplication;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
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 androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class NoteEditorActivity extends AppCompatActivity {
public static final String EXTRA_NOTE_ID = "noteId";
private static final String TAG = "NoteEditorActivity";
private static final int REQUEST_CODE_PICK_IMAGE = 1001;
private static final int REQUEST_CODE_TAKE_PHOTO = 1002;
private static final int REQUEST_CODE_PERMISSION = 1003;
private EditText etTitle;
private WebView webViewContent;
private Spinner spinnerCourse;
private Button btnSave;
private Button btnCancel;
private Button btnInsertImage;
private DataManager dataManager;
private Note editingNote;
private List<Course> courses;
private File currentPhotoFile;
private ValueCallback<Uri> imageCallback;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_editor);
dataManager = new DataManager(this);
initViews();
setupWebView();
loadCourses();
loadNoteFromIntent();
setupListeners();
}
private void initViews() {
etTitle = findViewById(R.id.et_title);
webViewContent = findViewById(R.id.webview_content);
spinnerCourse = findViewById(R.id.spinner_course);
btnSave = findViewById(R.id.btn_save);
btnCancel = findViewById(R.id.btn_cancel);
btnInsertImage = findViewById(R.id.btn_insert_image);
TextView btnClose = findViewById(R.id.btn_close);
btnClose.setOnClickListener(v -> finish());
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
WebSettings settings = webViewContent.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
webViewContent.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 页面加载完成后,加载笔记内容
loadContentToWebView();
}
});
webViewContent.setWebChromeClient(new WebChromeClient());
// 添加JavaScript接口
webViewContent.addJavascriptInterface(new WebAppInterface(), "Android");
// 加载富文本编辑器HTML
loadEditorHtml();
}
private void loadEditorHtml() {
String html = "<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
"<style>\n" +
"body { margin: 16px; font-family: sans-serif; }\n" +
"#editor { min-height: 300px; border: 1px solid #ddd; padding: 12px; outline: none; background: white; }\n" +
"img { max-width: 100%; height: auto; margin: 8px 0; display: block; }\n" +
"</style>\n" +
"</head>\n" +
"<body>\n" +
"<div id=\"editor\" contenteditable=\"true\"></div>\n" +
"<script>\n" +
"function insertImage(imagePath) {\n" +
" var editor = document.getElementById('editor');\n" +
" var img = document.createElement('img');\n" +
" img.src = 'file://' + imagePath;\n" +
" img.style.maxWidth = '100%';\n" +
" img.style.height = 'auto';\n" +
" img.style.margin = '8px 0';\n" +
" img.style.display = 'block';\n" +
" editor.appendChild(img);\n" +
" editor.appendChild(document.createElement('br'));\n" +
"}\n" +
"function getContent() {\n" +
" return document.getElementById('editor').innerHTML;\n" +
"}\n" +
"function setContent(html) {\n" +
" document.getElementById('editor').innerHTML = html || '';\n" +
"}\n" +
"function execCommand(command, value) {\n" +
" document.execCommand(command, false, value);\n" +
" document.getElementById('editor').focus();\n" +
"}\n" +
"</script>\n" +
"</body>\n" +
"</html>";
webViewContent.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
}
private void loadContentToWebView() {
if (editingNote != null) {
String content = editingNote.getContent() != null ? editingNote.getContent() : "";
// 如果内容为空但图片路径存在构建HTML
if (content.isEmpty() && editingNote.getImagePaths() != null && !editingNote.getImagePaths().isEmpty()) {
StringBuilder htmlContent = new StringBuilder();
for (String imagePath : editingNote.getImagePaths()) {
htmlContent.append("<img src=\"file://").append(imagePath).append("\" style=\"max-width: 100%; height: auto; margin: 8px 0; display: block;\" /><br>");
}
content = htmlContent.toString();
}
// 创建final副本用于lambda表达式
final String finalContent = content;
// 延迟加载内容确保WebView已准备好
webViewContent.postDelayed(() -> {
if (!finalContent.isEmpty()) {
// 使用JSON转义
String jsonContent = org.json.JSONObject.quote(finalContent);
webViewContent.evaluateJavascript("setContent(" + jsonContent + ");", null);
}
}, 300);
}
}
private void loadCourses() {
courses = dataManager.getCourses();
List<String> courseNames = new ArrayList<>();
courseNames.add("不关联课程");
for (Course c : courses) {
courseNames.add(c.getName());
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, courseNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCourse.setAdapter(adapter);
}
private void loadNoteFromIntent() {
String noteId = getIntent().getStringExtra(EXTRA_NOTE_ID);
if (noteId != null && !noteId.isEmpty()) {
List<Note> all = dataManager.getNotes();
for (Note n : all) {
if (noteId.equals(n.getId())) {
editingNote = n;
break;
}
}
}
if (editingNote == null) {
editingNote = new Note();
}
etTitle.setText(editingNote.getTitle() != null ? editingNote.getTitle() : "");
}
private void setupListeners() {
btnSave.setOnClickListener(v -> doSave());
btnCancel.setOnClickListener(v -> finish());
btnInsertImage.setOnClickListener(v -> showImageSourceDialog());
}
private void showImageSourceDialog() {
String[] options = {"从相册选择", "拍照"};
new AlertDialog.Builder(this)
.setTitle("选择图片")
.setItems(options, (dialog, which) -> {
if (which == 0) {
pickImageFromGallery();
} else if (which == 1) {
takePhoto();
}
})
.show();
}
private void pickImageFromGallery() {
if (checkReadPermission()) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
}
}
private void takePhoto() {
if (checkCameraPermission()) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
try {
currentPhotoFile = createImageFile();
if (currentPhotoFile != null) {
Uri photoURI = FileProvider.getUriForFile(this,
getApplicationContext().getPackageName() + ".fileprovider",
currentPhotoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
}
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "创建图片文件失败", Toast.LENGTH_SHORT).show();
}
}
}
}
private boolean checkReadPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_MEDIA_IMAGES},
REQUEST_CODE_PERMISSION);
return false;
}
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_PERMISSION);
return false;
}
}
return true;
}
private boolean checkCameraPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CODE_PERMISSION);
return false;
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
} else {
Toast.makeText(this, "需要权限才能插入图片", Toast.LENGTH_SHORT).show();
}
}
}
private File createImageFile() throws IOException {
String imageFileName = "NOTE_IMG_" + System.currentTimeMillis();
File storageDir = new File(getFilesDir(), "note_images");
if (!storageDir.exists()) {
storageDir.mkdirs();
}
return File.createTempFile(imageFileName, ".jpg", storageDir);
}
private String saveImageFromUri(Uri uri) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream != null) {
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();
if (bitmap != null) {
File imageFile = createImageFile();
FileOutputStream outputStream = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
outputStream.close();
return imageFile.getAbsolutePath();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_PICK_IMAGE && data != null) {
Uri selectedImage = data.getData();
if (selectedImage != null) {
String imagePath = saveImageFromUri(selectedImage);
if (imagePath != null) {
editingNote.addImagePath(imagePath);
// 插入图片到编辑器
String jsCode = "insertImage(" + org.json.JSONObject.quote(imagePath) + ");";
webViewContent.evaluateJavascript(jsCode, null);
} else {
Toast.makeText(this, "保存图片失败", Toast.LENGTH_SHORT).show();
}
}
} else if (requestCode == REQUEST_CODE_TAKE_PHOTO && currentPhotoFile != null) {
if (currentPhotoFile.exists()) {
editingNote.addImagePath(currentPhotoFile.getAbsolutePath());
// 插入图片到编辑器
String jsCode = "insertImage(" + org.json.JSONObject.quote(currentPhotoFile.getAbsolutePath()) + ");";
webViewContent.evaluateJavascript(jsCode, null);
} else {
Toast.makeText(this, "拍照失败", Toast.LENGTH_SHORT).show();
}
}
}
}
private void doSave() {
String title = etTitle.getText().toString().trim();
// 从WebView获取内容
webViewContent.evaluateJavascript("(function() { return document.getElementById('editor').innerHTML; })();",
new ValueCallback<String>() {
@Override
public void onReceiveValue(String htmlContent) {
// 移除JSON字符串的引号和转义
if (htmlContent != null && htmlContent.length() > 2) {
htmlContent = htmlContent.substring(1, htmlContent.length() - 1);
htmlContent = htmlContent.replace("\\\"", "\"");
htmlContent = htmlContent.replace("\\n", "\n");
htmlContent = htmlContent.replace("\\/", "/");
// 还原 WebView 返回的 Unicode 转义(例如 \u003C -> <
htmlContent = htmlContent
.replace("\\u003C", "<").replace("\\u003c", "<")
.replace("\\u003E", ">").replace("\\u003e", ">")
.replace("\\u0026", "&");
}
// 提取图片路径
List<String> imagePaths = new ArrayList<>();
if (htmlContent != null && htmlContent.contains("<img")) {
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("file://([^\"]+)");
java.util.regex.Matcher matcher = pattern.matcher(htmlContent);
while (matcher.find()) {
String path = matcher.group(1);
if (path != null && !path.isEmpty()) {
imagePaths.add(path);
}
}
}
if (title.isEmpty() && (htmlContent == null || htmlContent.trim().isEmpty()) && imagePaths.isEmpty()) {
runOnUiThread(() -> Toast.makeText(NoteEditorActivity.this,
"请输入标题、内容或插入图片", Toast.LENGTH_SHORT).show());
return;
}
editingNote.setTitle(title);
editingNote.setContent(htmlContent != null ? htmlContent : "");
editingNote.setImagePaths(imagePaths);
int pos = spinnerCourse.getSelectedItemPosition();
if (pos <= 0) {
editingNote.setCourseId("");
editingNote.setCourseName("");
} else {
Course c = courses.get(pos - 1);
editingNote.setCourseId(c.getId());
editingNote.setCourseName(c.getName());
}
// 检查是新建还是更新
List<Note> allNotes = dataManager.getNotes();
boolean exists = false;
for (Note n : allNotes) {
if (n.getId().equals(editingNote.getId())) {
exists = true;
break;
}
}
if (exists) {
dataManager.updateNote(editingNote);
} else {
dataManager.addNote(editingNote);
}
runOnUiThread(() -> {
Toast.makeText(NoteEditorActivity.this, "笔记已保存", Toast.LENGTH_SHORT).show();
finish();
});
}
});
}
public class WebAppInterface {
@JavascriptInterface
public void onContentChanged(String content) {
// 内容变化时的回调
}
}
}

@ -0,0 +1,177 @@
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
public class NoteReaderActivity extends AppCompatActivity {
public static final String EXTRA_NOTE_ID = "noteId";
private WebView webView;
private Note note;
private DataManager dataManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_reader);
dataManager = new DataManager(this);
loadNote();
initViews();
displayNote();
}
private void loadNote() {
String noteId = getIntent().getStringExtra(EXTRA_NOTE_ID);
if (noteId != null && !noteId.isEmpty()) {
for (Note n : dataManager.getNotes()) {
if (noteId.equals(n.getId())) {
note = n;
break;
}
}
}
}
private void initViews() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(note != null ? note.getTitle() : "笔记");
}
webView = findViewById(R.id.webview);
android.webkit.WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
webView.setWebViewClient(new WebViewClient());
}
private void displayNote() {
if (note == null) return;
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html><html><head>");
html.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
html.append("<style>");
html.append("body { font-family: sans-serif; padding: 16px; line-height: 1.6; background: #fff; }");
html.append("h1 { color: #333; margin-bottom: 16px; font-size: 24px; }");
html.append("p { color: #666; margin-bottom: 12px; }");
html.append("img { max-width: 100%; height: auto; margin: 12px 0; border-radius: 8px; display: block; }");
html.append(".course-info { color: #999; font-size: 14px; margin-bottom: 16px; }");
html.append("</style></head><body>");
if (note.getTitle() != null && !note.getTitle().isEmpty()) {
html.append("<h1>").append(escapeHtml(note.getTitle())).append("</h1>");
}
if (note.getCourseName() != null && !note.getCourseName().isEmpty()) {
html.append("<p class=\"course-info\">课程: ").append(escapeHtml(note.getCourseName())).append("</p>");
}
// 显示内容HTML格式
String content = note.getContent() != null ? note.getContent() : "";
// 兼容历史数据:还原 Unicode 转义(例如 \\u003C -> <
content = content
.replace("\\u003C", "<").replace("\\u003c", "<")
.replace("\\u003E", ">").replace("\\u003e", ">")
.replace("\\u0026", "&");
if (!content.isEmpty()) {
// 如果内容包含HTML直接使用否则转换为HTML
if (!content.contains("<")) {
content = content.replace("\n", "<br>");
}
html.append("<div>").append(content).append("</div>");
}
// 显示图片(如果内容中没有图片,则单独显示)
if (note.getImagePaths() != null && !note.getImagePaths().isEmpty()) {
boolean hasImagesInContent = content != null && content.contains("<img");
if (!hasImagesInContent) {
for (String imagePath : note.getImagePaths()) {
java.io.File imageFile = new java.io.File(imagePath);
if (imageFile.exists()) {
html.append("<img src=\"file://").append(imagePath).append("\" />");
}
}
}
}
html.append("</body></html>");
String baseUrl = "file://" + getFilesDir().getAbsolutePath() + "/";
webView.loadDataWithBaseURL(baseUrl, html.toString(), "text/html", "UTF-8", null);
}
private String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_note_reader, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void editNote() {
if (note == null) return;
Intent intent = new Intent(this, NoteEditorActivity.class);
intent.putExtra(NoteEditorActivity.EXTRA_NOTE_ID, note.getId());
startActivity(intent);
finish();
}
private void deleteNote() {
if (note == null) return;
new android.app.AlertDialog.Builder(this)
.setTitle("删除笔记")
.setMessage("确定要删除笔记《" + note.getTitle() + "》吗?")
.setPositiveButton("删除", (dialog, which) -> {
dataManager.deleteNote(note.getId());
// 删除图片文件
if (note.getImagePaths() != null) {
for (String imagePath : note.getImagePaths()) {
try {
java.io.File file = new java.io.File(imagePath);
if (file.exists()) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
finish();
})
.setNegativeButton("取消", null)
.show();
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save