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 @@ + + + + + + + + + + + + + + + + + +