diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0353834
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+*.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
+/app/build
+/app/.cxx
+/app/release
+/app/src/main/cpp
+/app/src/main/jniLibs
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..359bb53
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..0897082
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..fdf8d99
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0ad17cb
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Beijing_University_of_Technology.svg b/Beijing_University_of_Technology.svg
new file mode 100644
index 0000000..c91a101
--- /dev/null
+++ b/Beijing_University_of_Technology.svg
@@ -0,0 +1,281 @@
+
+
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..32d230b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+# bjut app
+
+日新工大第三方安卓版本,本APP是非官方的,免费开源的,功能实现完全依赖官方接口,个人数据仅保存在本地。初衷是集成北京工业大学 (BJUT) 所有有用的线上功能,本应用后续不定期更新。
+本APP无意冒充官方日新工大,请认真辨别,禁止用于非法用途。
+
+## 计划支持的功能
+
+* [x] 课程表查看
+* [x] 校园一卡通服务
+* [x] 新闻查看
+* [x] WEBVPN集成
+* [x] 集成校内VPN动态otp验证码 (特色)
+* [x] 桌面APP长按快捷菜单
+* [ ] 图书馆相关功能
+* [ ] 校内邮箱浏览
+* [ ] 办事大厅
+* [ ] 重要消息提示
+
+## 应用编写说明
+
+本代码使用Android Studio kotlin编写,由于版权和安全因素,部分静态链接库未在此处发布。相当于官版日新工大,此应用优化了一些数据的缓存逻辑,整体界面使用类Google Material风格设计,且适配了系统的暗色主题,部分页面依赖于系统WebView。
+
+## 应用兼容性说明
+
+本app支持armeabi-v7a和arm64-v8a两种安卓架构,理论上支持Android 7.0+版本。
+
+## 应用内部分截图
+
+![image](./img/1.jpg)
+![image](./img/2.jpg)
+![image](./img/3.jpg)
+![image](./img/4.jpg)
+![image](./img/5.jpg)
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..2ba7535
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,89 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.hlwdy.bjut"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.hlwdy.bjut"
+ minSdk = 24
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+ ndk {
+ abiFilters += listOf("armeabi-v7a", "arm64-v8a")
+ }
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ externalNativeBuild {
+ cmake {
+ cppFlags("-std=c++14")
+ //arguments("-DANDROID_STL=c++_shared")
+ arguments("-DANDROID_STL=none")
+ }
+ }
+ }
+
+ splits {
+ abi {
+ isEnable = true
+ reset()
+ include("armeabi-v7a", "arm64-v8a")
+ isUniversalApk=true
+ }
+ }
+
+ sourceSets {
+ getByName("main") {
+ jniLibs.srcDirs("src/main/jniLibs")
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ viewBinding = true
+ }
+
+ externalNativeBuild {
+ cmake {
+ path("src/main/cpp/CMakeLists.txt")
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.lifecycle.livedata.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.navigation.fragment.ktx)
+ implementation(libs.androidx.navigation.ui.ktx)
+ implementation(libs.androidx.gridlayout)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+ implementation("com.squareup.okhttp3:okhttp:4.10.0")
+ implementation("androidx.core:core-splashscreen:1.0.1")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f4b5930
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,31 @@
+# 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
+
+-dontwarn org.bouncycastle.jsse.BCSSLParameters
+-dontwarn org.bouncycastle.jsse.BCSSLSocket
+-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
+-dontwarn org.conscrypt.Conscrypt$Version
+-dontwarn org.conscrypt.Conscrypt
+-dontwarn org.conscrypt.ConscryptHostnameVerifier
+-dontwarn org.openjsse.javax.net.ssl.SSLParameters
+-dontwarn org.openjsse.javax.net.ssl.SSLSocket
+-dontwarn org.openjsse.net.ssl.OpenJSSE
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/hlwdy/bjut/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/hlwdy/bjut/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..64e8c9b
--- /dev/null
+++ b/app/src/androidTest/java/com/hlwdy/bjut/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.hlwdy.bjut
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.hlwdy.bjut", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..68247bc
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..d562689
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/hlwdy/bjut/ApkUpdate.kt b/app/src/main/java/com/hlwdy/bjut/ApkUpdate.kt
new file mode 100644
index 0000000..3d1e908
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ApkUpdate.kt
@@ -0,0 +1,10 @@
+package com.hlwdy.bjut
+
+import okhttp3.Callback
+
+class ApkUpdate {
+ fun getLatest(callback: Callback){
+ HttpUtils()
+ .get("https://api.github.com/repos/bjutapp/bjut/releases/latest",true,callback)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/BjutAPI.kt b/app/src/main/java/com/hlwdy/bjut/BjutAPI.kt
new file mode 100644
index 0000000..84c8346
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/BjutAPI.kt
@@ -0,0 +1,160 @@
+package com.hlwdy.bjut
+
+import android.util.Log
+import okhttp3.*
+import org.json.JSONObject
+import java.io.IOException
+
+class BjutAPI {
+ private fun timestamp(): String {
+ return System.currentTimeMillis().toString()
+ }
+
+ fun login(usname: String,pwd:String,callback: Callback){
+ val imei="bjut"
+ HttpUtils().addHeader("x-requested-with","XMLHttpRequest")
+ .addHeader("from-eai","1").addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("authorization-str",BjutHttpRsa.requestEncrypt("{\"ticket\":\"\",\"timestamp\":"+timestamp()+"}")).addParam(
+ "content",BjutHttpRsa.requestEncrypt(
+ "{\"xgh\":\"$usname\",\"password\":\"$pwd\",\"username\":\"$usname\",\"cid\":\"\",\"city\":\"\",\"type\":\"\",\"del_ids\":[],\"id\":\"\",\"ids\":[],\"imei\":\"$imei\",\"keyword\":\"\",\"login_ticket\":\"\",\"mobile_type\":\"android\",\"app_id\":\"\",\"name\":\"\",\"page\":\"1\",\"page_size\":\"5\",\"pageSize\":\"10\",\"province\":\"\",\"sid\":\"$imei\",\"ticket\":\"\",\"url\":\"\",\"words\":\"\"}"
+ )
+ ).post("https://itsapp.bjut.edu.cn/bjutapp/wap/app-login/index",callback)
+ }
+
+ fun getCardUrl(ses: String,callback: Callback){
+ HttpUtils().addHeader("Cookie","eai-sess=$ses;")
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .get("https://itsapp.bjut.edu.cn/uc/api/oauth/index?redirect=https%3A%2F%2Fydapp.bjut.edu.cn%2FopenV8HomePage&appid=200220816093810809&state=V8YKT&qrcode=1",false,callback)
+ }
+
+ fun getSchedule(ses: String,year: String, term: String, week: String,callback: Callback){
+ HttpUtils().addHeader("Cookie","eai-sess=$ses;")
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5").addHeader("Referer","https://itsapp.bjut.edu.cn/site/schedule/index")
+ .addParam("year",year)
+ .addParam("term",term)
+ .addParam("week",week)
+ .addParam("type","1")
+ .post("https://itsapp.bjut.edu.cn/timetable/wap/default/get-data",callback)
+ }
+
+ fun getTermWeek(ses: String,callback: Callback){
+ HttpUtils().addHeader("Cookie","eai-sess=$ses;")
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .get("https://itsapp.bjut.edu.cn/timetable/wap/default/get-index",true,callback)
+ }
+
+ fun getNewsList(ses: String,cid:String,page:String,callback: Callback){
+ val imei="bjut"
+ HttpUtils().addHeader("Cookie","eai-sess=$ses;")
+ .addHeader("x-requested-with","XMLHttpRequest")
+ .addHeader("from-eai","1").addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("authorization-str",BjutHttpRsa.requestEncrypt("{\"ticket\":\"\",\"timestamp\":"+timestamp()+"}")).addParam(
+ "content",BjutHttpRsa.requestEncrypt(
+ "{\"xgh\":\"\",\"password\":\"\",\"username\":\"\",\"cid\":\"$cid\",\"city\":\"\",\"type\":\"\",\"del_ids\":[],\"id\":\"\",\"ids\":[],\"imei\":\"$imei\",\"keyword\":\"\",\"login_ticket\":\"\",\"mobile_type\":\"android\",\"app_id\":\"\",\"name\":\"\",\"page\":\"$page\",\"page_size\":\"5\",\"pageSize\":\"10\",\"province\":\"\",\"sid\":\"$imei\",\"ticket\":\"\",\"url\":\"\",\"words\":\"\"}"
+ )
+ ).post("https://itsapp.bjut.edu.cn/bjutapp/wap/news/news-list",callback)
+ }
+
+ fun getWebVpnCookie(ses: String,callback: Callback){
+ HttpUtils()
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("x-requested-with","cn.edu.bjut.app")
+ .get("https://webvpn.bjut.edu.cn/https/77726476706e69737468656265737421f3f652d2253a7d44300d8db9d6562d/clientredirect?client_name=mc-wx&service=https%3A%2F%2Fwebvpn.bjut.edu.cn%2Flogin%3Fcas_login%3Dtrue",false,
+ object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ callback.onFailure(call,e)
+ }
+ override fun onResponse(call: Call, response: Response) {
+ var newUrl= response.headers["Location"].toString()
+ val cookies = response.headers.values("Set-Cookie")
+ .map { it.split(";")[0] }
+ .joinToString("; ")
+
+ HttpUtils()
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("x-requested-with","cn.edu.bjut.app")
+ .addHeader("Cookie", cookies)
+ .get(newUrl,false,
+ object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ callback.onFailure(call,e)
+ }
+ override fun onResponse(call: Call, response: Response) {
+ newUrl= response.headers["Location"].toString()
+ val cookies1 = "$cookies; eai-sess=$ses;"
+ //itsapp
+ HttpUtils()
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("x-requested-with","cn.edu.bjut.app")
+ .addHeader("Cookie", cookies1)
+ .get(newUrl,true,
+ object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ callback.onFailure(call,e)
+ }
+ override fun onResponse(call: Call, response: Response) {
+ //replace
+ newUrl= response.request.url.toString().replace("/http/", "/https/")
+ //callback.onResponse(call,response)
+ HttpUtils()
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("x-requested-with","cn.edu.bjut.app")
+ .addHeader("Cookie", cookies)
+ .get(newUrl,true,
+ object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ callback.onFailure(call,e)
+ }
+ override fun onResponse(call: Call, response: Response) {
+ newUrl= response.request.url.toString().replace("/http/", "/https/")
+ HttpUtils()
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("x-requested-with","cn.edu.bjut.app")
+ .addHeader("Cookie", cookies)
+ .get(newUrl,true,callback)
+ } }
+ )
+ } }
+ )
+
+ } }
+ )
+
+ } }
+ )
+ }
+
+ fun WebVpnLoginMy(tk: String,callback: Callback){
+ HttpUtils().addHeader("Cookie","wengine_vpn_ticketwebvpn_bjut_edu_cn=$tk;")
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("x-requested-with","cn.edu.bjut.app")
+ .get("https://webvpn.bjut.edu.cn/https/77726476706e69737468656265737421f3f652d2253a7d44300d8db9d6562d/login?service=https%3A%2F%2Fportal.bjut.edu.cn%2Fcommon%2FactionCasLogin%3Fredirect_url%3Dhttps%3A%2F%2Fportal.bjut.edu.cn%2Fpage%2Fsite%2Findex",false,
+ object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ callback.onFailure(call,e)
+ }
+ override fun onResponse(call: Call, response: Response) {
+ var newUrl= response.headers["Location"].toString()
+ if(newUrl=="null"){
+ callback.onFailure(call,IOException("no tk"))
+ return
+ }
+ HttpUtils().addHeader("Cookie","wengine_vpn_ticketwebvpn_bjut_edu_cn=$tk;")
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .addHeader("x-requested-with","cn.edu.bjut.app")
+ .get(newUrl,true,callback)
+ } }
+ )
+ }
+
+ fun getNewsDetail(tk: String,id:String,callback: Callback){
+ HttpUtils().addHeader("Cookie","wengine_vpn_ticketwebvpn_bjut_edu_cn=$tk;")
+ .addHeader("User-Agent","ZhilinEai ZhilinBjutApp/2.5")
+ .get("https://webvpn.bjut.edu.cn/https/77726476706e69737468656265737421e0f85388263c2652741d9de29d51367b7cd8/site/get_news_detail?id=$id",true,callback)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/LogDbHelper.kt b/app/src/main/java/com/hlwdy/bjut/LogDbHelper.kt
new file mode 100644
index 0000000..4f041d1
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/LogDbHelper.kt
@@ -0,0 +1,98 @@
+package com.hlwdy.bjut
+
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+
+class LogDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
+ companion object {
+ private const val DATABASE_NAME = "AppLogs.db"
+ private const val DATABASE_VERSION = 1
+
+ // 表结构定义
+ private const val TABLE_LOGS = "logs"
+ const val COLUMN_ID = "_id"
+ const val COLUMN_TIMESTAMP = "timestamp"
+ const val COLUMN_TAG = "tag"
+ const val COLUMN_MESSAGE = "message"
+ const val COLUMN_STACK_TRACE = "stack_trace"
+
+ // 建表SQL
+ private const val SQL_CREATE_LOGS_TABLE = """
+ CREATE TABLE $TABLE_LOGS (
+ $COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT,
+ $COLUMN_TIMESTAMP INTEGER NOT NULL,
+ $COLUMN_TAG TEXT NOT NULL,
+ $COLUMN_MESSAGE TEXT NOT NULL,
+ $COLUMN_STACK_TRACE TEXT
+ )
+ """
+ }
+
+ override fun onCreate(db: SQLiteDatabase) {
+ db.execSQL(SQL_CREATE_LOGS_TABLE)
+ }
+
+ override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
+ // 简单处理,直接删除旧表并创建新表
+ db.execSQL("DROP TABLE IF EXISTS $TABLE_LOGS")
+ onCreate(db)
+ }
+
+ fun addLog(tag: String, message: String, throwable: Throwable? = null) {
+ val db = writableDatabase
+
+ val values = ContentValues().apply {
+ put(COLUMN_TIMESTAMP, System.currentTimeMillis())
+ put(COLUMN_TAG, tag)
+ put(COLUMN_MESSAGE, message)
+ put(COLUMN_STACK_TRACE, throwable?.stackTraceToString())
+ }
+
+ db.insert(TABLE_LOGS, null, values)
+
+ // 保持最新的1000条记录
+ db.execSQL("""
+ DELETE FROM $TABLE_LOGS
+ WHERE $COLUMN_ID NOT IN (
+ SELECT $COLUMN_ID FROM $TABLE_LOGS
+ ORDER BY $COLUMN_TIMESTAMP DESC
+ LIMIT 200
+ )
+ """)
+ }
+
+ fun getLogs(): Cursor {
+ return readableDatabase.query(
+ TABLE_LOGS,
+ null,
+ null,
+ null,
+ null,
+ null,
+ "$COLUMN_TIMESTAMP DESC"
+ )
+ }
+
+ fun clearLogs() {
+ writableDatabase.delete(TABLE_LOGS, null, null)
+ }
+}
+
+object appLogger {
+ private lateinit var dbHelper: LogDbHelper
+
+ fun init(context: Context) {
+ dbHelper = LogDbHelper(context.applicationContext)
+ }
+
+ fun e(tag: String, message: String, throwable: Throwable? = null) {
+ dbHelper.addLog(tag, message, throwable)
+ }
+
+ fun getLogs(): Cursor = dbHelper.getLogs()
+
+ fun clearLogs() = dbHelper.clearLogs()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/MainActivity.kt b/app/src/main/java/com/hlwdy/bjut/MainActivity.kt
new file mode 100644
index 0000000..f76a868
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/MainActivity.kt
@@ -0,0 +1,275 @@
+package com.hlwdy.bjut
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.WindowManager
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toast
+import com.google.android.material.navigation.NavigationView
+import androidx.navigation.findNavController
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.navigateUp
+import androidx.navigation.ui.setupActionBarWithNavController
+import androidx.navigation.ui.setupWithNavController
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.hlwdy.bjut.databinding.ActivityMainBinding
+import com.hlwdy.bjut.ui.AboutActivity
+import com.hlwdy.bjut.ui.LogViewActivity
+import com.hlwdy.bjut.ui.LoginActivity
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.File
+import java.io.IOException
+
+class MainActivity : AppCompatActivity() {
+
+ fun showToast(message: String) {
+ android.os.Handler(this.mainLooper).post {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ fun updateVPNTK(tk:String){
+ android.os.Handler(this.mainLooper).post {
+ account_session_util(this).editWebVpnTK(tk,(System.currentTimeMillis() / 1000L).toString())
+ }
+ }
+
+ fun refreshWebVpn(){
+ fun getCookieValue(cookieString: String, key: String): String? {
+ return cookieString.split(";")
+ .map { it.trim() }
+ .find { it.startsWith("$key=") }
+ ?.substringAfter("=")
+ }
+ var time_str=account_session_util(this).getUserDetails()[account_session_util.KEY_WEBVPNTKTIME]
+ if(time_str!=null){
+ if(time_str.toLong()+1200>(System.currentTimeMillis() / 1000L)){
+ //showToast("use tk cache")
+ appLogger.e("Info", "Use WebVPNTK cache:$time_str")
+ return
+ }
+ }
+ //refresh webvpn tk
+ BjutAPI().getWebVpnCookie(account_session_util(this).getUserDetails()[account_session_util.KEY_SESS].toString(),object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ var tk=getCookieValue(response.request.headers.get("Cookie").toString(),"wengine_vpn_ticketwebvpn_bjut_edu_cn")
+ updateVPNTK(tk.toString())
+ appLogger.e("Info", "New WebVPN tk $tk")
+ //showToast("new webvpn tk $tk")
+ //prelogin to my
+ BjutAPI().WebVpnLoginMy(tk.toString()
+ ,object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {}
+ })
+ }
+ })
+ }
+
+ private lateinit var appBarConfiguration: AppBarConfiguration
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ appLogger.init(this)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ //webvpn
+ refreshWebVpn()
+
+ setSupportActionBar(binding.appBarMain.toolbar)
+
+ val drawerLayout: DrawerLayout = binding.drawerLayout
+ val navView: NavigationView = binding.navView
+ val navController = findNavController(R.id.nav_host_fragment_content_main)
+ // Passing each menu ID as a set of Ids because each
+ // menu should be considered as top level destinations.
+ appBarConfiguration = AppBarConfiguration(
+ setOf(
+ R.id.nav_home,R.id.nav_news, R.id.nav_card, R.id.nav_library,R.id.nav_schedule,R.id.nav_otp
+ ), drawerLayout
+ )
+ setupActionBarWithNavController(navController, appBarConfiguration)
+ navView.setupWithNavController(navController)
+
+ window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
+
+ val navheader=binding.navView.getHeaderView(0)
+ var user_info=account_session_util(this).getUserDetails()
+ navheader.findViewById(R.id.name_text).setText(user_info[account_session_util.KEY_NAME])
+ navheader.findViewById(R.id.userid).setText(user_info[account_session_util.KEY_USERNAME])
+
+ appLogger.e("Info", "User配置读取完成-"+user_info[account_session_util.KEY_USERNAME])
+ }
+
+ fun clearAppWebView() {
+ try {
+ // 获取应用的 app_webview 目录
+ val webviewDir = File(this.applicationInfo.dataDir, "app_webview")
+ if (webviewDir.exists()) {
+ webviewDir.deleteRecursively()
+ }
+ showToast("Cleared successfully")
+
+ } catch (e: Exception) {
+ showToast("Error clearing webview data: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ menuInflater.inflate(R.menu.main, menu)
+ return true
+ }
+
+ fun logout(){
+ Toast.makeText(this, "logout", Toast.LENGTH_SHORT).show()
+ account_session_util(this).logoutUser()
+ val intent = Intent(this, LoginActivity::class.java)
+ startActivity(intent)
+ finish()
+ }
+
+ fun ShowUpdate(res: JSONObject) {
+ val Curversion = packageManager.getPackageInfo(packageName, 0).versionName
+ if (Curversion != res.getString("tag_name")) {
+ val tmp = res.getJSONArray("assets")
+ android.os.Handler(this.mainLooper).post {
+ val container = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ )
+ setPadding(32, 16, 32, 16)
+ }
+
+ // 版本信息
+ TextView(this).apply {
+ text = """
+ 当前版本:$Curversion
+ 新版本发布日期:${res.getString("published_at")}
+
+ 更新内容:
+ ${res.getString("body")}
+
+ 可用下载:
+ """.trimIndent()
+ container.addView(this)
+ }
+
+ // 为每个下载项创建按钮
+ for (i in 0 until tmp.length()) {
+ val classObject = tmp.getJSONObject(i)
+ val name = classObject.getString("name")
+ val url = classObject.getString("browser_download_url")
+/*
+ TextView(this).apply {
+ text = name
+ setPadding(0, 16, 0, 4)
+ container.addView(this)
+ }
+*/
+ MaterialButton(this).apply {
+ text = "$name"
+ setOnClickListener {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ }
+ isAllCaps = false
+ container.addView(this)
+ }
+ }
+
+ MaterialAlertDialogBuilder(this)
+ .setTitle("发现新版本:" + res.getString("tag_name"))
+ .setView(container)
+ .setNegativeButton("稍后再说") { dialog, _ ->
+ dialog.dismiss()
+ }
+ .setCancelable(false)
+ .show()
+ }
+ }else{
+ showToast("已是最新版本")
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.action_clearCache->{
+ clearAppWebView()
+ true
+ }
+ //logout
+ R.id.action_logout -> {
+ if(account_session_util(this).getUserDetails()[account_session_util.KEY_OTPDATA]==null){
+ logout()
+ }else{
+ MaterialAlertDialogBuilder(this)
+ .setTitle("退出前请备份好OTPdata,否则无法找回,是否确认退出?")
+ .setPositiveButton("继续退出") { _, _ ->
+ logout()
+ }
+ .setNegativeButton("取消", null)
+ .show()
+ }
+ true
+ }
+ R.id.action_loglook->{
+ startActivity(Intent(this, LogViewActivity::class.java))
+ true
+ }
+ R.id.action_about->{
+ startActivity(Intent(this, AboutActivity::class.java))
+ true
+ }
+ R.id.action_checkUpdate->{
+ ApkUpdate().getLatest(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ val res_text=response.body?.string().toString()
+ try{
+ val res=JSONObject(res_text)
+ ShowUpdate(res)
+ }catch (e: JSONException){
+ showToast("error")
+ appLogger.e("Error", "Try CheckUpdate error",e)
+ }
+ }
+ })
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ val navController = findNavController(R.id.nav_host_fragment_content_main)
+ return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/NativeEncrypt.kt b/app/src/main/java/com/hlwdy/bjut/NativeEncrypt.kt
new file mode 100644
index 0000000..df13d90
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/NativeEncrypt.kt
@@ -0,0 +1,10 @@
+package com.hlwdy.bjut
+
+class NativeEncrypthlwdyck {
+ init {
+ System.loadLibrary("encrypt")
+ }
+
+ external fun keypr(): String
+ external fun keypu(): String
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/RouterActivity.kt b/app/src/main/java/com/hlwdy/bjut/RouterActivity.kt
new file mode 100644
index 0000000..8cf6dc0
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/RouterActivity.kt
@@ -0,0 +1,40 @@
+package com.hlwdy.bjut
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import com.hlwdy.bjut.ui.card.CardFragment
+import com.hlwdy.bjut.ui.otp.OtpFragment
+import com.hlwdy.bjut.ui.schedule.ScheduleFragment
+
+class RouterActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ val splashScreen = installSplashScreen()
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_router)
+
+ appLogger.init(this)
+
+ if (intent?.action == "openCardCode") {
+ val fragment = CardFragment().apply {
+ arguments = Bundle().apply {
+ putBoolean("jump_code", true)
+ }
+ }
+
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.container, fragment)
+ .commit()
+ }else if (intent?.action == "openSchedule") {
+ val fragment = ScheduleFragment()
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.container, fragment)
+ .commit()
+ }else if (intent?.action == "openOTP") {
+ val fragment = OtpFragment()
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.container, fragment)
+ .commit()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/account_session_util.kt b/app/src/main/java/com/hlwdy/bjut/account_session_util.kt
new file mode 100644
index 0000000..ecd0621
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/account_session_util.kt
@@ -0,0 +1,72 @@
+package com.hlwdy.bjut
+
+import android.content.Context
+import android.content.SharedPreferences
+
+class account_session_util(context: Context) {
+ private var prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ private var editor: SharedPreferences.Editor = prefs.edit()
+
+ companion object {
+ const val PREF_NAME = "AppPrefs"
+ const val IS_LOGIN = "IsLogin"
+ const val KEY_USERNAME = "usname"
+ const val KEY_NAME = "name"
+ const val KEY_TK = "ticket"
+ const val KEY_SESS = "ses"
+ const val KEY_LGTIME = "lgtime"
+ const val KEY_WEBVPNTK="webvpn"
+ const val KEY_WEBVPNTKTIME="webvpntktime"
+ const val KEY_CARDID="cardid"
+ const val KEY_OTPDATA="otpdata"
+ }
+
+ fun createLoginSession(usname: String,name: String,tk: String,ses: String) {
+ editor.putBoolean(IS_LOGIN, true)
+ editor.putString(KEY_USERNAME, usname)
+ editor.putString(KEY_NAME, name)
+ editor.putString(KEY_SESS, ses)
+ editor.putString(KEY_TK, tk)
+ editor.putString(KEY_LGTIME, System.currentTimeMillis().toString())
+ editor.commit()
+ }
+
+ fun isLoggedIn(): Boolean {
+ return prefs.getBoolean(IS_LOGIN, false)
+ }
+
+ fun getUserDetails(): HashMap {
+ val user = HashMap()
+ user[KEY_USERNAME] = prefs.getString(KEY_USERNAME, null)
+ user[KEY_NAME] = prefs.getString(KEY_NAME, null)
+ user[KEY_SESS] = prefs.getString(KEY_SESS, null)
+ user[KEY_TK] = prefs.getString(KEY_TK, null)
+ user[KEY_LGTIME] = prefs.getString(KEY_LGTIME, null)
+ user[KEY_WEBVPNTK] = prefs.getString(KEY_WEBVPNTK, null)
+ user[KEY_WEBVPNTKTIME] = prefs.getString(KEY_WEBVPNTKTIME, "0")
+ user[KEY_CARDID] = prefs.getString(KEY_CARDID, null)
+ user[KEY_OTPDATA] = prefs.getString(KEY_OTPDATA, null)
+ return user
+ }
+
+ fun logoutUser() {
+ editor.clear()
+ editor.commit()
+ }
+
+ fun editWebVpnTK(tk: String,time:String) {
+ editor.putString(KEY_WEBVPNTK, tk)
+ editor.putString(KEY_WEBVPNTKTIME, time)
+ editor.commit()
+ }
+
+ fun editCardID(id: String) {
+ editor.putString(KEY_CARDID, id)
+ editor.commit()
+ }
+
+ fun editOTPData(data: String) {
+ editor.putString(KEY_OTPDATA, data)
+ editor.commit()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/basefragment.kt b/app/src/main/java/com/hlwdy/bjut/basefragment.kt
new file mode 100644
index 0000000..aa488b3
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/basefragment.kt
@@ -0,0 +1,62 @@
+package com.hlwdy.bjut
+
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.fragment.app.Fragment
+import com.hlwdy.bjut.ui.LoadingDialog
+
+abstract class BaseFragment : Fragment() {
+ private var loadingView: View? = null
+
+ protected fun showLoading() {
+ if (loadingView == null) {
+ // 获取Fragment的根视图
+ val rootView = view as? ViewGroup ?: return
+
+ // 创建加载视图
+ loadingView = layoutInflater.inflate(R.layout.dialog_loading, null)
+
+ // 创建与父容器相同大小的LayoutParams
+ val params = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+
+ // 找到Fragment内容的实际位置
+ val location = IntArray(2)
+ rootView.getLocationInWindow(location)
+
+ // 设置loadingView的位置和大小
+ loadingView?.layoutParams = params
+
+ // 添加到根视图
+ rootView.addView(loadingView)
+
+ // 确保loadingView在最上层
+ loadingView?.bringToFront()
+ }
+ loadingView?.visibility = View.VISIBLE
+ }
+
+ fun hideLoading() {
+ loadingView?.visibility = View.GONE
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ (loadingView?.parent as? ViewGroup)?.removeView(loadingView)
+ loadingView = null
+ }
+
+ // 协程扩展函数
+ protected suspend fun withLoading(block: suspend () -> T): T {
+ try {
+ showLoading()
+ return block()
+ } finally {
+ hideLoading()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/encrypt.kt b/app/src/main/java/com/hlwdy/bjut/encrypt.kt
new file mode 100644
index 0000000..10726ec
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/encrypt.kt
@@ -0,0 +1,82 @@
+package com.hlwdy.bjut
+
+import java.security.spec.PKCS8EncodedKeySpec
+import java.security.spec.X509EncodedKeySpec
+import java.security.KeyFactory
+import java.security.PrivateKey
+import java.security.PublicKey
+import android.util.Base64
+import javax.crypto.Cipher
+
+object RSAUtils {
+ private const val RSA = "RSA"
+ private const val RSA_PADDING = "RSA/ECB/PKCS1Padding"
+ private const val MAX_ENCRYPT_BLOCK = 117 // RSA_KEY_SIZE / 8 - 11 for PKCS1Padding
+ private const val MAX_DECRYPT_BLOCK = 128 // RSA_KEY_SIZE / 8
+
+ fun encrypt(data: String, publicKey: String): String {
+ val keySpec = X509EncodedKeySpec(Base64.decode(publicKey, Base64.NO_WRAP))
+ val keyFactory = KeyFactory.getInstance(RSA)
+ val key: PublicKey = keyFactory.generatePublic(keySpec)
+ val cipher = Cipher.getInstance(RSA_PADDING)
+ cipher.init(Cipher.ENCRYPT_MODE, key)
+
+ val dataBytes = data.toByteArray()
+ val inputLen = dataBytes.size
+ var offset = 0
+ val output = mutableListOf()
+
+ while (inputLen - offset > 0) {
+ val curBlock = if (inputLen - offset > MAX_ENCRYPT_BLOCK) MAX_ENCRYPT_BLOCK else inputLen - offset
+ val encryptedBlock = cipher.doFinal(dataBytes, offset, curBlock)
+ output.addAll(encryptedBlock.toList())
+ offset += curBlock
+ }
+
+ return Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
+ }
+
+ fun decrypt(data: String, privateKey: String): String {
+ val keySpec = PKCS8EncodedKeySpec(Base64.decode(privateKey, Base64.NO_WRAP))
+ val keyFactory = KeyFactory.getInstance(RSA)
+ val key: PrivateKey = keyFactory.generatePrivate(keySpec)
+ val cipher = Cipher.getInstance(RSA_PADDING)
+ cipher.init(Cipher.DECRYPT_MODE, key)
+
+ val encryptedData = Base64.decode(data, Base64.NO_WRAP)
+ val inputLen = encryptedData.size
+ var offset = 0
+ val output = mutableListOf()
+
+ while (inputLen - offset > 0) {
+ val curBlock = if (inputLen - offset > MAX_DECRYPT_BLOCK) MAX_DECRYPT_BLOCK else inputLen - offset
+ val decryptedBlock = cipher.doFinal(encryptedData, offset, curBlock)
+ output.addAll(decryptedBlock.toList())
+ offset += curBlock
+ }
+
+ return String(output.toByteArray())
+ }
+}
+
+object BjutHttpRsa{
+ private val PRIVATE_KEY = NativeEncrypthlwdyck().keypr()
+ private val PUBLIC_KEY = NativeEncrypthlwdyck().keypu()
+ fun requestDecrypt(paramString: String): String {
+ return try {
+ RSAUtils.decrypt(paramString, PRIVATE_KEY)
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ ""
+ }
+ }
+
+ fun requestEncrypt(paramString: String): String {
+ return try {
+ RSAUtils.encrypt(paramString, PUBLIC_KEY)
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/request_util.kt b/app/src/main/java/com/hlwdy/bjut/request_util.kt
new file mode 100644
index 0000000..ebd56f4
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/request_util.kt
@@ -0,0 +1,64 @@
+package com.hlwdy.bjut
+
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.RequestBody.Companion.toRequestBody
+
+class HttpUtils {
+ private var params = mutableMapOf()
+ private var headers = mutableMapOf()
+
+ fun get(url: String,is_redirect:Boolean,callBack: Callback) {
+ val okHttpClient = OkHttpClient().newBuilder().followRedirects(is_redirect).build()
+ val request: Request = Request.Builder().apply {
+ headers.forEach {
+ addHeader(it.key, it.value)
+ }
+ url(url)
+ get()
+ }.build()
+ okHttpClient.newCall(request).enqueue(callBack)
+ }
+
+ fun post(url: String, callBack: Callback) {
+ val okHttpClient = OkHttpClient()
+ val formBody: FormBody.Builder = FormBody.Builder()
+ params.forEach(formBody::add)
+ val request: Request = Request.Builder()
+ .apply {
+ headers.forEach {
+ addHeader(it.key, it.value)
+ }
+ url(url)
+ post(formBody.build())
+ }.build()
+ okHttpClient.newCall(request).enqueue(callBack)
+ }
+
+ fun postJson(url: String, jsonString: String, callBack: Callback) {
+ val okHttpClient = OkHttpClient()
+ val stringBody = jsonString.toRequestBody("application/json;charset=utf-8".toMediaType())
+ val request: Request = Request.Builder().apply {
+ headers.forEach {
+ addHeader(it.key, it.value)
+ }
+ url(url)
+ post(stringBody)
+ }.build()
+
+ okHttpClient.newCall(request).enqueue(callBack)
+
+ }
+
+ fun addParam(key: String, value: String): HttpUtils {
+ params[key] = value
+ return this
+ }
+
+ fun addHeader(key: String, value: String): HttpUtils {
+ headers[key] = value
+ return this
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/AboutActivity.kt b/app/src/main/java/com/hlwdy/bjut/ui/AboutActivity.kt
new file mode 100644
index 0000000..b6f2176
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/AboutActivity.kt
@@ -0,0 +1,24 @@
+package com.hlwdy.bjut.ui
+
+import android.os.Bundle
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.hlwdy.bjut.R
+
+class AboutActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_about)
+
+ val versionName = try {
+ packageManager.getPackageInfo(packageName, 0).versionName
+ } catch (e: Exception) {
+ "unknown"
+ }
+
+ // 设置版本号文本
+ findViewById(R.id.tvVersion).text = "Version $versionName"
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/LogViewActivity.kt b/app/src/main/java/com/hlwdy/bjut/ui/LogViewActivity.kt
new file mode 100644
index 0000000..e52d2ff
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/LogViewActivity.kt
@@ -0,0 +1,96 @@
+package com.hlwdy.bjut.ui
+
+import android.database.Cursor
+import android.graphics.Color
+import android.icu.text.SimpleDateFormat
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.hlwdy.bjut.LogDbHelper
+import com.hlwdy.bjut.appLogger
+import com.hlwdy.bjut.R
+import java.util.Date
+import java.util.Locale
+
+class LogAdapter : RecyclerView.Adapter() {
+ private var cursor: Cursor? = null
+ private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+
+ fun swapCursor(newCursor: Cursor?) {
+ cursor?.close()
+ cursor = newCursor
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_log, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ cursor?.let { cursor ->
+ if (cursor.moveToPosition(position)) {
+ val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(LogDbHelper.COLUMN_TIMESTAMP))
+ val tag = cursor.getString(cursor.getColumnIndexOrThrow(LogDbHelper.COLUMN_TAG))
+ val message = cursor.getString(cursor.getColumnIndexOrThrow(LogDbHelper.COLUMN_MESSAGE))
+ val stackTrace = cursor.getString(cursor.getColumnIndexOrThrow(LogDbHelper.COLUMN_STACK_TRACE))
+
+ holder.bind(timestamp, tag, message, stackTrace)
+ }
+ }
+ }
+
+ override fun getItemCount(): Int = cursor?.count ?: 0
+
+ inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ private val tvTimestamp: TextView = view.findViewById(R.id.tvTimestamp)
+ private val tvTag: TextView = view.findViewById(R.id.tvTag)
+ private val tvMessage: TextView = view.findViewById(R.id.tvMessage)
+ private val tvStackTrace: TextView = view.findViewById(R.id.tvStackTrace)
+
+ fun bind(timestamp: Long, tag: String, message: String, stackTrace: String?) {
+ tvTimestamp.text = dateFormat.format(Date(timestamp))
+ tvTag.text = tag
+ if(tag=="Error")tvTag.setTextColor(Color.RED)
+ tvMessage.text = message
+
+ if (stackTrace != null) {
+ tvStackTrace.visibility = View.VISIBLE
+ tvStackTrace.text = stackTrace
+ } else {
+ tvStackTrace.visibility = View.GONE
+ }
+ }
+ }
+}
+
+class LogViewActivity : AppCompatActivity() {
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var adapter: LogAdapter
+ private var cursor: Cursor? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_log_view)
+
+ recyclerView = findViewById(R.id.recyclerView)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+
+ adapter = LogAdapter()
+ recyclerView.adapter = adapter
+
+ cursor = appLogger.getLogs()
+ adapter.swapCursor(cursor)
+ }
+
+ override fun onDestroy() {
+ cursor?.close()
+ super.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/LoginActivity.kt b/app/src/main/java/com/hlwdy/bjut/ui/LoginActivity.kt
new file mode 100644
index 0000000..0c64eb1
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/LoginActivity.kt
@@ -0,0 +1,228 @@
+package com.hlwdy.bjut.ui
+
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.os.Bundle
+import android.text.method.HideReturnsTransformationMethod
+import android.text.method.PasswordTransformationMethod
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import com.hlwdy.bjut.BjutAPI
+import com.hlwdy.bjut.BjutHttpRsa
+import com.hlwdy.bjut.MainActivity
+import com.hlwdy.bjut.R
+import com.hlwdy.bjut.account_session_util
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import okhttp3.*
+import java.io.IOException
+import org.json.JSONObject
+
+class LoginActivity : AppCompatActivity() {
+ private val scope = CoroutineScope(Dispatchers.Main + Job())
+
+ private lateinit var usernameEditText: EditText
+ private lateinit var passwordEditText: EditText
+ private lateinit var loginButton: Button
+ private lateinit var togglePasswordVisibility: ImageButton
+ private lateinit var rememberPasswordCheckBox: CheckBox
+
+ private lateinit var overlayView: View
+
+ private lateinit var sharedPreferences: SharedPreferences
+
+ fun showToast(message: String) {
+ android.os.Handler(this.mainLooper).post {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ fun finishLogin(usname: String,name: String,tk: String,ses: String) {
+ android.os.Handler(this.mainLooper).post {
+ account_session_util(this).createLoginSession(usname, name, tk, ses)
+ }
+ }
+
+ fun jumpToMain(){
+ // 登录成功,跳转到主界面
+ val intent = Intent(this, MainActivity::class.java)
+ //intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+ startActivity(intent)
+ finish() // 结束登录Activity,防止用户返回
+ }
+
+ class OverlayView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+ ) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {
+
+ init {
+ setBackgroundColor(ContextCompat.getColor(context, R.color.bjut_blue))
+
+ // 创建主图片的 Bitmap
+ val mainOptions = BitmapFactory.Options().apply {
+ inSampleSize = 7 // 缩小
+ }
+ val mainBitmap = BitmapFactory.decodeResource(resources, R.drawable.bjut, mainOptions)
+
+ // 创建底部横幅的 Bitmap
+ val bannerOptions = BitmapFactory.Options().apply {
+ inSampleSize = 1
+ }
+ val bannerBitmap = BitmapFactory.decodeResource(resources, R.drawable.bjut_all, bannerOptions)
+
+ // 创建一个新的 Bitmap 来组合两张图片
+ val combinedBitmap = Bitmap.createBitmap(
+ mainBitmap.width,
+ mainBitmap.height,
+ Bitmap.Config.ARGB_8888
+ )
+
+ val canvas = Canvas(combinedBitmap)
+
+ // 绘制主图片
+ canvas.drawBitmap(mainBitmap, 0f, 0f, null)
+
+ // 计算横幅的位置(底部)
+ val bannerMatrix = Matrix()
+ val scale = mainBitmap.width.toFloat() / bannerBitmap.width.toFloat()
+ bannerMatrix.setScale(scale, scale)
+ bannerMatrix.postTranslate(
+ 0f,
+ mainBitmap.height - (bannerBitmap.height * scale)
+ )
+
+ // 绘制横幅
+ canvas.drawBitmap(bannerBitmap, bannerMatrix, null)
+
+ // 设置组合后的图片
+ setImageBitmap(combinedBitmap)
+ scaleType = ScaleType.CENTER
+
+ // 回收不需要的 Bitmap
+ mainBitmap.recycle()
+ bannerBitmap.recycle()
+ }
+
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ return true
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ val splashScreen = installSplashScreen()
+ super.onCreate(savedInstanceState)
+ //splashScreen.setKeepOnScreenCondition { true }
+ //setContentView(R.layout.login_page)
+
+ overlayView = OverlayView(this).apply {
+ layoutParams = FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT
+ )
+ visibility = View.VISIBLE
+ }
+ val rootLayout = findViewById(android.R.id.content)
+ rootLayout.addView(overlayView)
+
+ val context=this
+ scope.launch {
+ if(account_session_util(context).isLoggedIn())jumpToMain()
+ else {
+ rootLayout.removeView(overlayView)
+ setContentView(R.layout.login_page)
+ usernameEditText = findViewById(R.id.usernameEditText)
+ passwordEditText = findViewById(R.id.passwordEditText)
+ loginButton = findViewById(R.id.loginButton)
+ togglePasswordVisibility = findViewById(R.id.togglePasswordVisibility)
+ rememberPasswordCheckBox = findViewById(R.id.rememberPasswordCheckBox)
+
+ var passwordVisible = false
+ togglePasswordVisibility.setOnClickListener {
+ passwordVisible = !passwordVisible
+ if (passwordVisible) {
+ passwordEditText.transformationMethod = HideReturnsTransformationMethod.getInstance()
+ togglePasswordVisibility.setImageResource(android.R.drawable.ic_menu_view) // 使用"隐藏"图标
+ } else {
+ passwordEditText.transformationMethod = PasswordTransformationMethod.getInstance()
+ togglePasswordVisibility.setImageResource(android.R.drawable.ic_menu_view) // 使用"显示"图标
+ }
+ passwordEditText.setSelection(passwordEditText.text.length)
+ }
+
+ sharedPreferences = getSharedPreferences("LoginPrefs", Context.MODE_PRIVATE)
+ if (sharedPreferences.getBoolean("remember_password", false)) {
+ usernameEditText.setText(sharedPreferences.getString("username", ""))
+ passwordEditText.setText(sharedPreferences.getString("password", ""))
+ rememberPasswordCheckBox.isChecked = true
+ }
+
+ loginButton.setOnClickListener {
+ val usname = usernameEditText.text.toString()
+ val password = passwordEditText.text.toString()
+
+ if (rememberPasswordCheckBox.isChecked) {
+ with(sharedPreferences.edit()) {
+ putString("username", usname)
+ putString("password", password)
+ putBoolean("remember_password", true)
+ apply()
+ }
+ } else {
+ with(sharedPreferences.edit()) {
+ remove("username")
+ remove("password")
+ putBoolean("remember_password", false)
+ apply()
+ }
+ }
+
+ BjutAPI().login(usname,password,object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ //showToast(BjutHttpRsa.requestDecrypt(response.body?.string().toString()))
+ val res = JSONObject(
+ BjutHttpRsa.requestDecrypt(
+ response.body?.string().toString()
+ )
+ )
+ val ses= Cookie.parseAll(response.request.url,response.headers).find { it.name == "eai-sess" }?.value.toString()
+ if (res.get("e")==0) {
+ finishLogin(usname,res.getJSONObject("d").get("name").toString(),res.getJSONObject("d").get("login_ticket").toString(),ses)
+ jumpToMain()
+ } else {
+ showToast(res.get("m").toString())
+ }
+ }
+ })
+ }
+ }
+ }
+
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ scope.cancel()
+ }
+}
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/card/CardFragment.kt b/app/src/main/java/com/hlwdy/bjut/ui/card/CardFragment.kt
new file mode 100644
index 0000000..47111a9
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/card/CardFragment.kt
@@ -0,0 +1,252 @@
+package com.hlwdy.bjut.ui.card
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebViewClient
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.fragment.app.Fragment
+import com.hlwdy.bjut.BjutAPI
+import com.hlwdy.bjut.account_session_util
+import com.hlwdy.bjut.databinding.FragmentCardBinding
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import java.io.IOException
+import androidx.fragment.app.viewModels
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.File
+import java.net.URL
+import android.webkit.WebView
+import androidx.core.content.ContextCompat.startActivity
+import com.hlwdy.bjut.BaseFragment
+import com.hlwdy.bjut.appLogger
+import java.net.HttpURLConnection
+import java.util.concurrent.TimeUnit
+
+private var isJumpCode:Boolean=false
+
+class EnhancedCachingWebViewClient(private val context: Context,private val frag:BaseFragment) : WebViewClient() {
+
+ private val cacheDir = File(context.cacheDir, "web_cache")
+ private val cssPattern = Regex("\\.css$")
+ private val vendorPattern = Regex("chunk-vendors.*\\.js$")
+ private val picPattern = Regex("\\.png$")
+
+ // 缓存有效期(毫秒)
+ private val CSS_CACHE_DURATION = TimeUnit.DAYS.toMillis(5) // 1天
+ private val VENDOR_CACHE_DURATION = TimeUnit.DAYS.toMillis(7) // 7天
+ private val PIC_CACHE_DURATION = TimeUnit.DAYS.toMillis(15)
+
+ init {
+ cacheDir.mkdirs()
+ }
+
+ override fun shouldInterceptRequest(
+ view: WebView,
+ request: WebResourceRequest
+ ): WebResourceResponse? {
+ val url = request.url.toString()
+
+ when {
+ cssPattern.containsMatchIn(url) -> return handleCacheableResource(url, "text/css", CSS_CACHE_DURATION)
+ vendorPattern.containsMatchIn(url) -> return handleCacheableResource(url, "application/javascript", VENDOR_CACHE_DURATION)
+ picPattern.containsMatchIn(url) -> return handleCacheableResource(url, "image/png", PIC_CACHE_DURATION)
+ }
+
+ // 对于其他文件,使用默认处理
+ return super.shouldInterceptRequest(view, request)
+ }
+
+ private fun handleCacheableResource(url: String, mimeType: String, cacheDuration: Long): WebResourceResponse? {
+ val cachedFile = File(cacheDir, url.hashCode().toString())
+
+ // 如果缓存文件存在且未过期,直接从缓存加载
+ if (cachedFile.exists() && (System.currentTimeMillis() - cachedFile.lastModified() < cacheDuration)) {
+ return WebResourceResponse(mimeType, "UTF-8", FileInputStream(cachedFile))
+ }
+
+ // 如果缓存不存在或已过期,下载并缓存
+ try {
+ val connection = URL(url).openConnection() as HttpURLConnection
+ connection.connect()
+ val inputStream = connection.inputStream
+
+ // 保存到缓存
+ FileOutputStream(cachedFile).use { output ->
+ inputStream.copyTo(output)
+ }
+
+ // 返回下载的内容
+ return WebResourceResponse(mimeType, "UTF-8", FileInputStream(cachedFile))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ return null
+ }
+
+
+ override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
+ return try {
+ if (url!!.startsWith("http:") || url.startsWith("https:")) {
+ false
+ } else {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ startActivity(context,intent,null)
+ true
+ }
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ override fun onPageFinished(view: WebView, url: String) {
+ frag.hideLoading()
+ account_session_util(context).editCardID(url.takeLast(32))
+ // 注入CSS
+ val css = "@media (prefers-color-scheme: dark) { body,uni-page-body,.bdbg1,.code-bg,.news-w-bg,.bdbg,.ticket-nav,.tap2{ background: #121212 !important; }uni-navigator,.menu-list,.news-w,.payment-method,.condition, .condition-checkbox,.amtbutton,.v-tabs__container,.v-tabs__container-item,.tr,.nowcode,.tap-box,.uni-picker-select,.uni-picker-header,.newsCol,.text-w,.txt,.itemArea_li,#popup_content{background:#313131 !important;color:white !important;}uni-text{color:white !important}.code-w{box-shadow:none !important;}.news-w,.newsli{border-bottom:none !important;}.bdbg1,uni-view{color:white !important}.bdt{border-top: 1px solid #555555 !important;}.per-details,.bor-bottom{border-bottom: 1px solid #555555 !important;}}"
+ view.evaluateJavascript(
+ """
+ (function() {
+ var style = document.createElement('style');
+ style.type = 'text/css';
+ style.innerHTML = '$css';
+ document.head.appendChild(style);
+ })();
+ """,
+ null
+ )
+ // 显示WebView
+ view.visibility = View.VISIBLE
+ if(isJumpCode){
+ view.loadUrl("https://ydapp.bjut.edu.cn/#/pages_other/qrcode/qrcode/qrcode?openid="+url.takeLast(32))
+ isJumpCode=false
+ }
+ }
+
+}
+
+class CardFragment : BaseFragment() {
+
+ private var _binding: FragmentCardBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ private val viewModel: CardViewModel by viewModels()
+
+ fun showToast(message: String) {
+ activity?.let { fragmentActivity ->
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ Toast.makeText(fragmentActivity, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ fun loadUrl(url: String) {
+ activity?.let {
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ viewModel.webView?.settings?.javaScriptEnabled=true
+ viewModel.webView?.webViewClient=EnhancedCachingWebViewClient(requireContext(),this)
+ viewModel.webView?.loadUrl(url)
+ viewModel.isWebViewInitialized=true
+ //hideLoading()
+ }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+
+ _binding = FragmentCardBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+ if (viewModel.webView == null) {
+ viewModel.webView = binding.cardView
+ } else {
+ // 如果WebView已存在,从旧的父视图中移除
+ (viewModel.webView?.parent as? ViewGroup)?.removeView(viewModel.webView)
+ // 将已存在的WebView添加到新的布局中
+ (binding.root as? ViewGroup)?.addView(viewModel.webView)
+ }
+
+ return root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ isJumpCode = arguments?.getBoolean("jump_code", false) ?: false
+
+ showLoading()
+
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (viewModel.webView?.canGoBack() == true) {
+ viewModel.webView?.goBack()
+ } else {
+ isEnabled = false
+ requireActivity().onBackPressedDispatcher.onBackPressed()
+ }
+ }
+ })
+
+ if(!viewModel.isWebViewInitialized){
+ BjutAPI().getCardUrl(account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_SESS].toString(),object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ if(response.code==302){
+ val cardUrl= response.headers["Location"].toString()
+ //showToast(cardUrl)
+ appLogger.e("Info", "CardUrl ok:$cardUrl")
+ loadUrl(cardUrl)
+ }
+ }
+ })
+ }else{
+ viewModel.webViewState?.let { viewModel.webView?.restoreState(it) }
+ hideLoading()
+ }
+
+ }
+ override fun onPause() {
+ super.onPause()
+ // 保存WebView状态
+ viewModel.webView?.let { webView ->
+ val state = Bundle()
+ webView.saveState(state)
+ viewModel.webViewState = state
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ (viewModel.webView?.parent as? ViewGroup)?.removeView(viewModel.webView)
+ _binding = null
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/card/CardViewModel.kt b/app/src/main/java/com/hlwdy/bjut/ui/card/CardViewModel.kt
new file mode 100644
index 0000000..be10e28
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/card/CardViewModel.kt
@@ -0,0 +1,11 @@
+package com.hlwdy.bjut.ui.card
+
+import android.os.Bundle
+import android.webkit.WebView
+import androidx.lifecycle.ViewModel
+
+class CardViewModel : ViewModel() {
+ var webView: WebView? = null
+ var webViewState: Bundle? = null
+ var isWebViewInitialized = false
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/home/HomeFragment.kt b/app/src/main/java/com/hlwdy/bjut/ui/home/HomeFragment.kt
new file mode 100644
index 0000000..015b14d
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/home/HomeFragment.kt
@@ -0,0 +1,70 @@
+package com.hlwdy.bjut.ui.home
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.CookieManager
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import com.hlwdy.bjut.R
+import com.hlwdy.bjut.account_session_util
+import com.hlwdy.bjut.databinding.FragmentHomeBinding
+
+class HomeFragment : Fragment() {
+
+ private var _binding: FragmentHomeBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ fun showToast(message: String) {
+ activity?.let { fragmentActivity ->
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ Toast.makeText(fragmentActivity, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+
+ _binding = FragmentHomeBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+ binding.btnWebVpn.setOnClickListener {
+ val cookieManager = CookieManager.getInstance()
+ cookieManager.setAcceptCookie(true)
+ cookieManager.setCookie(".webvpn.bjut.edu.cn", "wengine_vpn_ticketwebvpn_bjut_edu_cn="+
+ account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_WEBVPNTK].toString()+"; path=/")
+ cookieManager.flush()
+ val intent = Intent(requireContext(), WebVpnViewActivity::class.java)
+ startActivity(intent)
+ }
+
+ binding.btnCardCode.setOnClickListener {
+ val bundle = Bundle().apply {
+ putBoolean("jump_code", true)
+ }
+ findNavController().navigate(R.id.nav_card,bundle)
+ }
+
+ return root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/home/WebVpnEntry.kt b/app/src/main/java/com/hlwdy/bjut/ui/home/WebVpnEntry.kt
new file mode 100644
index 0000000..17488d5
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/home/WebVpnEntry.kt
@@ -0,0 +1,155 @@
+package com.hlwdy.bjut.ui.home
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.view.View
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipGroup
+import com.google.android.material.progressindicator.LinearProgressIndicator
+import com.hlwdy.bjut.R
+
+class WebVpnViewActivity : AppCompatActivity() {
+ private lateinit var webView: WebView
+ private lateinit var chipGroup: ChipGroup
+ private lateinit var progressBar: LinearProgressIndicator
+
+ data class Website(
+ val name: String,
+ val url: String
+ )
+
+ private val websites = listOf(
+ //Website("教务管理系统", "https://webvpn.bjut.edu.cn/https/77726476706e69737468656265737421fae046903f242652741d9de29d51367becd8/"),
+ Website("教务管理系统", "https://webvpn.bjut.edu.cn/https/77726476706e69737468656265737421fae046903f242652741d9de29d51367becd8/sso/ddlogin"),
+ //https://jwglxt.bjut.edu.cn/
+ Website("网络计费系统", "https://webvpn.bjut.edu.cn/https/77726476706e69737468656265737421faf152992b362652741d9de29d51367b6c5c/"),
+ //https://jfself.bjut.edu.cn/
+ Website("EduOJ", "https://webvpn.bjut.edu.cn/http/77726476706e69737468656265737421a1a013d2756126012946d8fccc/"),
+ //http://172.21.17.104/
+ )
+ private fun setTitle(text:String){
+ android.os.Handler(this.mainLooper).post {
+ findViewById(R.id.webTitle).text = text
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.webvpn_view)
+
+ webView = findViewById(R.id.webvpn_webview)
+ chipGroup = findViewById(R.id.chipGroup)
+ findViewById(R.id.btnBack).setOnClickListener {
+ finish()
+ }
+ progressBar = findViewById(R.id.progressBar)
+ setupWebView()
+ createChips()
+ }
+
+ private fun setupWebView() {
+ webView.apply {
+ settings.javaScriptEnabled = true
+ webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
+ if (url != null&&url.contains("http")) {
+ view?.loadUrl(url)
+ return true
+ }
+ return false
+ }
+
+ // 对于Android API 24及以上版本
+ override fun shouldOverrideUrlLoading(
+ view: WebView?,
+ request: WebResourceRequest?
+ ): Boolean {
+ if (url != null&&url.toString().contains("http")) {
+ request?.url?.let { uri ->
+ view?.loadUrl(uri.toString())
+ return true
+ }
+ }
+ return false
+ }
+
+ // 可选:添加页面加载状态回调
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ super.onPageStarted(view, url, favicon)
+ // 显示加载进度条或提示
+ }
+
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ // 隐藏加载进度条或提示
+ }
+ }
+
+ webChromeClient=object:WebChromeClient() {
+ override fun onProgressChanged(view: WebView?, newProgress: Int) {
+ if (newProgress < 100) {
+ progressBar.visibility = View.VISIBLE
+ progressBar.progress = newProgress
+ } else {
+ progressBar.visibility = View.GONE
+ }
+ }
+ }
+
+ settings.apply {
+ // 支持缩放
+ setSupportZoom(true)
+ builtInZoomControls = true
+ displayZoomControls = false
+
+ // 自适应屏幕
+ useWideViewPort = true
+ loadWithOverviewMode = true
+
+ // 允许混合内容(HTTP和HTTPS)
+ mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
+ }
+ }
+ }
+
+ private fun createChips() {
+ chipGroup.isSelectionRequired=true
+ websites.forEachIndexed { index, (name, url) ->
+ val chip = Chip(this).apply {
+ text = name
+ isCheckable = true
+
+ setOnClickListener {
+ if (isChecked) {
+ webView.loadUrl(url)
+ }
+ }
+
+ if (index == 0) {
+ isChecked = true
+ webView.loadUrl(url)
+ }
+ }
+ chipGroup.addView(chip)
+ }
+ }
+
+ // 处理返回按钮
+ override fun onBackPressed() {
+ if (webView.canGoBack()) {
+ webView.goBack()
+ } else {
+ super.onBackPressed()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/library/LibraryFragment.kt b/app/src/main/java/com/hlwdy/bjut/ui/library/LibraryFragment.kt
new file mode 100644
index 0000000..8d41a26
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/library/LibraryFragment.kt
@@ -0,0 +1,42 @@
+package com.hlwdy.bjut.ui.library
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.hlwdy.bjut.databinding.FragmentLibraryBinding
+
+class LibraryFragment : Fragment() {
+
+ private var _binding: FragmentLibraryBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val slideshowViewModel =
+ ViewModelProvider(this).get(LibraryViewModel::class.java)
+
+ _binding = FragmentLibraryBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+ val textView: TextView = binding.textLibrary
+ slideshowViewModel.text.observe(viewLifecycleOwner) {
+ textView.text = it
+ }
+ return root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/library/LibraryViewModel.kt b/app/src/main/java/com/hlwdy/bjut/ui/library/LibraryViewModel.kt
new file mode 100644
index 0000000..d33e0cd
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/library/LibraryViewModel.kt
@@ -0,0 +1,13 @@
+package com.hlwdy.bjut.ui.library
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class LibraryViewModel : ViewModel() {
+
+ private val _text = MutableLiveData().apply {
+ value = "Library, coming soon"
+ }
+ val text: LiveData = _text
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/loading.kt b/app/src/main/java/com/hlwdy/bjut/ui/loading.kt
new file mode 100644
index 0000000..6e34516
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/loading.kt
@@ -0,0 +1,41 @@
+package com.hlwdy.bjut.ui
+
+import android.app.Dialog
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import com.hlwdy.bjut.R
+
+class LoadingDialog private constructor(context: Context) {
+ private val dialog: Dialog = Dialog(context, R.style.TransparentDialog)
+
+ init {
+ dialog.setContentView(R.layout.dialog_loading)
+ dialog.setCancelable(false)
+ dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ }
+
+ companion object {
+ private var instance: LoadingDialog? = null
+
+ @Synchronized
+ fun getInstance(context: Context): LoadingDialog {
+ if (instance == null) {
+ instance = LoadingDialog(context)
+ }
+ return instance!!
+ }
+ }
+
+ fun show() {
+ if (!dialog.isShowing) {
+ dialog.show()
+ }
+ }
+
+ fun dismiss() {
+ if (dialog.isShowing) {
+ dialog.dismiss()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/news/NewsAdapter.kt b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsAdapter.kt
new file mode 100644
index 0000000..5700a40
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsAdapter.kt
@@ -0,0 +1,36 @@
+package com.hlwdy.bjut.ui.news
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.hlwdy.bjut.R
+
+class NewsAdapter(
+ private val items: List,
+ private val onItemClick: (NewsItem) -> Unit
+) : RecyclerView.Adapter() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val TitleText: TextView = view.findViewById(R.id.newsItemText)
+ val DateText: TextView = view.findViewById(R.id.newsItemDate)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.news_item_layout, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val newsItem = items[position]
+ holder.TitleText.text = newsItem.title
+ holder.DateText.text = newsItem.date
+ holder.itemView.setOnClickListener {
+ onItemClick(newsItem)
+ }
+ }
+
+ override fun getItemCount() = items.size
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/news/NewsDetailActivity.kt b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsDetailActivity.kt
new file mode 100644
index 0000000..3b07345
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsDetailActivity.kt
@@ -0,0 +1,81 @@
+package com.hlwdy.bjut.ui.news
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.webkit.WebView
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.hlwdy.bjut.BjutAPI
+import com.hlwdy.bjut.R
+import com.hlwdy.bjut.account_session_util
+import com.hlwdy.bjut.ui.schedule.Course
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import org.json.JSONObject
+import java.io.IOException
+
+class NewsDetailActivity : AppCompatActivity() {
+ fun showToast(message: String) {
+ android.os.Handler(this.mainLooper).post {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ private fun showContent(content:String){
+ android.os.Handler(this.mainLooper).post {
+ var webview=findViewById(R.id.news_webview)
+ webview.settings.javaScriptEnabled=true
+ webview.loadDataWithBaseURL(null,
+ "$content "
+ +"",
+ "text/html", "UTF-8", null)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.news_detail)
+
+ val title = intent.getStringExtra(EXTRA_TITLE)
+ val id = intent.getStringExtra(EXTRA_ID)
+
+ BjutAPI().getNewsDetail(account_session_util(this).getUserDetails()[account_session_util.KEY_WEBVPNTK].toString(),id.toString(),object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ try{
+ val res=JSONObject(response.body?.string().toString())
+ showContent(res.toString())
+ if(res.getString("e")=="0"){
+ showContent(res.getJSONObject("d").getString("content"))
+ }else{
+ showToast("error")
+ }
+ }catch (e:Exception){
+ showToast("error")
+ }
+ }
+ })
+
+ findViewById(R.id.news_title).text = title
+ }
+
+ companion object {
+ private const val EXTRA_TITLE = "extra_title"
+ private const val EXTRA_ID = "extra_id"
+
+ fun start(context: Context, newsItem: NewsItem) {
+ val intent = Intent(context, NewsDetailActivity::class.java).apply {
+ putExtra(EXTRA_TITLE, newsItem.title)
+ putExtra(EXTRA_ID, newsItem.id)
+ }
+ context.startActivity(intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/news/NewsFragment.kt b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsFragment.kt
new file mode 100644
index 0000000..f495d06
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsFragment.kt
@@ -0,0 +1,200 @@
+package com.hlwdy.bjut.ui.news
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.tabs.TabLayout
+import com.hlwdy.bjut.BaseFragment
+import com.hlwdy.bjut.BjutAPI
+import com.hlwdy.bjut.BjutHttpRsa
+import com.hlwdy.bjut.account_session_util
+import com.hlwdy.bjut.appLogger
+import com.hlwdy.bjut.databinding.FragmentNewsBinding
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import org.json.JSONObject
+import java.io.IOException
+
+class NewsFragment : BaseFragment() {
+ private var _binding: FragmentNewsBinding? = null
+ private val binding get() = _binding!!
+
+ private lateinit var adapter: NewsAdapter
+ private val newsList = mutableListOf()
+
+ private val viewModel: NewsViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentNewsBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ fun showToast(message: String) {
+ activity?.let { fragmentActivity ->
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ Toast.makeText(fragmentActivity, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ private fun refreshNewsList(cid:String,page:Int=1){
+ isLoading=true
+ showLoading()
+ BjutAPI().getNewsList(account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_SESS].toString(),cid,page.toString()
+ ,object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ val res = JSONObject(
+ BjutHttpRsa.requestDecrypt(
+ response.body?.string().toString()
+ )
+ )
+ if(res.getString("e")=="0"){
+ val tmp=res.getJSONObject("d").getJSONObject("list").getJSONArray("data")
+ val l=mutableListOf()
+ for (i in 0 until tmp.length()) {
+ val classObject = tmp.getJSONObject(i)
+ l.add(NewsItem(classObject.getString("id"),classObject.getString("title"),
+ classObject.getString("summary"), classObject.getString("createdate")))
+ }
+ activity?.let {
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ if(page==1){//first
+ updateNewsList(l)
+ }else{
+ addNewsItems(l)
+ }
+ Curpage=page
+ Curcid=cid
+ hideLoading()
+ }
+ }
+ }
+ isLoading=false
+ }else{
+ showToast("error")
+ appLogger.e("Error", "Try NewsList $cid-$page error")
+ }
+ }
+ })
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupRecyclerView()
+
+ BjutAPI().WebVpnLoginMy(account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_WEBVPNTK].toString()
+ ,object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {}
+ })
+
+ val tabLayout = binding.tabLayout
+ val tabTitles = listOf("校发通知", "会议通知", "公示公告", "教学通告","学校工作信息","院部处通知","院部处工作信息","学术海报")
+ tabTitles.forEach { title ->
+ tabLayout.addTab(tabLayout.newTab().apply {
+ text = title
+ contentDescription = title
+ })
+ }
+ viewModel.selectedTabPosition.observe(viewLifecycleOwner) { position ->
+ tabLayout.selectTab(tabLayout.getTabAt(position))
+ }
+
+ tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab?) {
+ viewModel.setSelectedTab(tab?.position ?: 0)
+ refreshNewsList((tab?.position?.plus(1)).toString())
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) {}
+ override fun onTabReselected(tab: TabLayout.Tab?) {}
+ })
+ if (viewModel.isFirstLoad||viewModel.selectedTabPosition.value==0) {
+ refreshNewsList("1")
+ viewModel.isFirstLoad = false
+ }
+ }
+
+ private var isLoading = false
+ private var Curpage=0
+ private var Curcid=""
+
+ private fun setupRecyclerView() {
+ adapter = NewsAdapter(newsList) { newsItem ->
+ openNewsDetail(newsItem)
+ }
+ binding.NewsListView.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = this@NewsFragment.adapter
+ }
+ binding.NewsListView.layoutManager = LinearLayoutManager(requireContext())
+ binding.NewsListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+ val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
+ val totalItemCount = layoutManager.itemCount
+ if (!isLoading&&lastVisibleItem >= totalItemCount - 1) {
+ refreshNewsList(Curcid,Curpage+1)
+ //showToast("next")
+ }
+ }
+ })
+ }
+
+ // 添加新闻项的方法
+ private fun addNewsItem(id:String,title: String, content: String, date:String) {
+ newsList.add(NewsItem(id,title, content,date))
+ adapter.notifyItemInserted(newsList.size - 1)
+ }
+
+ private fun addNewsItems(newItems: List) {
+ val cnt=newsList.size
+ newsList.addAll(newItems)
+ adapter.notifyItemRangeInserted(cnt, newItems.size)
+ }
+
+ private fun updateNewsList(newItems: List) {
+ clearNewsList()
+ newsList.addAll(newItems)
+ adapter.notifyItemRangeInserted(0, newItems.size)
+ }
+
+ private fun clearNewsList() {
+ val size = newsList.size
+ newsList.clear()
+ adapter.notifyItemRangeRemoved(0, size)
+ }
+
+ private fun openNewsDetail(newsItem: NewsItem) {
+ NewsDetailActivity.start(requireContext(), newsItem)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/news/NewsItem.kt b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsItem.kt
new file mode 100644
index 0000000..ce9c1a4
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsItem.kt
@@ -0,0 +1,8 @@
+package com.hlwdy.bjut.ui.news
+
+data class NewsItem(
+ val id:String,
+ val title: String,
+ val content: String,
+ val date:String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/news/NewsViewModel.kt b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsViewModel.kt
new file mode 100644
index 0000000..b8f3512
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/news/NewsViewModel.kt
@@ -0,0 +1,16 @@
+package com.hlwdy.bjut.ui.news
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class NewsViewModel: ViewModel() {
+ private val _selectedTabPosition = MutableLiveData(0)
+ val selectedTabPosition: LiveData = _selectedTabPosition
+
+ fun setSelectedTab(position: Int) {
+ _selectedTabPosition.value = position
+ }
+
+ var isFirstLoad = true
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/otp/OtpFragment.kt b/app/src/main/java/com/hlwdy/bjut/ui/otp/OtpFragment.kt
new file mode 100644
index 0000000..19cf43d
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/otp/OtpFragment.kt
@@ -0,0 +1,379 @@
+package com.hlwdy.bjut.ui.otp
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.database.sqlite.SQLiteDatabase
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.text.InputType
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import com.hlwdy.bjut.account_session_util
+import com.hlwdy.bjut.databinding.FragmentOtpBinding
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.launch
+import net.arraynetworks.vpn.OTPManager
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import java.io.IOException
+import java.security.SecureRandom
+import java.util.UUID
+
+/*
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+
+fun readOtpData(context:Context): ByteArray? {
+ var otpData: ByteArray?=null
+ var database: SQLiteDatabase? = null
+ try {
+ val dbPath = context.getDatabasePath("otp.db").path
+ database = SQLiteDatabase.openDatabase(
+ dbPath,
+ null,
+ SQLiteDatabase.OPEN_READONLY
+ )
+ database.query(
+ "servers", // 表名
+ null, // 所有列
+ null, // 无条件
+ null,
+ null,
+ null,
+ null
+ )?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ otpData = cursor.getBlob(cursor.getColumnIndexOrThrow("data"))
+ }
+ }
+ } catch (e: Exception) {
+
+ } finally {
+ database?.close()
+ }
+ return otpData
+}
+
+class AESCrypto {
+ // IV (Initialization Vector)
+ private val initVector: ByteArray = byteArrayOf(
+ 97, 114, 114, 97, 121, 100, 101, 118, 73, 86,
+ 99, 108, 105, 99, 107, 49
+ )
+
+ private val ivSpec: IvParameterSpec = IvParameterSpec(initVector)
+
+ // Encryption Key
+ private val secretKey: ByteArray = byteArrayOf(
+ 97, 114, 114, 97, 121, 110, 101, 116, 119, 111,
+ 114, 107, 115, 57, 50, 48, 106, 83, 100, 56,
+ 102, 42, 35, 57, 42, 100, 45, 35, 106, 48,
+ 46, 72
+ )
+
+ private val keySpec: SecretKeySpec = SecretKeySpec(secretKey, "AES")
+
+ private var cipher: Cipher? = null
+
+ init {
+ cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
+ }
+
+ /**
+ * 解密数据
+ * @param encryptedData 加密的数据
+ * @return 解密后的数据
+ * @throws Exception 如果解密失败
+ */
+ @Throws(Exception::class)
+ fun decrypt(encryptedData: ByteArray): ByteArray {
+ try {
+ cipher?.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
+ return cipher?.doFinal(encryptedData) ?: throw Exception("Cipher not initialized")
+ } catch (e: Exception) {
+ val message = "[decrypt] ${e.message}"
+ throw Exception(message)
+ }
+ }
+
+ /**
+ * 加密数据
+ * @param plainData 原始数据
+ * @return 加密后的数据
+ * @throws Exception 如果加密失败
+ */
+ @Throws(Exception::class)
+ fun encrypt(plainData: ByteArray): ByteArray {
+ try {
+ cipher?.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
+ return cipher?.doFinal(plainData) ?: throw Exception("Cipher not initialized")
+ } catch (e: Exception) {
+ val message = "[encrypt] ${e.message}"
+ throw Exception(message)
+ }
+ }
+
+ companion object {
+ /**
+ * 静态解密方法
+ * @param data 要解密的数据
+ * @return 解密后的数据,如果解密失败返回null
+ */
+ @JvmStatic
+ fun decryptStatic(data: ByteArray?): ByteArray? {
+ if (data != null && data.isNotEmpty()) {
+ try {
+ return AESCrypto().decrypt(data)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ return null
+ }
+ }
+}*/
+
+fun generateSM():ByteArray{
+ val uuid = UUID.randomUUID().toString()
+ val secureRandom = SecureRandom.getInstance("SHA1PRNG")
+ secureRandom.setSeed(uuid.toByteArray())
+ val randomBytes = ByteArray(10)
+ secureRandom.nextBytes(randomBytes)
+ return randomBytes
+}
+
+class OtpFragment : Fragment() {
+
+ private var _binding: FragmentOtpBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ fun showToast(message: String) {
+ activity?.let { fragmentActivity ->
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ Toast.makeText(fragmentActivity, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ private var otpD:String?=null
+ fun refreshOtp(){
+ if(otpD!=null){
+ val otpManager = OTPManager()
+ val secret = otpD.toString().chunked(2).map { it.toInt(16).toByte() }.toByteArray()
+ val otp = otpManager.generateOTP(secret)
+ binding.textOtp.text=String.format("%06d",otp)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+
+ _binding = FragmentOtpBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+ otpD=account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_OTPDATA]
+ refreshOtp()
+ /*var otpData=readOtpData(requireContext())
+ if(otpData!=null){
+ val otpManager = OTPManager()
+ val secret = AESCrypto().decrypt(otpData)
+ showToast(String(net.arraynetworks.vpn.NativeLib().encodeOptData(secret)))
+ val otp = otpManager.generateOTP(secret)
+ textView.text=String.format("%06d",otp)
+ }*/
+
+ binding.btnLook.setOnClickListener {
+ val editText = EditText(context).apply {
+ setText(otpD.toString())
+ isFocusable = false
+ isFocusableInTouchMode = false
+ isLongClickable = true
+ keyListener=null
+ setTextIsSelectable(true) // 允许选择文本
+ setPadding(32, 32, 32, 32)
+ }
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle("OTP data")
+ .setView(editText)
+ .setPositiveButton("复制") { _, _ ->
+ // 复制文本到剪贴板
+ val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("text", otpD.toString())
+ clipboard.setPrimaryClip(clip)
+ showToast("已复制")
+ }
+ .setNegativeButton("取消", null)
+ .show()
+ }
+
+ binding.btnRegister.setOnClickListener {
+ val builder = MaterialAlertDialogBuilder(requireContext())
+ builder.setTitle("登录到校园VPN")
+ val layout = LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(32, 32, 32, 32)
+ }
+ val usernameLayout = TextInputLayout(requireContext()).apply {
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ )
+ hint = "账号"
+ boxBackgroundMode = TextInputLayout.BOX_BACKGROUND_OUTLINE // 设置外边框样式
+ }
+ val username = TextInputEditText(requireContext()).apply { setText(account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_USERNAME]) }
+ usernameLayout.addView(username)
+
+ val passwordLayout = TextInputLayout(requireContext()).apply {
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ )
+ hint = "密码"
+ boxBackgroundMode = TextInputLayout.BOX_BACKGROUND_OUTLINE
+ // 添加密码可见性开关
+ endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
+ }
+ val password = TextInputEditText(requireContext()).apply {
+ inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ }
+ passwordLayout.addView(password)
+
+ layout.addView(usernameLayout)
+ layout.addView(passwordLayout)
+ builder.setView(layout)
+ builder.setPositiveButton("设备注册") { dialog, which ->
+ val userAccount = username.text.toString()
+ val userPassword = password.text.toString()
+ val data=generateSM()
+ showToast("请稍后")
+ OTPManager().registerOTP(userAccount,userPassword,String(net.arraynetworks.vpn.NativeLib().encodeOptData(data)),
+ object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ val res=response.body?.string().toString()
+ activity?.let { fragmentActivity ->
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ if(res.contains("登录失败")){
+ showToast("登录失败,请检查账号密码")
+ }else if(res.contains("重复绑定")){
+ showToast("已经绑定过,无法重复绑定")
+ }else {
+ showToast("绑定完成")
+ otpD=data.joinToString("") { "%02x".format(it) }
+ account_session_util(requireContext()).editOTPData(otpD.toString())
+ refreshOtp()
+ }
+ }
+ }
+ }
+ }})
+ }
+
+ builder.setNegativeButton("取消") { dialog, which ->
+ dialog.cancel()
+ }
+
+ val dialog = builder.create()
+ dialog.show()
+ }
+
+ binding.btnAdd.setOnClickListener {
+ val editText = EditText(context).apply {
+ setTextIsSelectable(true) // 允许选择文本
+ setPadding(32, 32, 32, 32)
+ }
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle("导入OTP data")
+ .setView(editText)
+ .setPositiveButton("导入") { _, _ ->
+ if(!editText.text.toString().matches(Regex("^[a-zA-Z0-9]*$")) ||editText.text.toString().length!=20){//20位
+ showToast("数据格式无效")
+ return@setPositiveButton
+ }
+ if(otpD!=null){
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle("这将覆盖当前data,是否确认操作?")
+ .setPositiveButton("确认") { _, _ ->
+ otpD=editText.text.toString()
+ account_session_util(requireContext()).editOTPData(otpD.toString())
+ refreshOtp()
+ }
+ .setNegativeButton("取消", null)
+ .show()
+ }else{
+ otpD=editText.text.toString()
+ account_session_util(requireContext()).editOTPData(otpD.toString())
+ refreshOtp()
+ }
+ }
+ .setNegativeButton("取消", null)
+ .show()
+ }
+
+ return root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ // 当Fragment处于STARTED状态(可见)时才会执行
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ refreshOtp()
+ flow {
+ while (true) {
+ // 每次都基于当前系统时间计算,所以恢复时会显示正确的时间
+ val remainingSeconds = 60 - (System.currentTimeMillis() / 1000).toInt() % 60
+ emit(remainingSeconds)
+ delay(1000)
+ }
+ }.collect { remainingSeconds ->
+ binding.textOtptime.text = "$remainingSeconds 秒后更新"
+ if (remainingSeconds == 60) {
+ // 处理刷新逻辑
+ refreshOtp()
+ }
+ }
+ }
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/schedule/ScheduleFragment.kt b/app/src/main/java/com/hlwdy/bjut/ui/schedule/ScheduleFragment.kt
new file mode 100644
index 0000000..c8ed9a9
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/schedule/ScheduleFragment.kt
@@ -0,0 +1,424 @@
+package com.hlwdy.bjut.ui.schedule
+
+import android.content.Context
+import android.graphics.Color
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.text.TextUtils
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.Space
+import android.widget.TableRow
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.hlwdy.bjut.BaseFragment
+import com.hlwdy.bjut.BjutAPI
+import com.hlwdy.bjut.R
+import com.hlwdy.bjut.account_session_util
+import com.hlwdy.bjut.appLogger
+import com.hlwdy.bjut.databinding.FragmentScheduleBinding
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+import kotlin.math.abs
+
+class ScheduleCacheManager(private val context: Context) {
+ private val cacheDir = File(context.cacheDir, "schedule_cache")
+ private val cacheFile = File(cacheDir, "schedule.json")
+ private val cacheDuration = TimeUnit.HOURS.toMillis(8) // 缓存时间
+ private val maxCacheEntries = 50 // 最大缓存条目数
+
+ init {
+ cacheDir.mkdirs()
+ if (!cacheFile.exists()) {
+ try {
+ cacheFile.createNewFile()
+ cacheFile.writeText("{}")
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ fun getCachedData(key: String): String? {
+ if (!cacheFile.exists() || cacheFile.length() == 0L) return null
+
+ try {
+ val jsonObject = JSONObject(cacheFile.readText())
+ if (!jsonObject.has(key)) return null
+
+ val entry = jsonObject.getJSONObject(key)
+ val timestamp = entry.getLong("timestamp")
+ val data = entry.getString("data")
+
+ // 检查缓存是否过期
+ if (System.currentTimeMillis() - timestamp > cacheDuration) {
+ return null
+ }
+
+ return data
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ }
+ }
+
+ fun cacheData(key: String, data: String) {
+ try {
+ val jsonObject = if (cacheFile.exists() && cacheFile.length() > 0) {
+ JSONObject(cacheFile.readText())
+ } else {
+ JSONObject()
+ }
+
+ val entry = JSONObject().apply {
+ put("data", data)
+ put("timestamp", System.currentTimeMillis())
+ }
+
+ jsonObject.put(key, entry)
+
+ // 清理旧的缓存条目
+ cleanupOldEntries(jsonObject)
+
+ cacheFile.writeText(jsonObject.toString())
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun cleanupOldEntries(jsonObject: JSONObject) {
+ if (jsonObject.length() <= maxCacheEntries) return
+
+ val entries = jsonObject.keys().asSequence().toList()
+ val sortedEntries = entries.sortedBy { key ->
+ jsonObject.getJSONObject(key).getLong("timestamp")
+ }
+
+ val entriesToRemove = sortedEntries.take(sortedEntries.size - maxCacheEntries)
+ entriesToRemove.forEach { key ->
+ jsonObject.remove(key)
+ }
+ }
+}
+
+class ScheduleFragment : BaseFragment() {
+
+ private var _binding: FragmentScheduleBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ private lateinit var viewModel: ScheduleViewModel
+
+ private fun generateColorIntForId(id: String): Int {
+ val hue = (id.hashCode() and 0xFFFFFF) % 360
+ val lightness = 0.3f + (id.hashCode() and 0xFF) / 255f * 0.15f
+ return hslToColorInt(hue, lightness)
+ }
+
+ private fun hslToColorInt(hue: Int, lightness: Float): Int {
+ val c = (1 - abs(2 * lightness - 1)) * 0.7f
+ val x = c * (1 - abs((hue / 60f % 2) - 1))
+ val m = lightness - c / 2
+
+ val (r, g, b) = when (hue) {
+ in 0..59 -> Triple(c, x, 0f)
+ in 60..119 -> Triple(x, c, 0f)
+ in 120..179 -> Triple(0f, c, x)
+ in 180..239 -> Triple(0f, x, c)
+ in 240..299 -> Triple(x, 0f, c)
+ else -> Triple(c, 0f, x)
+ }
+
+ val red = ((r + m) * 255).toInt()
+ val green = ((g + m) * 255).toInt()
+ val blue = ((b + m) * 255).toInt()
+
+ return Color.rgb(red, green, blue)
+ }
+
+
+ private fun createSchedule(courses: List) {
+ val table = binding.scheduleTable
+
+ // Add header row
+ val headerRow = TableRow(context)
+ headerRow.addView(createTextView("时间", isHeader = true))
+ viewModel.days.forEach { day ->
+ headerRow.addView(createTextView(day, isHeader = true))
+ }
+ table.addView(headerRow)
+
+ // Create a 2D array to represent the schedule
+ val scheduleGrid = Array(viewModel.timeSlots.size) { Array(viewModel.days.size) { null } }
+
+ // Fill the scheduleGrid with courses
+ courses.forEach { course ->
+ val dayIndex = viewModel.days.indexOf(course.day)
+ val startSlot = course.timeCode.substring(0, 2).toInt() - 1
+ val endSlot = course.timeCode.substring(course.timeCode.length-2, course.timeCode.length).toInt() - 1
+ for (i in startSlot..endSlot) {
+ scheduleGrid[i][dayIndex] = course
+ }
+ }
+
+ // Add time slots and courses
+ viewModel.timeSlots.forEachIndexed { rowIndex, timeSlot ->
+ val row = TableRow(context)
+ row.addView(createTextView(timeSlot))
+
+ viewModel.days.forEachIndexed { dayIndex, _ ->
+ val course = scheduleGrid[rowIndex][dayIndex]
+ if (course != null) {
+ if(rowIndex>=1&&scheduleGrid[rowIndex-1][dayIndex]==scheduleGrid[rowIndex][dayIndex]){
+ if(rowIndex>=2&&scheduleGrid[rowIndex-2][dayIndex]==scheduleGrid[rowIndex-1][dayIndex])row.addView(createTextView("",
+ isCourse = true, colorInt = generateColorIntForId(course.id),
+ moreInfo = "${course.name} (${course.id})\n地点: ${course.location}\n教师: ${course.teacher}\n节次: ${course.timeCode}\n周次: ${course.weeks}"))
+ else row.addView(createTextView(course.location,
+ isCourse = true, colorInt = generateColorIntForId(course.id),
+ moreInfo = "${course.name} (${course.id})\n地点: ${course.location}\n教师: ${course.teacher}\n节次: ${course.timeCode}\n周次: ${course.weeks}"))
+ }else{
+ row.addView(createTextView(course.name,
+ isCourse = true, colorInt = generateColorIntForId(course.id),
+ moreInfo = "${course.name} (${course.id})\n地点: ${course.location}\n教师: ${course.teacher}\n节次: ${course.timeCode}\n周次: ${course.weeks}"))
+ }
+ } else{
+ //row.addView(createTextView(""))
+ row.addView(Space(context).apply {
+ layoutParams = TableRow.LayoutParams(1, TableRow.LayoutParams.MATCH_PARENT, 1f)
+ })
+ }
+ }
+ table.addView(row)
+ }
+ }
+
+ fun Context.getCustomColor(attr: Int): Int {
+ val typedValue = TypedValue()
+ theme.resolveAttribute(attr, typedValue, true)
+ return typedValue.data
+ }
+
+ private fun createTextView(text: String, isHeader: Boolean = false, isCourse: Boolean = false,colorInt: Int=0,moreInfo:String=""): TextView {
+ return TextView(context).apply {
+ this.text = text
+ textSize = 13f
+ setTextColor(context.getCustomColor(R.attr.textColor))
+ gravity = Gravity.CENTER
+ setPadding(8, 18, 8, 18)
+ ellipsize = TextUtils.TruncateAt.END
+ if (isHeader) {
+ setBackgroundColor(context.getCustomColor(R.attr.header_background))
+ setTypeface(null, android.graphics.Typeface.BOLD)
+ } else if (isCourse) {
+ setBackgroundColor(colorInt)
+ setTextColor(Color.WHITE)
+ } else {
+ setBackgroundColor(context.getCustomColor(R.attr.cell_background))
+ }
+ if(moreInfo!="")setOnClickListener {
+ AlertDialog.Builder(context).apply {
+ setTitle("课程信息")
+ setMessage(moreInfo)
+ setPositiveButton("确定") { dialog, _ ->
+ dialog.dismiss()
+ }
+ create()
+ show()
+ }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentScheduleBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+ return root
+ }
+
+ fun showToast(message: String) {
+ activity?.let { fragmentActivity ->
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ Toast.makeText(fragmentActivity, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ fun processScheduleData(d:String){
+ val res = JSONObject(d)
+ val courses = mutableListOf()
+ val ar=res.getJSONObject("d").getJSONArray("classes")
+ for (i in 0 until ar.length()) {
+ val classObject = ar.getJSONObject(i)
+ courses.add(Course(classObject.getString("course_name"), viewModel.mp[classObject.getString("weekday")].toString(),
+ classObject.getString("lessons"), classObject.getString("location"),
+ classObject.getString("teacher"),classObject.getString("course_id"),classObject.getString("week")))
+ }
+ activity?.let {
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ hideLoading()
+ binding.weekChoice.visibility=View.VISIBLE
+ createSchedule(courses)
+ }
+ }
+ }
+ }
+
+ fun runSchedule(year:String,term:String,week:String){
+ showLoading()
+ binding.scheduleTable.removeAllViews()
+ val cacheKey="$year-$term-$week"
+ //showToast(cacheKey)
+ val cacheManager=ScheduleCacheManager(requireContext())
+ val cachedData = cacheManager.getCachedData(cacheKey)
+ if(cachedData!=null){
+ processScheduleData(cachedData)
+ }else{
+ BjutAPI().getSchedule(account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_SESS].toString()
+ ,year,term,week,object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ val res=response.body?.string().toString()
+ //showToast(res)
+ try {
+ processScheduleData(res)
+ appLogger.e("Info", "Refresh ScheduleData:$cacheKey")
+ cacheManager.cacheData(cacheKey,res)
+ }catch (e: JSONException){
+ showToast("error")
+ appLogger.e("Error", "Try ScheduleInfo error",e)
+ }
+ }
+ })
+ }
+ }
+
+ private var selectedWeek = 1
+
+ private fun createWeeks(weekCount: Int, currentWeek: Int) {
+ val dropdown = binding.weekDropdown
+ val weeks = (1..weekCount).map { "第${it}周" }
+ val adapter = ArrayAdapter(
+ requireContext(),
+ android.R.layout.simple_spinner_dropdown_item,
+ weeks
+ )
+ dropdown.post {
+ dropdown.setAdapter(adapter)
+ dropdown.setText(weeks[currentWeek - 1], false)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel = ViewModelProvider(this)[ScheduleViewModel::class.java]
+
+ showLoading()
+ /*
+ val year="2024-2025"
+ val term="1"
+ val week="11"*/
+
+ selectedWeek = savedInstanceState?.getInt("selected_week", 0) ?: 0
+
+ val cacheManager=ScheduleCacheManager(requireContext())
+ val cachedTermData = cacheManager.getCachedData("term-info")
+
+ var year=""
+ var term=""
+
+ binding.weekDropdown.apply {
+ setOnClickListener {
+ showDropDown()
+ }
+ setOnItemClickListener { _, _, position, _ ->
+ val week = position + 1
+ viewModel.selectedWeek = week
+ runSchedule(year, term, viewModel.selectedWeek.toString())
+ //showToast(week.toString())
+ }
+ }
+ if (viewModel.selectedWeek == null) {
+ if(cachedTermData!=null){
+ val tmp=JSONObject(cachedTermData).getJSONObject("d").getJSONObject("params")
+ year=tmp.getString("year")
+ term=tmp.getString("term")
+ runSchedule(year,term,tmp.getString("week"))
+ createWeeks(tmp.getString("countweek").toInt(),tmp.getString("week").toInt())
+ binding.weekChoice.hint="$year 第 $term 学期:选择周数"
+ }else{
+ BjutAPI().getTermWeek(account_session_util(requireContext()).getUserDetails()[account_session_util.KEY_SESS].toString()
+ ,object :
+ Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ showToast("network error")
+ }
+ override fun onResponse(call: Call, response: Response) {
+ val res_text=response.body?.string().toString()
+ try{
+ val res=JSONObject(res_text)
+ //if(res.get("e")==0)
+ val tmp=res.getJSONObject("d").getJSONObject("params")
+ year=tmp.getString("year")
+ term=tmp.getString("term")
+ viewModel.selectedWeek = tmp.getString("week").toInt()
+ activity?.let {
+ Handler(Looper.getMainLooper()).post {
+ if (isAdded) {
+ runSchedule(year,term, tmp.getString("week"))
+ createWeeks(tmp.getString("countweek").toInt(),tmp.getString("week").toInt())
+ binding.weekChoice.hint="$year 第 $term 学期:选择周数"
+ }
+ }
+ }
+ cacheManager.cacheData("term-info",res_text)
+ }catch (e: JSONException){
+ showToast("error")
+ appLogger.e("Error", "Try TermInfo error",e)
+ }
+ }
+ })
+ }
+ } else {
+ //如果已经选择过周数,使用保存的选择,此时一定有term缓存
+ val tmp=JSONObject(cachedTermData).getJSONObject("d").getJSONObject("params")
+ year=tmp.getString("year")
+ term=tmp.getString("term")
+ runSchedule(year,term, viewModel.selectedWeek.toString())
+ createWeeks(tmp.getString("countweek").toInt(), viewModel.selectedWeek!!)
+ binding.weekChoice.hint="$year 第 $term 学期:选择周数"
+ }
+
+
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hlwdy/bjut/ui/schedule/ScheduleViewModel.kt b/app/src/main/java/com/hlwdy/bjut/ui/schedule/ScheduleViewModel.kt
new file mode 100644
index 0000000..4b16b6b
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/ui/schedule/ScheduleViewModel.kt
@@ -0,0 +1,30 @@
+package com.hlwdy.bjut.ui.schedule
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+
+class ScheduleViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
+ val days = listOf("周一", "周二", "周三", "周四", "周五","周六","周日")
+ val timeSlots = listOf(
+ "8:00-8:45", "8:50-9:35", "9:55-10:40", "10:45-11:30",
+ "13:30-14:15", "14:20-15:05", "15:25-16:10", "16:15-17:00",
+ "18:00-18:45", "18:50-19:35", "19:55-20:40", "20:45-21:30"
+ )
+ var mp=mapOf("1" to "周一", "2" to "周二","3" to "周三","4" to "周四","5" to "周五","6" to "周六","7" to "周日")
+
+ var selectedWeek: Int?
+ get() = savedStateHandle.get("selected_week")
+ set(value) {
+ savedStateHandle["selected_week"] = value
+ }
+}
+
+data class Course(
+ val name: String,
+ val day: String,
+ val timeCode: String, // 例如 "0102" 表示第1-2节课
+ val location: String,
+ var teacher: String,
+ var id: String,
+ var weeks: String
+)
diff --git a/app/src/main/java/com/hlwdy/bjut/vpnOTP.kt b/app/src/main/java/com/hlwdy/bjut/vpnOTP.kt
new file mode 100644
index 0000000..44edbd2
--- /dev/null
+++ b/app/src/main/java/com/hlwdy/bjut/vpnOTP.kt
@@ -0,0 +1,54 @@
+package net.arraynetworks.vpn
+
+import android.os.Handler
+import android.os.Looper
+import com.hlwdy.bjut.BjutHttpRsa
+import com.hlwdy.bjut.HttpUtils
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import org.json.JSONObject
+import java.io.IOException
+
+class NativeLib {
+ init {
+ System.loadLibrary("vpn") // 加载libvpn.so
+ }
+
+ // 声明native方法
+ external fun getOptValue(timestamp: Int, secret: ByteArray, interval: Int): Int
+ external fun encodeOptData(data: ByteArray): ByteArray
+}
+class OTPManager {
+ private var timeOffset: Long = 0 // 时间偏移量(毫秒)
+
+ // 获取校准后的时间戳(秒)
+ private fun getAdjustedTimestamp(): Int {
+ return ((System.currentTimeMillis() + timeOffset) / 1000L).toInt()
+ }
+
+ // 生成OTP
+ fun generateOTP(secret: ByteArray, interval: Int = 60): Int {
+ val timestamp = getAdjustedTimestamp()
+ return try {
+ NativeLib().getOptValue(timestamp, secret, interval)
+ } catch (e: Exception) {
+ -1
+ }
+ }
+
+ // 设置时间偏移
+ fun setTimeOffset(offset: Long) {
+ timeOffset = offset
+ }
+
+ fun registerOTP(usname:String,pwd:String,sm:String,callback: Callback){
+ HttpUtils().addHeader("User-Agent","BjutApp")
+ .addHeader("Cookie","ANStandalone=true;;ANStandalone=true")
+ .addParam("method","otp")
+ .addParam("uname",usname)
+ .addParam("pwd",pwd)
+ .addParam("sm",sm)
+ .post("https://vpn.bjut.edu.cn/prx/000/http/localhost/register",callback)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/baseline_arrow_back_24.xml b/app/src/main/res/drawable/baseline_arrow_back_24.xml
new file mode 100644
index 0000000..075e95d
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_arrow_back_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_credit_card_24.xml b/app/src/main/res/drawable/baseline_credit_card_24.xml
new file mode 100644
index 0000000..cfb6c96
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_credit_card_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml
new file mode 100644
index 0000000..20cb4d6
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_home_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_menu_book_24.xml b/app/src/main/res/drawable/baseline_menu_book_24.xml
new file mode 100644
index 0000000..10d9f76
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_menu_book_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_newspaper_24.xml b/app/src/main/res/drawable/baseline_newspaper_24.xml
new file mode 100644
index 0000000..a1f1836
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_newspaper_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_password_24.xml b/app/src/main/res/drawable/baseline_password_24.xml
new file mode 100644
index 0000000..322ade7
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_password_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_person_24.xml b/app/src/main/res/drawable/baseline_person_24.xml
new file mode 100644
index 0000000..506e0ff
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_person_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_qr_code_24.xml b/app/src/main/res/drawable/baseline_qr_code_24.xml
new file mode 100644
index 0000000..b03f9ae
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_qr_code_24.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_restore_page_24.xml b/app/src/main/res/drawable/baseline_restore_page_24.xml
new file mode 100644
index 0000000..fdcbec2
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_restore_page_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_table_chart_24.xml b/app/src/main/res/drawable/baseline_table_chart_24.xml
new file mode 100644
index 0000000..418f387
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_table_chart_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_vpn_lock_24.xml b/app/src/main/res/drawable/baseline_vpn_lock_24.xml
new file mode 100644
index 0000000..fa4aab0
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_vpn_lock_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bjut.webp b/app/src/main/res/drawable/bjut.webp
new file mode 100644
index 0000000..457c85a
Binary files /dev/null and b/app/src/main/res/drawable/bjut.webp differ
diff --git a/app/src/main/res/drawable/bjut_all.webp b/app/src/main/res/drawable/bjut_all.webp
new file mode 100644
index 0000000..214d957
Binary files /dev/null and b/app/src/main/res/drawable/bjut_all.webp differ
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..b757c5a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..ab065a6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground1.xml b/app/src/main/res/drawable/ic_launcher_foreground1.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground1.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/app/src/main/res/drawable/side_nav_bar.xml
new file mode 100644
index 0000000..18adf03
--- /dev/null
+++ b/app/src/main/res/drawable/side_nav_bar.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
new file mode 100644
index 0000000..e98aa35
--- /dev/null
+++ b/app/src/main/res/layout/activity_about.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_log_view.xml b/app/src/main/res/layout/activity_log_view.xml
new file mode 100644
index 0000000..8c5ff66
--- /dev/null
+++ b/app/src/main/res/layout/activity_log_view.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..6c7dd7c
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_router.xml b/app/src/main/res/layout/activity_router.xml
new file mode 100644
index 0000000..3413146
--- /dev/null
+++ b/app/src/main/res/layout/activity_router.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml
new file mode 100644
index 0000000..b2e0166
--- /dev/null
+++ b/app/src/main/res/layout/app_bar_main.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
new file mode 100644
index 0000000..6e0ea39
--- /dev/null
+++ b/app/src/main/res/layout/content_main.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layout/dialog_loading.xml
new file mode 100644
index 0000000..d82aafe
--- /dev/null
+++ b/app/src/main/res/layout/dialog_loading.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_card.xml b/app/src/main/res/layout/fragment_card.xml
new file mode 100644
index 0000000..fa7c04e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_card.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 0000000..36ab683
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml
new file mode 100644
index 0000000..7784fc8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_library.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_news.xml b/app/src/main/res/layout/fragment_news.xml
new file mode 100644
index 0000000..49ebc44
--- /dev/null
+++ b/app/src/main/res/layout/fragment_news.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_otp.xml b/app/src/main/res/layout/fragment_otp.xml
new file mode 100644
index 0000000..30997a3
--- /dev/null
+++ b/app/src/main/res/layout/fragment_otp.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_schedule.xml b/app/src/main/res/layout/fragment_schedule.xml
new file mode 100644
index 0000000..5d70247
--- /dev/null
+++ b/app/src/main/res/layout/fragment_schedule.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_log.xml b/app/src/main/res/layout/item_log.xml
new file mode 100644
index 0000000..998a598
--- /dev/null
+++ b/app/src/main/res/layout/item_log.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/login_page.xml b/app/src/main/res/layout/login_page.xml
new file mode 100644
index 0000000..cca5fc9
--- /dev/null
+++ b/app/src/main/res/layout/login_page.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml
new file mode 100644
index 0000000..79fc44d
--- /dev/null
+++ b/app/src/main/res/layout/nav_header_main.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/news_detail.xml b/app/src/main/res/layout/news_detail.xml
new file mode 100644
index 0000000..766705b
--- /dev/null
+++ b/app/src/main/res/layout/news_detail.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/news_item_layout.xml b/app/src/main/res/layout/news_item_layout.xml
new file mode 100644
index 0000000..1f08819
--- /dev/null
+++ b/app/src/main/res/layout/news_item_layout.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/webvpn_view.xml b/app/src/main/res/layout/webvpn_view.xml
new file mode 100644
index 0000000..d274eec
--- /dev/null
+++ b/app/src/main/res/layout/webvpn_view.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml
new file mode 100644
index 0000000..7ed5109
--- /dev/null
+++ b/app/src/main/res/menu/activity_main_drawer.xml
@@ -0,0 +1,33 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
new file mode 100644
index 0000000..0340689
--- /dev/null
+++ b/app/src/main/res/menu/main.xml
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..aae98c4
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..66b17e8
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..c1e36a0
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..027cef1
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..e06ce18
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..7fd9fc2
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..6aa3748
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..d261220
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..31b78c2
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..0f59fbf
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
new file mode 100644
index 0000000..7b5ccea
--- /dev/null
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml
new file mode 100644
index 0000000..22d7f00
--- /dev/null
+++ b/app/src/main/res/values-land/dimens.xml
@@ -0,0 +1,3 @@
+
+ 48dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..692f0ec
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml
new file mode 100644
index 0000000..d73f4a3
--- /dev/null
+++ b/app/src/main/res/values-w1240dp/dimens.xml
@@ -0,0 +1,3 @@
+
+ 200dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml
new file mode 100644
index 0000000..22d7f00
--- /dev/null
+++ b/app/src/main/res/values-w600dp/dimens.xml
@@ -0,0 +1,3 @@
+
+ 48dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..24f3552
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c039adc
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,14 @@
+
+
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+ #0096c3
+
+ #EEEEEE
+ #FFFFFF
+
+ #202020
+ #262626
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..4ab4520
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+
+ 16dp
+ 16dp
+ 8dp
+ 176dp
+ 16dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1b58ce3
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,23 @@
+
+ bjut
+ Open navigation drawer
+ Close navigation drawer
+ name
+ 24
+ Navigation header
+ Settings
+
+ 首页
+ 通知公告
+ 校园一卡通
+ 图书馆
+ 我的课表
+ VPN OTP
+
+ 一卡通付款码
+ 课程表
+ VPN OTP
+
+ 本APP是非官方的,第三方免费开源版本,功能实现完全依赖官方接口,个人数据仅保存在本地。目标是集成所有有用的功能。
+ https://github.com/bjutapp/bjut
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..a3e14de
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 0000000..f9c14b7
--- /dev/null
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/hlwdy/bjut/ExampleUnitTest.kt b/app/src/test/java/com/hlwdy/bjut/ExampleUnitTest.kt
new file mode 100644
index 0000000..b6e5fc3
--- /dev/null
+++ b/app/src/test/java/com/hlwdy/bjut/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.hlwdy.bjut
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/bjut.svg b/bjut.svg
new file mode 100644
index 0000000..24ab119
--- /dev/null
+++ b/bjut.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bjuticon.svg b/bjuticon.svg
new file mode 100644
index 0000000..81dcf3f
--- /dev/null
+++ b/bjuticon.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..922f551
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..dbca3e0
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,34 @@
+[versions]
+agp = "8.6.0"
+kotlin = "1.9.0"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.6.1"
+material = "1.10.0"
+constraintlayout = "2.1.4"
+lifecycleLivedataKtx = "2.6.1"
+lifecycleViewmodelKtx = "2.6.1"
+navigationFragmentKtx = "2.6.0"
+navigationUiKtx = "2.6.0"
+gridlayout = "1.0.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
+androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
+androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
+androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
+androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..58af846
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Oct 19 14:00:59 HKT 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/img/1.jpg b/img/1.jpg
new file mode 100644
index 0000000..cd12ce7
Binary files /dev/null and b/img/1.jpg differ
diff --git a/img/2.jpg b/img/2.jpg
new file mode 100644
index 0000000..f32d6a1
Binary files /dev/null and b/img/2.jpg differ
diff --git a/img/3.jpg b/img/3.jpg
new file mode 100644
index 0000000..d4e810c
Binary files /dev/null and b/img/3.jpg differ
diff --git a/img/4.jpg b/img/4.jpg
new file mode 100644
index 0000000..08732ce
Binary files /dev/null and b/img/4.jpg differ
diff --git a/img/5.jpg b/img/5.jpg
new file mode 100644
index 0000000..1338162
Binary files /dev/null and b/img/5.jpg differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..7eb5505
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "bjut"
+include(":app")
+
\ No newline at end of file