feat(android): 增加独立Android原生离线工程与GitHub Actions自动化构建
- 原生工程物理隔离:新建 android/ 独立工程,零侵入 Web 端现有源码 - 自动化资产同步:通过 android/copy-web-assets.sh 在构建时复制 Web 静态资产 - 原生体验调优:沉浸式状态栏适配、明文HTTP与DOM存储支持、返回键防误退 - 高清图标生成:基于 icon-512.png 自动生成全套 mipmap 分辨率桌面图标 - 云端CI构建:添加 .github/workflows/build-apk.yml 支持一键/Tag打包输出 APK
@@ -0,0 +1,51 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'top.ouaone.workhours'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "top.ouaone.workhours"
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
def keyFilePath = System.getenv("SIGNING_KEY_FILE")
|
||||
if (keyFilePath && file(keyFilePath).exists()) {
|
||||
storeFile file(keyFilePath)
|
||||
storePassword System.getenv("KEY_STORE_PASSWORD") ?: "workhours123"
|
||||
keyAlias System.getenv("ALIAS") ?: "workhours"
|
||||
keyPassword System.getenv("KEY_PASSWORD") ?: "workhours123"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
def keyFilePath = System.getenv("SIGNING_KEY_FILE")
|
||||
if (keyFilePath && file(keyFilePath).exists()) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.activity:activity:1.8.2'
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# ProGuard rules for Work Hours Tracker
|
||||
-keepattributes JavascriptInterface
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WorkHours"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,149 @@
|
||||
package top.ouaone.workhours;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private WebView webView;
|
||||
private long lastBackPressTime = 0;
|
||||
private static final long BACK_PRESS_INTERVAL = 2000; // 2秒双击防误退
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 设置状态栏与导航栏外观
|
||||
setupSystemBars();
|
||||
|
||||
webView = new WebView(this);
|
||||
webView.setBackgroundColor(Color.parseColor("#1E293B"));
|
||||
setContentView(webView);
|
||||
|
||||
configureWebSettings();
|
||||
setupClients();
|
||||
setupBackNavigation();
|
||||
|
||||
// 加载打包在 assets 中的离线应用
|
||||
webView.loadUrl("file:///android_asset/www/index.html");
|
||||
}
|
||||
|
||||
private void setupSystemBars() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(Color.parseColor("#1E293B"));
|
||||
getWindow().setNavigationBarColor(Color.parseColor("#F8FAFC"));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
View decor = getWindow().getDecorView();
|
||||
int flags = decor.getSystemUiVisibility();
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; // 浅色导航栏图标
|
||||
decor.setSystemUiVisibility(flags);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private void configureWebSettings() {
|
||||
WebSettings s = webView.getSettings();
|
||||
s.setJavaScriptEnabled(true);
|
||||
s.setDomStorageEnabled(true);
|
||||
s.setDatabaseEnabled(true);
|
||||
s.setAllowFileAccess(true);
|
||||
s.setAllowContentAccess(true);
|
||||
s.setAllowFileAccessFromFileURLs(true);
|
||||
s.setAllowUniversalAccessFromFileURLs(true);
|
||||
s.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
s.setUseWideViewPort(true);
|
||||
s.setLoadWithOverviewMode(true);
|
||||
s.setSupportZoom(false);
|
||||
s.setDisplayZoomControls(false);
|
||||
s.setCacheMode(WebSettings.LOAD_DEFAULT);
|
||||
}
|
||||
|
||||
private void setupClients() {
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||
Uri uri = request.getUrl();
|
||||
String scheme = uri.getScheme();
|
||||
if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
|
||||
// 仅当点击外部链接且不是同步 API 时调用系统浏览器
|
||||
String url = uri.toString();
|
||||
if (!url.contains("/api/")) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public boolean onConsoleMessage(ConsoleMessage cm) {
|
||||
// 控制台日志过滤输出到 Logcat
|
||||
android.util.Log.d("WorkHoursWebView", cm.message() + " -- From line "
|
||||
+ cm.lineNumber() + " of " + cm.sourceId());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBackNavigation() {
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastBackPressTime < BACK_PRESS_INTERVAL) {
|
||||
finish();
|
||||
} else {
|
||||
lastBackPressTime = now;
|
||||
Toast.makeText(MainActivity.this, "再按一次退出工时记账", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (webView != null) {
|
||||
webView.onResume();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (webView != null) {
|
||||
webView.onPause();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (webView != null) {
|
||||
webView.destroy();
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#4F46E5</color>
|
||||
<color name="status_bar_color">#1E293B</color>
|
||||
<color name="nav_bar_color">#F8FAFC</color>
|
||||
<color name="window_background">#1E293B</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">工时记账</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.WorkHours" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/status_bar_color</item>
|
||||
<item name="android:navigationBarColor">@color/nav_bar_color</item>
|
||||
<item name="android:windowBackground">@color/window_background</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
</resources>
|
||||