init: 河南女子职业学院智慧学工考勤签到 Android 应用
功能包含:学生登录、微信授权、自定义GPS签到、预设管理、历史记录 开发者:凡笙 Made-with: Cursor
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
/app/build
|
||||
/app/release
|
||||
*.apk
|
||||
*.aab
|
||||
*.keystore
|
||||
*.jks
|
||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# GPSPunchApp
|
||||
|
||||
河南女子职业学院智慧学工考勤签到 Android 应用
|
||||
|
||||
## 功能
|
||||
|
||||
- 学生身份登录(姓名 + 学号 + 验证码)
|
||||
- 微信扫码授权登录
|
||||
- 登录状态持久化,下次打开无需重复登录
|
||||
- 自定义 GPS 经纬度进行考勤签到
|
||||
- GPS 预设位置管理(添加 / 删除)
|
||||
- 课程列表与打卡记录查看
|
||||
- 签到历史记录本地保存
|
||||
- 退出登录
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Kotlin
|
||||
- OkHttp3(网络请求 + Cookie 管理)
|
||||
- Jsoup(HTML 解析)
|
||||
- Material3(UI 组件)
|
||||
- SharedPreferences(本地持久化)
|
||||
- Coroutines(异步任务)
|
||||
|
||||
## 构建
|
||||
|
||||
使用 Android Studio 打开 `GPSPunchApp` 目录,或命令行执行:
|
||||
|
||||
```
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
生成的 APK 位于 `app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
## 声明
|
||||
|
||||
本项目仅供参考学习使用,未加入任何第三方接口,未收集用户个人信息。
|
||||
|
||||
开发者:凡笙
|
||||
48
app/build.gradle
Normal file
48
app/build.gradle
Normal file
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.gpspunch.app'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.gpspunch.app"
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'org.jsoup:jsoup:1.17.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
}
|
||||
7
app/proguard-rules.pro
vendored
Normal file
7
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# OkHttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-keep class okhttp3.** { *; }
|
||||
|
||||
# Jsoup
|
||||
-keep class org.jsoup.** { *; }
|
||||
28
app/src/main/AndroidManifest.xml
Normal file
28
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GPSPunch"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
521
app/src/main/java/com/gpspunch/app/MainActivity.kt
Normal file
521
app/src/main/java/com/gpspunch/app/MainActivity.kt
Normal file
@@ -0,0 +1,521 @@
|
||||
package com.gpspunch.app
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.gpspunch.app.databinding.ActivityMainBinding
|
||||
import kotlinx.coroutines.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val api = PunchApi()
|
||||
private lateinit var prefs: PrefsManager
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
prefs = PrefsManager(this)
|
||||
|
||||
setupLogin()
|
||||
setupPunch()
|
||||
setupGps()
|
||||
setupHistory()
|
||||
setupNav()
|
||||
showDisclaimerIfNeeded()
|
||||
}
|
||||
|
||||
private fun showDisclaimerIfNeeded() {
|
||||
if (prefs.isDisclaimerAccepted()) {
|
||||
checkSavedLogin()
|
||||
return
|
||||
}
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("安全提示")
|
||||
.setMessage(
|
||||
"本应用仅供参考学习使用。\n\n" +
|
||||
"如果您使用本应用的相关功能,即代表您自愿放弃河南女子职业学院相关规章制度所赋予的权益," +
|
||||
"并自行承担一切后果。\n\n" +
|
||||
"本 App 未加入任何第三方接口,也未收集您的任何个人信息。\n" +
|
||||
"全部使用的接口如下:\n" +
|
||||
"• https://wx703c2206450720dd.g8n.cn\n" +
|
||||
"• https://login.b8n.cn\n\n" +
|
||||
"开发者:凡笙\n\n" +
|
||||
"请确认您已知晓以上内容。"
|
||||
)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("我同意并知晓") { _, _ ->
|
||||
prefs.setDisclaimerAccepted()
|
||||
checkSavedLogin()
|
||||
}
|
||||
.setNegativeButton("我拒绝") { _, _ ->
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
// ── Navigation ──
|
||||
|
||||
private fun setupNav() {
|
||||
binding.bottomNav.setOnItemSelectedListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.nav_punch -> showView(binding.punchView)
|
||||
R.id.nav_gps -> showView(binding.gpsView)
|
||||
R.id.nav_history -> {
|
||||
showView(binding.historyView)
|
||||
renderHistory()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun showView(target: View) {
|
||||
binding.punchView.visibility = View.GONE
|
||||
binding.gpsView.visibility = View.GONE
|
||||
binding.historyView.visibility = View.GONE
|
||||
target.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showLogin() {
|
||||
binding.loginView.visibility = View.VISIBLE
|
||||
binding.guideView.visibility = View.GONE
|
||||
binding.mainContent.visibility = View.GONE
|
||||
binding.bottomNav.visibility = View.GONE
|
||||
binding.headerUserBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showGuide(qrUrl: String) {
|
||||
binding.loginView.visibility = View.GONE
|
||||
binding.guideView.visibility = View.VISIBLE
|
||||
binding.mainContent.visibility = View.GONE
|
||||
binding.bottomNav.visibility = View.GONE
|
||||
binding.headerUserBar.visibility = View.GONE
|
||||
binding.tvWeixinLink.text = qrUrl
|
||||
}
|
||||
|
||||
private fun showApp() {
|
||||
binding.loginView.visibility = View.GONE
|
||||
binding.guideView.visibility = View.GONE
|
||||
binding.mainContent.visibility = View.VISIBLE
|
||||
binding.bottomNav.visibility = View.VISIBLE
|
||||
binding.headerUserBar.visibility = View.VISIBLE
|
||||
binding.bottomNav.selectedItemId = R.id.nav_punch
|
||||
showView(binding.punchView)
|
||||
loadGpsPresets()
|
||||
loadCurrentGps()
|
||||
}
|
||||
|
||||
// ── Login ──
|
||||
|
||||
private var pollingJob: Job? = null
|
||||
private var captchaInited = false
|
||||
|
||||
private fun setupLogin() {
|
||||
binding.captchaImg.setOnClickListener { refreshCaptcha() }
|
||||
binding.btnLogin.setOnClickListener { doLogin() }
|
||||
binding.btnCookieLogin.setOnClickListener { doCookieLogin() }
|
||||
binding.btnCopyLink.setOnClickListener { copyWeixinLink() }
|
||||
binding.btnBackToLogin.setOnClickListener {
|
||||
pollingJob?.cancel()
|
||||
showLogin()
|
||||
refreshCaptcha()
|
||||
}
|
||||
refreshCaptcha()
|
||||
}
|
||||
|
||||
private fun checkSavedLogin() {
|
||||
val s = prefs.getCookieS()
|
||||
val remember = prefs.getCookieRemember()
|
||||
if (s.isNotEmpty()) {
|
||||
scope.launch {
|
||||
val r = withContext(Dispatchers.IO) { api.cookieLogin(s, remember) }
|
||||
if (r.ok) {
|
||||
binding.tvUser.text = r.userName
|
||||
prefs.saveUser(r.userName)
|
||||
showApp()
|
||||
} else {
|
||||
showLogin()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showLogin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshCaptcha() {
|
||||
scope.launch {
|
||||
try {
|
||||
val bytes = withContext(Dispatchers.IO) {
|
||||
if (captchaInited) api.refreshCaptchaOnly() else api.getCaptchaBytes()
|
||||
}
|
||||
captchaInited = true
|
||||
val bmp = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
if (bmp != null) binding.captchaImg.setImageBitmap(bmp)
|
||||
} catch (_: Exception) {
|
||||
toast("验证码加载失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLogin() {
|
||||
val name = binding.etName.text.toString().trim()
|
||||
val no = binding.etNo.text.toString().trim()
|
||||
val code = binding.etCode.text.toString().trim()
|
||||
|
||||
if (name.isEmpty() || no.isEmpty() || code.isEmpty()) {
|
||||
toast("请填写完整信息")
|
||||
return
|
||||
}
|
||||
|
||||
binding.btnLogin.isEnabled = false
|
||||
binding.btnLogin.text = "登录中…"
|
||||
|
||||
scope.launch {
|
||||
val r = withContext(Dispatchers.IO) { api.loginAndGetQr(name, no, code) }
|
||||
binding.btnLogin.isEnabled = true
|
||||
binding.btnLogin.text = "登 录"
|
||||
|
||||
if (r.ok) {
|
||||
toast("请复制链接发送到微信")
|
||||
showGuide(r.qrUrl)
|
||||
startPolling()
|
||||
} else {
|
||||
toast(r.msg)
|
||||
refreshCaptcha()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyWeixinLink() {
|
||||
val link = binding.tvWeixinLink.text.toString()
|
||||
if (link.isEmpty()) return
|
||||
val cm = getSystemService(CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
cm.setPrimaryClip(android.content.ClipData.newPlainText("login_link", link))
|
||||
toast("已复制!请发送到微信「文件传输助手」并打开")
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
pollingJob?.cancel()
|
||||
binding.tvPollingStatus.visibility = View.VISIBLE
|
||||
binding.tvPollingStatus.text = "等待微信确认登录…"
|
||||
|
||||
pollingJob = scope.launch {
|
||||
var attempts = 0
|
||||
while (attempts < 30) {
|
||||
delay(2000)
|
||||
attempts++
|
||||
binding.tvPollingStatus.text = "等待微信确认登录… ($attempts)"
|
||||
|
||||
try {
|
||||
val r = withContext(Dispatchers.IO) { api.checkWeixinLogin() }
|
||||
if (r.ok) {
|
||||
binding.tvPollingStatus.text = "登录成功,正在跳转…"
|
||||
val loginResult = withContext(Dispatchers.IO) {
|
||||
api.completeWeixinLogin(r.redirectUrl)
|
||||
}
|
||||
if (loginResult.ok) {
|
||||
toast("登录成功")
|
||||
prefs.saveCookies(api.getSavedCookieS(), api.getSavedCookieRemember())
|
||||
prefs.saveUser(loginResult.userName)
|
||||
binding.tvUser.text = loginResult.userName
|
||||
showApp()
|
||||
} else {
|
||||
toast("登录跳转失败: ${loginResult.msg}")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
binding.tvPollingStatus.text = "链接已过期,请重新登录"
|
||||
toast("登录超时")
|
||||
}
|
||||
}
|
||||
|
||||
private fun doCookieLogin() {
|
||||
val s = binding.etCookieS.text.toString().trim()
|
||||
if (s.isEmpty()) {
|
||||
toast("请填写 s cookie")
|
||||
return
|
||||
}
|
||||
val remember = binding.etCookieRemember.text.toString().trim()
|
||||
|
||||
binding.btnCookieLogin.isEnabled = false
|
||||
scope.launch {
|
||||
val r = withContext(Dispatchers.IO) { api.cookieLogin(s, remember) }
|
||||
binding.btnCookieLogin.isEnabled = true
|
||||
|
||||
if (r.ok) {
|
||||
toast("登录成功")
|
||||
prefs.saveCookies(s, remember)
|
||||
prefs.saveUser(r.userName)
|
||||
binding.tvUser.text = r.userName
|
||||
showApp()
|
||||
} else {
|
||||
toast(r.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Punch ──
|
||||
|
||||
private fun setupPunch() {
|
||||
binding.btnRefresh.setOnClickListener { refreshPunchs() }
|
||||
binding.btnLogout.setOnClickListener { doLogout() }
|
||||
}
|
||||
|
||||
private fun doLogout() {
|
||||
pollingJob?.cancel()
|
||||
api.clearCookies()
|
||||
prefs.clearLogin()
|
||||
captchaInited = false
|
||||
showLogin()
|
||||
refreshCaptcha()
|
||||
toast("已退出")
|
||||
}
|
||||
|
||||
private fun refreshPunchs() {
|
||||
binding.punchListContainer.removeAllViews()
|
||||
addLoadingView(binding.punchListContainer)
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val courses = withContext(Dispatchers.IO) { api.getCourses() }
|
||||
binding.punchListContainer.removeAllViews()
|
||||
|
||||
if (courses.isEmpty()) {
|
||||
addEmptyView(binding.punchListContainer, "未找到课程")
|
||||
return@launch
|
||||
}
|
||||
|
||||
var hasPunch = false
|
||||
for (course in courses) {
|
||||
val punchs = withContext(Dispatchers.IO) { api.getPunchs(course.id) }
|
||||
if (punchs.isEmpty()) continue
|
||||
hasPunch = true
|
||||
for (p in punchs) {
|
||||
addPunchCard(binding.punchListContainer, course, p)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPunch) {
|
||||
addEmptyView(binding.punchListContainer, "暂无进行中的签到")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
binding.punchListContainer.removeAllViews()
|
||||
addEmptyView(binding.punchListContainer, "加载失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addPunchCard(container: ViewGroup, course: CourseInfo, punch: PunchInfo) {
|
||||
val card = LayoutInflater.from(this).inflate(R.layout.item_punch, container, false)
|
||||
card.findViewById<TextView>(R.id.tvPunchTitle).text = punch.title
|
||||
card.findViewById<TextView>(R.id.tvPunchMeta).text = "${course.title} · ${punch.meta}"
|
||||
|
||||
val tvStatus = card.findViewById<TextView>(R.id.tvPunchStatus)
|
||||
tvStatus.text = punch.status
|
||||
when {
|
||||
"已签" in punch.status || "已结束" in punch.status -> {
|
||||
tvStatus.setBackgroundResource(R.drawable.bg_pill_success)
|
||||
tvStatus.setTextColor(Color.parseColor("#21b26f"))
|
||||
}
|
||||
"进行" in punch.status || "已开始" in punch.status -> {
|
||||
tvStatus.setBackgroundResource(R.drawable.bg_pill_primary)
|
||||
tvStatus.setTextColor(Color.parseColor("#4f6ef7"))
|
||||
}
|
||||
else -> {
|
||||
tvStatus.setBackgroundResource(R.drawable.bg_pill_muted)
|
||||
tvStatus.setTextColor(Color.parseColor("#8a94a6"))
|
||||
}
|
||||
}
|
||||
|
||||
card.setOnClickListener {
|
||||
if (punch.id.isNullOrEmpty()) {
|
||||
toast("签到未开始或无法获取 ID")
|
||||
return@setOnClickListener
|
||||
}
|
||||
confirmPunch(course.id, punch.id, punch.title)
|
||||
}
|
||||
|
||||
container.addView(card)
|
||||
}
|
||||
|
||||
private fun confirmPunch(courseId: String, punchId: String, title: String) {
|
||||
val lat = binding.etLat.text.toString().toDoubleOrNull()
|
||||
val lng = binding.etLng.text.toString().toDoubleOrNull()
|
||||
|
||||
if (lat == null || lng == null) {
|
||||
toast("请先设置 GPS 坐标")
|
||||
return
|
||||
}
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("确认签到")
|
||||
.setMessage("签到「$title」\n坐标: ($lat, $lng)")
|
||||
.setPositiveButton("签到") { _, _ ->
|
||||
executePunch(courseId, punchId, title, lat, lng)
|
||||
}
|
||||
.setNegativeButton("取消", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun executePunch(courseId: String, punchId: String, title: String, lat: Double, lng: Double) {
|
||||
scope.launch {
|
||||
try {
|
||||
val r = withContext(Dispatchers.IO) { api.doPunch(courseId, punchId, lat, lng) }
|
||||
if (r.ok) {
|
||||
toast("✓ ${r.msg}")
|
||||
} else {
|
||||
toast("✗ ${r.msg}")
|
||||
}
|
||||
val now = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(Date())
|
||||
prefs.addHistory(PunchRecord(title, lat, lng, r.ok, r.msg, now))
|
||||
refreshPunchs()
|
||||
} catch (e: Exception) {
|
||||
toast("网络错误: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── GPS Settings ──
|
||||
|
||||
private fun setupGps() {
|
||||
binding.btnAddPreset.setOnClickListener { addGpsPreset() }
|
||||
}
|
||||
|
||||
private fun loadCurrentGps() {
|
||||
val presets = prefs.getPresets()
|
||||
if (presets.isNotEmpty()) {
|
||||
binding.etLat.setText(presets[0].lat.toString())
|
||||
binding.etLng.setText(presets[0].lng.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadGpsPresets() {
|
||||
val presets = prefs.getPresets()
|
||||
|
||||
// Chips in punch view
|
||||
binding.chipGroupPresets.removeAllViews()
|
||||
for ((i, p) in presets.withIndex()) {
|
||||
val chip = Chip(this).apply {
|
||||
text = p.name
|
||||
isCheckable = true
|
||||
setOnClickListener {
|
||||
binding.etLat.setText(p.lat.toString())
|
||||
binding.etLng.setText(p.lng.toString())
|
||||
toast("已切换: ${p.name}")
|
||||
}
|
||||
}
|
||||
binding.chipGroupPresets.addView(chip)
|
||||
}
|
||||
|
||||
// List in GPS view
|
||||
binding.presetListContainer.removeAllViews()
|
||||
for ((i, p) in presets.withIndex()) {
|
||||
val row = LayoutInflater.from(this).inflate(R.layout.item_preset, binding.presetListContainer, false)
|
||||
row.findViewById<TextView>(R.id.tvPresetName).text = p.name
|
||||
row.findViewById<TextView>(R.id.tvPresetCoord).text = "${p.lat}, ${p.lng}"
|
||||
row.findViewById<View>(R.id.btnDeletePreset).setOnClickListener {
|
||||
prefs.removePreset(i)
|
||||
loadGpsPresets()
|
||||
toast("已删除")
|
||||
}
|
||||
binding.presetListContainer.addView(row)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGpsPreset() {
|
||||
val name = binding.etPresetName.text.toString().trim()
|
||||
val lat = binding.etPresetLat.text.toString().toDoubleOrNull()
|
||||
val lng = binding.etPresetLng.text.toString().toDoubleOrNull()
|
||||
|
||||
if (name.isEmpty()) { toast("请输入位置名称"); return }
|
||||
if (lat == null || lng == null) { toast("请输入有效坐标"); return }
|
||||
|
||||
prefs.addPreset(GpsPreset(name, lat, lng))
|
||||
loadGpsPresets()
|
||||
binding.etPresetName.text?.clear()
|
||||
binding.etPresetLat.text?.clear()
|
||||
binding.etPresetLng.text?.clear()
|
||||
toast("位置已保存")
|
||||
}
|
||||
|
||||
// ── History ──
|
||||
|
||||
private fun setupHistory() {}
|
||||
|
||||
private fun renderHistory() {
|
||||
binding.historyListContainer.removeAllViews()
|
||||
val list = prefs.getHistory()
|
||||
if (list.isEmpty()) {
|
||||
addEmptyView(binding.historyListContainer, "暂无签到记录")
|
||||
return
|
||||
}
|
||||
for (r in list) {
|
||||
val card = LayoutInflater.from(this).inflate(R.layout.item_history, binding.historyListContainer, false)
|
||||
card.findViewById<TextView>(R.id.tvHistoryTitle).text = r.title
|
||||
card.findViewById<TextView>(R.id.tvHistoryTime).text = "${r.time} · (${r.lat}, ${r.lng})"
|
||||
val tvResult = card.findViewById<TextView>(R.id.tvHistoryResult)
|
||||
tvResult.text = r.msg
|
||||
if (r.ok) {
|
||||
tvResult.setBackgroundResource(R.drawable.bg_pill_success)
|
||||
tvResult.setTextColor(Color.parseColor("#21b26f"))
|
||||
} else {
|
||||
tvResult.setBackgroundResource(R.drawable.bg_pill_danger)
|
||||
tvResult.setTextColor(Color.parseColor("#ff4d4f"))
|
||||
}
|
||||
binding.historyListContainer.addView(card)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private fun toast(msg: String) {
|
||||
Snackbar.make(binding.root, msg, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun addLoadingView(container: ViewGroup) {
|
||||
val pb = ProgressBar(this).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
if (this is LinearLayout.LayoutParams) gravity = android.view.Gravity.CENTER_HORIZONTAL
|
||||
}
|
||||
}
|
||||
val wrapper = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = android.view.Gravity.CENTER
|
||||
setPadding(0, 64, 0, 64)
|
||||
addView(pb)
|
||||
}
|
||||
container.addView(wrapper)
|
||||
}
|
||||
|
||||
private fun addEmptyView(container: ViewGroup, text: String) {
|
||||
val tv = TextView(this).apply {
|
||||
this.text = text
|
||||
setTextColor(Color.parseColor("#8a94a6"))
|
||||
textSize = 14f
|
||||
gravity = android.view.Gravity.CENTER
|
||||
setPadding(32, 64, 32, 64)
|
||||
}
|
||||
container.addView(tv)
|
||||
}
|
||||
}
|
||||
109
app/src/main/java/com/gpspunch/app/PrefsManager.kt
Normal file
109
app/src/main/java/com/gpspunch/app/PrefsManager.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package com.gpspunch.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
data class GpsPreset(val name: String, val lat: Double, val lng: Double)
|
||||
data class PunchRecord(
|
||||
val title: String, val lat: Double, val lng: Double,
|
||||
val ok: Boolean, val msg: String, val time: String
|
||||
)
|
||||
|
||||
class PrefsManager(context: Context) {
|
||||
|
||||
private val prefs: SharedPreferences =
|
||||
context.getSharedPreferences("gps_punch", Context.MODE_PRIVATE)
|
||||
|
||||
fun saveCookies(s: String, remember: String) {
|
||||
prefs.edit().putString("cookie_s", s).putString("cookie_remember", remember).apply()
|
||||
}
|
||||
|
||||
fun getCookieS(): String = prefs.getString("cookie_s", "") ?: ""
|
||||
fun getCookieRemember(): String = prefs.getString("cookie_remember", "") ?: ""
|
||||
|
||||
fun saveUser(name: String) {
|
||||
prefs.edit().putString("user_name", name).apply()
|
||||
}
|
||||
|
||||
fun getUserName(): String = prefs.getString("user_name", "") ?: ""
|
||||
|
||||
fun clearLogin() {
|
||||
prefs.edit()
|
||||
.remove("cookie_s").remove("cookie_remember").remove("user_name")
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun isDisclaimerAccepted(): Boolean = prefs.getBoolean("disclaimer_accepted", false)
|
||||
|
||||
fun setDisclaimerAccepted() {
|
||||
prefs.edit().putBoolean("disclaimer_accepted", true).apply()
|
||||
}
|
||||
|
||||
fun getPresets(): MutableList<GpsPreset> {
|
||||
val json = prefs.getString("gps_presets", null) ?: return mutableListOf(
|
||||
GpsPreset("默认位置", 0.0, 0.0)
|
||||
)
|
||||
val arr = JSONArray(json)
|
||||
val list = mutableListOf<GpsPreset>()
|
||||
for (i in 0 until arr.length()) {
|
||||
val o = arr.getJSONObject(i)
|
||||
list.add(GpsPreset(o.getString("name"), o.getDouble("lat"), o.getDouble("lng")))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun savePresets(list: List<GpsPreset>) {
|
||||
val arr = JSONArray()
|
||||
for (p in list) {
|
||||
arr.put(JSONObject().put("name", p.name).put("lat", p.lat).put("lng", p.lng))
|
||||
}
|
||||
prefs.edit().putString("gps_presets", arr.toString()).apply()
|
||||
}
|
||||
|
||||
fun addPreset(preset: GpsPreset) {
|
||||
val list = getPresets()
|
||||
list.add(preset)
|
||||
savePresets(list)
|
||||
}
|
||||
|
||||
fun removePreset(index: Int) {
|
||||
val list = getPresets()
|
||||
if (index in list.indices) {
|
||||
list.removeAt(index)
|
||||
savePresets(list)
|
||||
}
|
||||
}
|
||||
|
||||
fun getHistory(): MutableList<PunchRecord> {
|
||||
val json = prefs.getString("punch_history", null) ?: return mutableListOf()
|
||||
val arr = JSONArray(json)
|
||||
val list = mutableListOf<PunchRecord>()
|
||||
for (i in 0 until arr.length()) {
|
||||
val o = arr.getJSONObject(i)
|
||||
list.add(
|
||||
PunchRecord(
|
||||
o.getString("title"), o.getDouble("lat"), o.getDouble("lng"),
|
||||
o.getBoolean("ok"), o.getString("msg"), o.getString("time")
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun addHistory(record: PunchRecord) {
|
||||
val list = getHistory()
|
||||
list.add(0, record)
|
||||
if (list.size > 50) list.subList(50, list.size).clear()
|
||||
val arr = JSONArray()
|
||||
for (r in list) {
|
||||
arr.put(
|
||||
JSONObject()
|
||||
.put("title", r.title).put("lat", r.lat).put("lng", r.lng)
|
||||
.put("ok", r.ok).put("msg", r.msg).put("time", r.time)
|
||||
)
|
||||
}
|
||||
prefs.edit().putString("punch_history", arr.toString()).apply()
|
||||
}
|
||||
}
|
||||
390
app/src/main/java/com/gpspunch/app/PunchApi.kt
Normal file
390
app/src/main/java/com/gpspunch/app/PunchApi.kt
Normal file
@@ -0,0 +1,390 @@
|
||||
package com.gpspunch.app
|
||||
|
||||
import okhttp3.*
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class LoginResult(val ok: Boolean, val msg: String, val userName: String = "")
|
||||
data class CourseInfo(val id: String, val title: String)
|
||||
data class PunchInfo(
|
||||
val id: String?,
|
||||
val title: String,
|
||||
val status: String,
|
||||
val meta: String,
|
||||
val needsGps: Boolean
|
||||
)
|
||||
data class PunchResult(val ok: Boolean, val msg: String)
|
||||
|
||||
class PunchApi {
|
||||
|
||||
companion object {
|
||||
private const val APP_DOMAIN = "wx703c2206450720dd.g8n.cn"
|
||||
private const val BASE_URL = "https://$APP_DOMAIN"
|
||||
private const val LOGIN_DOMAIN = "login.b8n.cn"
|
||||
private const val LOGIN_URL = "https://$LOGIN_DOMAIN"
|
||||
private const val SCHOOL_ID = 45
|
||||
private const val UA =
|
||||
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/145.0.0.0 Mobile Safari/537.36"
|
||||
}
|
||||
|
||||
private val cookieStore = ConcurrentHashMap<String, MutableList<Cookie>>()
|
||||
|
||||
private val cookieJar = object : CookieJar {
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val host = url.host
|
||||
cookieStore.getOrPut(host) { mutableListOf() }.apply {
|
||||
for (c in cookies) {
|
||||
removeAll { it.name == c.name }
|
||||
add(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return cookieStore[url.host] ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.cookieJar(cookieJar)
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.build()
|
||||
|
||||
private val clientNoRedirect = OkHttpClient.Builder()
|
||||
.cookieJar(cookieJar)
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(false)
|
||||
.build()
|
||||
|
||||
var currentUser: String = ""
|
||||
private set
|
||||
|
||||
private fun getUid(): String? {
|
||||
val allCookies = cookieStore[APP_DOMAIN] ?: return null
|
||||
for (c in allCookies) {
|
||||
val m = Regex("(\\d+)%7C").find(c.value)
|
||||
if (m != null) return m.groupValues[1]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
data class WeixinQrResult(val ok: Boolean, val qrUrl: String = "", val msg: String = "")
|
||||
data class CheckLoginResult(val ok: Boolean, val redirectUrl: String = "")
|
||||
|
||||
fun initLoginSession() {
|
||||
val req = Request.Builder()
|
||||
.url("$BASE_URL/student/login")
|
||||
.header("User-Agent", UA)
|
||||
.header("Accept", "text/html,*/*")
|
||||
.build()
|
||||
client.newCall(req).execute().close()
|
||||
android.util.Log.d("PunchApi", "login session init done")
|
||||
}
|
||||
|
||||
fun getCaptchaBytes(): ByteArray {
|
||||
initLoginSession()
|
||||
val req = Request.Builder()
|
||||
.url("$LOGIN_URL/captcha/default?t=${System.currentTimeMillis()}")
|
||||
.header("User-Agent", UA)
|
||||
.header("Referer", "$LOGIN_URL/fields/login/student/$SCHOOL_ID")
|
||||
.build()
|
||||
client.newCall(req).execute().use { return it.body?.bytes() ?: ByteArray(0) }
|
||||
}
|
||||
|
||||
fun refreshCaptchaOnly(): ByteArray {
|
||||
val req = Request.Builder()
|
||||
.url("$LOGIN_URL/captcha/default?t=${System.currentTimeMillis()}")
|
||||
.header("User-Agent", UA)
|
||||
.header("Referer", "$LOGIN_URL/fields/login/student/$SCHOOL_ID")
|
||||
.build()
|
||||
client.newCall(req).execute().use { return it.body?.bytes() ?: ByteArray(0) }
|
||||
}
|
||||
|
||||
fun loginAndGetQr(name: String, no: String, checkcode: String): WeixinQrResult {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("name", name)
|
||||
.add("no", no)
|
||||
.add("checkcode", checkcode)
|
||||
.build()
|
||||
|
||||
val req = Request.Builder()
|
||||
.url("$LOGIN_URL/fields/login/student/$SCHOOL_ID")
|
||||
.header("User-Agent", UA)
|
||||
.header("Referer", "$LOGIN_URL/fields/login/student/$SCHOOL_ID")
|
||||
.header("Origin", LOGIN_URL)
|
||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
.header("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
.header("Upgrade-Insecure-Requests", "1")
|
||||
.post(formBody)
|
||||
.build()
|
||||
|
||||
val resp = client.newCall(req).execute()
|
||||
val body = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
|
||||
android.util.Log.d("PunchApi", "login POST → HTTP ${resp.code}, title match=${("<title>stop</title>" in body)}")
|
||||
|
||||
if ("验证码" in body && ("错误" in body || "不正确" in body))
|
||||
return WeixinQrResult(false, msg = "验证码错误,请刷新重试")
|
||||
if ("不匹配" in body)
|
||||
return WeixinQrResult(false, msg = "姓名与学号不匹配")
|
||||
if ("不存在" in body)
|
||||
return WeixinQrResult(false, msg = "学号不存在")
|
||||
|
||||
if ("<title>stop</title>" in body || "选择其他登录方式" in body || "/qr/weixin/" in body) {
|
||||
android.util.Log.d("PunchApi", "got stop page, fetching weixin QR...")
|
||||
return fetchWeixinQr()
|
||||
}
|
||||
|
||||
val doc = Jsoup.parse(body)
|
||||
val err = doc.selectFirst(".alert-danger, .text-danger, .error, .invalid-feedback")
|
||||
return WeixinQrResult(false, msg = err?.text()?.ifEmpty { "登录失败" } ?: "登录失败")
|
||||
}
|
||||
|
||||
private fun fetchWeixinQr(): WeixinQrResult {
|
||||
val qrReq = Request.Builder()
|
||||
.url("$LOGIN_URL/qr/weixin/student/$SCHOOL_ID")
|
||||
.header("User-Agent", UA)
|
||||
.header("Accept", "text/html,*/*")
|
||||
.header("Referer", "$LOGIN_URL/fields/login/student/$SCHOOL_ID")
|
||||
.build()
|
||||
val resp = client.newCall(qrReq).execute()
|
||||
val body = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
|
||||
val m = Regex("""var\s+qrurl\s*=\s*"([^"]+)"""").find(body)
|
||||
if (m != null) {
|
||||
val url = m.groupValues[1]
|
||||
android.util.Log.d("PunchApi", "got qrurl: $url")
|
||||
return WeixinQrResult(true, url)
|
||||
}
|
||||
|
||||
android.util.Log.d("PunchApi", "qr page fail: ${body.take(400)}")
|
||||
return WeixinQrResult(false, msg = "无法获取微信登录链接")
|
||||
}
|
||||
|
||||
fun checkWeixinLogin(): CheckLoginResult {
|
||||
val req = Request.Builder()
|
||||
.url("$LOGIN_URL/qr/weixin/student/$SCHOOL_ID?op=checklogin")
|
||||
.header("User-Agent", UA)
|
||||
.header("Accept", "application/json, */*")
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.header("Referer", "$LOGIN_URL/qr/weixin/student/$SCHOOL_ID")
|
||||
.build()
|
||||
val resp = client.newCall(req).execute()
|
||||
val body = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
|
||||
android.util.Log.d("PunchApi", "checklogin: $body")
|
||||
|
||||
if ("\"status\":true" in body || "\"status\": true" in body) {
|
||||
val um = Regex("""\"url\"\s*:\s*\"([^\"]+)\"""").find(body)
|
||||
val redirectUrl = um?.groupValues?.get(1) ?: ""
|
||||
return CheckLoginResult(true, redirectUrl)
|
||||
}
|
||||
return CheckLoginResult(false)
|
||||
}
|
||||
|
||||
fun completeWeixinLogin(redirectUrl: String): LoginResult {
|
||||
val cleaned = redirectUrl.replace("\\/", "/")
|
||||
val fullUrl = if (cleaned.startsWith("http")) cleaned
|
||||
else "$LOGIN_URL$cleaned"
|
||||
android.util.Log.d("PunchApi", "completing login, redirect: $fullUrl")
|
||||
|
||||
val req = Request.Builder()
|
||||
.url(fullUrl)
|
||||
.header("User-Agent", UA)
|
||||
.header("Accept", "text/html,*/*")
|
||||
.build()
|
||||
val resp = client.newCall(req).execute()
|
||||
val body = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
android.util.Log.d("PunchApi", "complete login final url: ${resp.request.url}")
|
||||
|
||||
val m = Regex("uname['\"]?\\s*[:=]\\s*['\"]([^'\"]+)").find(body)
|
||||
currentUser = m?.groupValues?.get(1) ?: ""
|
||||
|
||||
val appCookies = cookieStore[APP_DOMAIN]
|
||||
if (appCookies?.any { it.name == "s" } == true) {
|
||||
return LoginResult(true, "登录成功", currentUser.ifEmpty { "已登录" })
|
||||
}
|
||||
|
||||
if ("课程" in body || "考勤" in body || "uname" in body) {
|
||||
return LoginResult(true, "登录成功", currentUser.ifEmpty { "已登录" })
|
||||
}
|
||||
|
||||
return LoginResult(false, "登录跳转失败")
|
||||
}
|
||||
|
||||
fun cookieLogin(s: String, remember: String = ""): LoginResult {
|
||||
val sCookie = Cookie.Builder()
|
||||
.domain(APP_DOMAIN).name("s").value(s).path("/").build()
|
||||
cookieStore.getOrPut(APP_DOMAIN) { mutableListOf() }.apply {
|
||||
removeAll { it.name == "s" }
|
||||
add(sCookie)
|
||||
}
|
||||
|
||||
if (remember.isNotEmpty()) {
|
||||
val rCookie = Cookie.Builder()
|
||||
.domain(APP_DOMAIN)
|
||||
.name("remember_student_59ba36addc2b2f9401580f014c7f58ea4e30989d")
|
||||
.value(remember).path("/").build()
|
||||
cookieStore.getOrPut(APP_DOMAIN) { mutableListOf() }.apply {
|
||||
removeAll { it.name.startsWith("remember_student") }
|
||||
add(rCookie)
|
||||
}
|
||||
}
|
||||
|
||||
val req = Request.Builder()
|
||||
.url("$BASE_URL/student")
|
||||
.header("User-Agent", UA)
|
||||
.build()
|
||||
val resp = client.newCall(req).execute()
|
||||
val body = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
|
||||
if ("uname" in body || "课程" in body || "考勤" in body) {
|
||||
val m = Regex("uname['\"]?\\s*[:=]\\s*['\"]([^'\"]+)").find(body)
|
||||
currentUser = m?.groupValues?.get(1) ?: "已登录"
|
||||
return LoginResult(true, "Cookie 登录成功", currentUser)
|
||||
}
|
||||
return LoginResult(false, "Cookie 无效或已过期")
|
||||
}
|
||||
|
||||
fun getCourses(): List<CourseInfo> {
|
||||
val req = Request.Builder()
|
||||
.url("$BASE_URL/student")
|
||||
.header("User-Agent", UA)
|
||||
.build()
|
||||
val resp = client.newCall(req).execute()
|
||||
val body = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
|
||||
val doc = Jsoup.parse(body)
|
||||
val courses = mutableListOf<CourseInfo>()
|
||||
val seen = mutableSetOf<String>()
|
||||
|
||||
for (a in doc.select("a[href~=/student/course/\\d+]")) {
|
||||
val m = Regex("/student/course/(\\d+)").find(a.attr("href")) ?: continue
|
||||
val cid = m.groupValues[1]
|
||||
if (cid in seen) continue
|
||||
seen.add(cid)
|
||||
val title = a.text().trim().ifEmpty { "课程#$cid" }
|
||||
courses.add(CourseInfo(cid, title))
|
||||
}
|
||||
return courses
|
||||
}
|
||||
|
||||
fun getPunchs(courseId: String): List<PunchInfo> {
|
||||
val req = Request.Builder()
|
||||
.url("$BASE_URL/student/course/$courseId/punchs?op=ing")
|
||||
.header("User-Agent", UA)
|
||||
.build()
|
||||
val resp = client.newCall(req).execute()
|
||||
val html = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
|
||||
val doc = Jsoup.parse(html)
|
||||
val punchs = mutableListOf<PunchInfo>()
|
||||
|
||||
for (card in doc.select(".punch-card")) {
|
||||
val titleEl = card.selectFirst(".font-weight-bold")
|
||||
val statusEl = card.selectFirst(".punch-status")
|
||||
val title = titleEl?.text()?.trim() ?: "未知"
|
||||
val status = statusEl?.text()?.trim() ?: "未知"
|
||||
|
||||
var punchId: String? = null
|
||||
|
||||
for (a in card.select("a[href]")) {
|
||||
val m = Regex("/punchs?/(?:course/)?(?:\\d+/)?(\\d{5,})").find(a.attr("href"))
|
||||
if (m != null) {
|
||||
punchId = m.groupValues[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (punchId == null) {
|
||||
for (tag in card.allElements) {
|
||||
for ((_, v) in tag.attributes()) {
|
||||
val m = Regex("(\\d{7,})").find(v)
|
||||
if (m != null) {
|
||||
punchId = m.groupValues[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (punchId != null) break
|
||||
}
|
||||
}
|
||||
|
||||
val metaEl = card.selectFirst(".punch-meta")
|
||||
val meta = metaEl?.text()?.trim() ?: ""
|
||||
val needsGps = "GPS" in meta || "gps" in meta.lowercase()
|
||||
|
||||
punchs.add(PunchInfo(punchId, title, status, meta, needsGps))
|
||||
}
|
||||
return punchs
|
||||
}
|
||||
|
||||
fun doPunch(courseId: String, punchId: String, lat: Double, lng: Double, acc: Int = 20): PunchResult {
|
||||
val uid = getUid()
|
||||
var url = "$BASE_URL/student/punchs/course/$courseId/$punchId"
|
||||
if (uid != null) url += "?sid=$uid"
|
||||
|
||||
val formBody = FormBody.Builder()
|
||||
.add("lat", lat.toString())
|
||||
.add("lng", lng.toString())
|
||||
.add("acc", acc.toString())
|
||||
.add("res", "")
|
||||
.build()
|
||||
|
||||
val req = Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", UA)
|
||||
.header("Origin", BASE_URL)
|
||||
.header("Referer", url)
|
||||
.post(formBody)
|
||||
.build()
|
||||
|
||||
val resp = client.newCall(req).execute()
|
||||
val body = resp.body?.string() ?: ""
|
||||
resp.close()
|
||||
|
||||
return when {
|
||||
"已签到" in body || "签到过" in body || "已签" in body ->
|
||||
PunchResult(true, "签到成功!")
|
||||
"成功" in body || "success" in body.lowercase() ->
|
||||
PunchResult(true, "签到成功!")
|
||||
"未开始" in body ->
|
||||
PunchResult(false, "签到尚未开始")
|
||||
"已结束" in body ->
|
||||
PunchResult(false, "签到已结束")
|
||||
else ->
|
||||
PunchResult(true, "已提交,请确认状态")
|
||||
}
|
||||
}
|
||||
|
||||
fun isLoggedIn(): Boolean {
|
||||
return cookieStore[APP_DOMAIN]?.any { it.name == "s" } == true
|
||||
}
|
||||
|
||||
fun getSavedCookieS(): String {
|
||||
return cookieStore[APP_DOMAIN]?.find { it.name == "s" }?.value ?: ""
|
||||
}
|
||||
|
||||
fun getSavedCookieRemember(): String {
|
||||
return cookieStore[APP_DOMAIN]
|
||||
?.find { it.name.startsWith("remember_student") }?.value ?: ""
|
||||
}
|
||||
|
||||
fun clearCookies() {
|
||||
cookieStore.clear()
|
||||
currentUser = ""
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/drawable/bg_captcha.xml
Normal file
5
app/src/main/res/drawable/bg_captcha.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:width="1dp" android:color="#e0e0e0" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/bg_gps_box.xml
Normal file
8
app/src/main/res/drawable/bg_gps_box.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:startColor="#f6f8fb"
|
||||
android:endColor="#e8edff"
|
||||
android:angle="135" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
7
app/src/main/res/drawable/bg_header.xml
Normal file
7
app/src/main/res/drawable/bg_header.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:startColor="#3a5ef7"
|
||||
android:endColor="#6b8cff"
|
||||
android:angle="135" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_pill_danger.xml
Normal file
5
app/src/main/res/drawable/bg_pill_danger.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#1eff4d4f" />
|
||||
<corners android:radius="999dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_pill_muted.xml
Normal file
5
app/src/main/res/drawable/bg_pill_muted.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#1e8a94a6" />
|
||||
<corners android:radius="999dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_pill_primary.xml
Normal file
5
app/src/main/res/drawable/bg_pill_primary.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#1e4f6ef7" />
|
||||
<corners android:radius="999dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_pill_success.xml
Normal file
5
app/src/main/res/drawable/bg_pill_success.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#1e21b26f" />
|
||||
<corners android:radius="999dp" />
|
||||
</shape>
|
||||
6
app/src/main/res/drawable/bg_user_bar.xml
Normal file
6
app/src/main/res/drawable/bg_user_bar.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#33ffffff" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
22
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
22
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,30 C40.745,30 30,40.745 30,54 C30,67.255 40.745,78 54,78 C67.255,78 78,67.255 78,54 C78,40.745 67.255,30 54,30ZM54,34 C65.046,34 74,42.954 74,54 C74,65.046 65.046,74 54,74 C42.954,74 34,65.046 34,54 C34,42.954 42.954,34 54,34ZM54,44 C48.477,44 44,48.477 44,54 C44,59.523 48.477,64 54,64 C59.523,64 64,59.523 64,54 C64,48.477 59.523,44 54,44ZM54,48 C57.314,48 60,50.686 60,54 C60,57.314 57.314,60 54,60 C50.686,60 48,57.314 48,54 C48,50.686 50.686,48 54,48Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M53,24 L55,24 L55,32 L53,32Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M53,76 L55,76 L55,84 L53,84Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M24,53 L32,53 L32,55 L24,55Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M76,53 L84,53 L84,55 L76,55Z" />
|
||||
</vector>
|
||||
729
app/src/main/res/layout/activity_main.xml
Normal file
729
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,729 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="#f0f2f5">
|
||||
|
||||
<!-- Header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/bg_header"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="GPS 签到助手"
|
||||
android:textColor="#ffffff"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="自定义位置 · 一键打卡"
|
||||
android:textColor="#ccddff"
|
||||
android:textSize="13sp"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
<!-- User bar -->
|
||||
<LinearLayout
|
||||
android:id="@+id/headerUserBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/bg_user_bar"
|
||||
android:padding="10dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUser"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textColor="#ffffff"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnLogout"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:text="退出"
|
||||
android:textColor="#ffffff"
|
||||
android:textSize="12sp"
|
||||
android:minWidth="0dp"
|
||||
android:paddingHorizontal="12dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ===== Login View ===== -->
|
||||
<ScrollView
|
||||
android:id="@+id/loginView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:fillViewport="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- Login Form Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="学生身份验证"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="姓名"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="学号"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etNo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="验证码(点击图片刷新)"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#666"
|
||||
android:layout_marginBottom="6dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="4位数字"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginEnd="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:maxLength="6" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/captchaImg"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="fitXY"
|
||||
android:background="@drawable/bg_captcha"
|
||||
android:contentDescription="验证码" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLogin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:text="登 录"
|
||||
android:textSize="16sp"
|
||||
app:cornerRadius="12dp" />
|
||||
|
||||
<!-- removed, moved to guideView -->
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Divider -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginVertical="8dp">
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#e0e0e0" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=" 或 "
|
||||
android:textColor="#999"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#e0e0e0" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Cookie Login Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Cookie 登录"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="从微信浏览器抓包获取 Cookie"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#999"
|
||||
android:layout_marginBottom="14dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="s (session cookie)"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etCookieS"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="remember_student(可选)"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etCookieRemember"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCookieLogin"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:text="Cookie 登录"
|
||||
android:textSize="15sp"
|
||||
app:cornerRadius="12dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- ===== WeChat Guide View ===== -->
|
||||
<ScrollView
|
||||
android:id="@+id/guideView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:fillViewport="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<!-- Success Icon -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="微信登录"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<!-- Steps Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="操作步骤"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="① 点击下方按钮复制登录链接"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:drawablePadding="8dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="② 打开微信,发送给「文件传输助手」"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="③ 在微信中点击该链接"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="④ 返回本 App,自动登录完成"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/primary"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Link Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="登录链接"
|
||||
android:textSize="13sp"
|
||||
android:textColor="#666"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvWeixinLink"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_gps_box"
|
||||
android:padding="14dp"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/primary"
|
||||
android:fontFamily="monospace"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCopyLink"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="54dp"
|
||||
android:text="复制链接到剪贴板"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="12dp"
|
||||
android:layout_marginTop="14dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Polling Status -->
|
||||
<TextView
|
||||
android:id="@+id/tvPollingStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/primary"
|
||||
android:textStyle="bold"
|
||||
android:padding="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Back button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBackToLogin"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="返回重新登录"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- ===== Main Content ===== -->
|
||||
<FrameLayout
|
||||
android:id="@+id/mainContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- Punch View -->
|
||||
<ScrollView
|
||||
android:id="@+id/punchView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- GPS Quick Box -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/bg_gps_box"
|
||||
android:padding="16dp"
|
||||
android:layout_marginBottom="14dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📍 当前定位"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="纬度 (lat)"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberDecimal|numberSigned"
|
||||
android:hint="输入纬度" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="经度 (lng)"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLng"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberDecimal|numberSigned"
|
||||
android:hint="输入经度" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroupPresets"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
app:singleSelection="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Punch list header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="签到列表"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnRefresh"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:text="刷新"
|
||||
android:textSize="13sp"
|
||||
app:cornerRadius="10dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/punchListContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="点击「刷新」获取签到列表"
|
||||
android:textColor="#8a94a6"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center"
|
||||
android:padding="48dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- GPS View -->
|
||||
<ScrollView
|
||||
android:id="@+id/gpsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginBottom="14dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="添加常用位置"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="14dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="位置名称"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPresetName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="14dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="纬度"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPresetLat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberDecimal|numberSigned" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="经度"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPresetLng"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberDecimal|numberSigned" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAddPreset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="添加到常用位置"
|
||||
app:cornerRadius="12dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Saved presets list -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="已保存的位置"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/presetListContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<!-- Guide -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginTop="14dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="如何获取坐标?"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="10dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1. 打开手机地图 App(高德/百度/腾讯)\n2. 长按目标位置,弹出坐标信息\n3. 复制经纬度填入上方\n\n注意:使用 GCJ-02 坐标系(高德/腾讯地图默认即是)"
|
||||
android:textSize="13sp"
|
||||
android:textColor="#666"
|
||||
android:lineSpacingExtra="4dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- History View -->
|
||||
<ScrollView
|
||||
android:id="@+id/historyView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/historyListContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp" />
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Bottom Navigation -->
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottomNav"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white"
|
||||
app:menu="@menu/bottom_nav"
|
||||
app:labelVisibilityMode="labeled"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
47
app/src/main/res/layout/item_history.xml
Normal file
47
app/src/main/res/layout/item_history.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginBottom="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHistoryTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHistoryResult"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHistoryTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#8a94a6"
|
||||
android:layout_marginTop="6dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
49
app/src/main/res/layout/item_preset.xml
Normal file
49
app/src/main/res/layout/item_preset.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="14dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPresetName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPresetCoord"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#8a94a6"
|
||||
android:layout_marginTop="2dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDeletePreset"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:text="删除"
|
||||
android:textColor="#ff4d4f"
|
||||
android:textSize="12sp"
|
||||
android:minWidth="0dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
52
app/src/main/res/layout/item_punch.xml
Normal file
52
app/src/main/res/layout/item_punch.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPunchTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#1a1a2e" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPunchMeta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#8a94a6"
|
||||
android:layout_marginTop="4dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPunchStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
15
app/src/main/res/menu/bottom_nav.xml
Normal file
15
app/src/main/res/menu/bottom_nav.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/nav_punch"
|
||||
android:icon="@android:drawable/ic_menu_mylocation"
|
||||
android:title="签到" />
|
||||
<item
|
||||
android:id="@+id/nav_gps"
|
||||
android:icon="@android:drawable/ic_menu_mapmode"
|
||||
android:title="GPS设置" />
|
||||
<item
|
||||
android:id="@+id/nav_history"
|
||||
android:icon="@android:drawable/ic_menu_recent_history"
|
||||
android:title="历史" />
|
||||
</menu>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/primary" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 B |
13
app/src/main/res/values/colors.xml
Normal file
13
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#4f6ef7</color>
|
||||
<color name="primary_dark">#3a5ef7</color>
|
||||
<color name="primary_light">#e8edff</color>
|
||||
<color name="success">#21b26f</color>
|
||||
<color name="danger">#ff4d4f</color>
|
||||
<color name="warn">#ff9f1a</color>
|
||||
<color name="muted">#8a94a6</color>
|
||||
<color name="bg">#f0f2f5</color>
|
||||
<color name="white">#ffffff</color>
|
||||
<color name="text_primary">#1a1a2e</color>
|
||||
</resources>
|
||||
4
app/src/main/res/values/strings.xml
Normal file
4
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">GPS签到助手</string>
|
||||
</resources>
|
||||
10
app/src/main/res/values/themes.xml
Normal file
10
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.GPSPunch" parent="Theme.Material3.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="android:statusBarColor">@color/primary_dark</item>
|
||||
<item name="android:windowBackground">@color/bg</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
build.gradle
Normal file
4
build.gradle
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.2.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
|
||||
}
|
||||
4
gradle.properties
Normal file
4
gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
249
gradlew
vendored
Executable file
249
gradlew
vendored
Executable file
@@ -0,0 +1,249 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
gradlew.bat
vendored
Normal file
92
gradlew.bat
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
16
settings.gradle
Normal file
16
settings.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.name = "GPSPunchApp"
|
||||
include ':app'
|
||||
Reference in New Issue
Block a user