diff --git a/bytheway/.gitignore b/bytheway/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/bytheway/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/bytheway/README.md b/bytheway/README.md new file mode 100644 index 0000000..c0d41d4 --- /dev/null +++ b/bytheway/README.md @@ -0,0 +1,39 @@ +# [ByTheWay] +#【顺道说】 + +#### 介绍 +The [ByTheWay] project is designed to quickly schedule task reminders related to time and location. +Its main feature is the ability to quickly create task-related data. +【顺道说】项目应用应用于快速安排一个以时间和位置有关的任务提醒事项。 +其主要的特点是能快速建立事项任务数据。 + +#### 软件架构 +适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。 +也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。 + + +#### Gradle 编译说明 +调试版编译命令 :gradle assembleBetaDebug +阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh bytheway + +#### 使用说明 +在安卓操作系统里使用。 + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 : ZhanGSKen(ZhanGSKen) +4. 新建 Pull Request + + +#### 特技 + +1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md +2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) +3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 +4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 +5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) +6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) + +#### 参考文档 diff --git a/bytheway/app_update_description.txt b/bytheway/app_update_description.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bytheway/app_update_description.txt @@ -0,0 +1 @@ + diff --git a/bytheway/build.gradle b/bytheway/build.gradle new file mode 100644 index 0000000..e931eb0 --- /dev/null +++ b/bytheway/build.gradle @@ -0,0 +1,110 @@ +apply plugin: 'com.android.application' +apply from: '../.winboll/winboll_app_build.gradle' +apply from: '../.winboll/winboll_lint_build.gradle' + +def genVersionName(def versionName){ + // 检查编译标志位配置 + assert (winbollBuildProps['stageCount'] != null) + assert (winbollBuildProps['baseVersion'] != null) + // 保存基础版本号 + winbollBuildProps.setProperty("baseVersion", "${versionName}"); + //保存编译标志配置 + FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile) + winbollBuildProps.store(fos, "${winbollBuildPropsDesc}"); + fos.close(); + + // 返回编译版本号 + return "${versionName}." + winbollBuildProps['stageCount'] +} + +android { + // 适配MIUI12 + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "cc.winboll.studio.bytheway" + minSdkVersion 26 + targetSdkVersion 30 + //1. Android 官方规则 + //-  versionCode  类型:int 整型 + //- Java int 最大值:2147483647 + versionCode 1520000 + // versionName 更新后需要手动设置 + // 项目模块目录的 build.gradle 文件的 stageCount=0 + // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" + versionName "15.21" + if(true) { + versionName = genVersionName("${versionName}") + } + } + +} + +dependencies { + api 'com.google.code.gson:gson:2.10.1' + + // 下拉控件 + api 'com.baoyz.pullrefreshlayout:library:1.2.0' + + // SSH + api 'com.jcraft:jsch:0.1.55' + // Html 解析 + api 'org.jsoup:jsoup:1.13.1' + // 二维码类库 + api 'com.google.zxing:core:3.4.1' + api 'com.journeyapps:zxing-android-embedded:3.6.0' + // 应用介绍页类库 + api 'io.github.medyo:android-about-page:2.0.0' + // 网络连接类库 + api 'com.squareup.okhttp3:okhttp:4.4.1' + // FastJSON解析 + implementation 'com.alibaba:fastjson:1.2.76' + + // AndroidX 类库 + /*api 'androidx.appcompat:appcompat:1.1.0' + //api 'com.google.android.material:material:1.4.0' + //api 'androidx.viewpager:viewpager:1.0.0' + //api 'androidx.vectordrawable:vectordrawable:1.1.0' + //api 'androidx.vectordrawable:vectordrawable-animated:1.1.0' + //api 'androidx.fragment:fragment:1.1.0'*/ + + + // 米盟 + //api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk + //注意:以下5个库必须要引入 + //implementation 'androidx.appcompat:appcompat:1.4.1' + api 'androidx.recyclerview:recyclerview:1.0.0' + api 'com.github.bumptech.glide:glide:4.9.0' + //annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' + + implementation "androidx.annotation:annotation:1.3.0" + implementation "androidx.core:core:1.6.0" + implementation "androidx.drawerlayout:drawerlayout:1.1.1" + implementation "androidx.preference:preference:1.1.1" + implementation "androidx.viewpager:viewpager:1.0.0" + implementation "com.google.android.material:material:1.4.0" + implementation "com.google.guava:guava:24.1-jre" + /* + implementation "io.noties.markwon:core:$markwonVersion" + implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" + implementation "io.noties.markwon:linkify:$markwonVersion" + implementation "io.noties.markwon:recycler:$markwonVersion" + */ + implementation 'com.termux:terminal-emulator:0.118.0' + implementation 'com.termux:terminal-view:0.118.0' + implementation 'com.termux:termux-shared:0.118.0' + + // Biometric (指纹识别) + implementation 'androidx.biometric:biometric:1.1.0' + + // WinBoLL库 nexus.winboll.cc 地址 + api 'cc.winboll.studio:libappbase:15.21.+' + api 'cc.winboll.studio:libaes:15.21.+' + + // 备用库 jitpack.io 地址 + //api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.33' + //api 'com.github.ZhanGSKen:libaes:aes-v15.20.16' + + api fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/bytheway/build.properties b/bytheway/build.properties new file mode 100644 index 0000000..4df5b19 --- /dev/null +++ b/bytheway/build.properties @@ -0,0 +1,8 @@ +#Created by .winboll/winboll_app_build.gradle +#Tue Jul 21 18:53:02 GMT 2026 +stageCount=0 +libraryProject= +baseVersion=15.21 +publishVersion=15.21.0 +buildCount=1 +baseBetaVersion=15.21.1 diff --git a/bytheway/proguard-rules.pro b/bytheway/proguard-rules.pro new file mode 100644 index 0000000..a18de74 --- /dev/null +++ b/bytheway/proguard-rules.pro @@ -0,0 +1,137 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# ============================== 基础通用规则 ============================== +# 保留系统组件 +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference + +# 保留 WinBoLL 核心包及子类(合并简化规则) +-keep class cc.winboll.studio.** { *; } +-keepclassmembers class cc.winboll.studio.** { *; } + +# 保留所有类中的 public static final String TAG 字段(便于日志定位) +-keepclassmembers class * { + public static final java.lang.String TAG; +} + +# 保留序列化类(避免Parcelable/Gson解析异常) +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# 保留 R 文件(避免资源ID混淆) +-keepclassmembers class **.R$* { + public static ; +} + +# 保留 native 方法(避免JNI调用失败) +-keepclasseswithmembernames class * { + native ; +} + +# 保留注解和泛型(避免反射/序列化异常) +-keepattributes *Annotation* +-keepattributes Signature + +# 屏蔽 Java 8+ 警告(适配 Java 7 语法) +-dontwarn java.lang.invoke.* +-dontwarn android.support.v8.renderscript.* +-dontwarn java.util.function.** + +# ============================== 第三方框架专项规则 ============================== +# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容) +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-keep class okhttp3.internal.** { *; } +-keep class okio.** { *; } +-dontwarn okhttp3.internal.platform.** +-dontwarn okio.** + +# Glide 4.9.0(米盟广告图片加载依赖) +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType { + **[] $VALUES; + public *; +} +-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule { + (); +} +-dontwarn com.bumptech.glide.** + +# Gson 2.8.5(米盟广告数据序列化依赖) +-keep class com.google.gson.** { *; } +-keep interface com.google.gson.** { *; } +-keepclassmembers class * { + @com.google.gson.annotations.SerializedName ; +} + +# 米盟 SDK(核心广告组件,完整保留避免加载失败) +-keep class com.miui.zeus.** { *; } +-keep interface com.miui.zeus.** { *; } +# 保留米盟日志字段(便于广告加载失败排查) +-keepclassmembers class com.miui.zeus.mimo.sdk.** { + public static final java.lang.String TAG; +} + +# RecyclerView 1.0.0(米盟广告布局渲染依赖) +-keep class androidx.recyclerview.** { *; } +-keep interface androidx.recyclerview.** { *; } +-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter { + public *; +} + +# 其他第三方框架(按引入依赖保留,无则可删除) +# XXPermissions 18.63 +-keep class com.hjq.permissions.** { *; } +-keep interface com.hjq.permissions.** { *; } + +# ZXing 二维码(核心解析组件) +-keep class com.google.zxing.** { *; } +-keep class com.journeyapps.zxing.** { *; } + +# Jsoup HTML解析 +-keep class org.jsoup.** { *; } + +# Pinyin4j 拼音搜索 +-keep class net.sourceforge.pinyin4j.** { *; } + +# JSch SSH组件 +-keep class com.jcraft.jsch.** { *; } + +# AndroidX 基础组件 +-keep class androidx.appcompat.** { *; } +-keep interface androidx.appcompat.** { *; } + +# ============================== 优化与调试配置 ============================== +# 优化级别(平衡混淆效果与性能) +-optimizationpasses 5 +-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* + +# 调试辅助(保留行号便于崩溃定位) +-verbose +-dontpreverify +-dontusemixedcaseclassnames +-keepattributes SourceFile,LineNumberTable + diff --git a/bytheway/src/beta/AndroidManifest.xml b/bytheway/src/beta/AndroidManifest.xml new file mode 100644 index 0000000..ee78d9f --- /dev/null +++ b/bytheway/src/beta/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/bytheway/src/beta/res/values-zh/strings.xml b/bytheway/src/beta/res/values-zh/strings.xml new file mode 100644 index 0000000..20d0176 --- /dev/null +++ b/bytheway/src/beta/res/values-zh/strings.xml @@ -0,0 +1,4 @@ + + + 顺道说☯string> + diff --git a/bytheway/src/beta/res/values/strings.xml b/bytheway/src/beta/res/values/strings.xml new file mode 100644 index 0000000..5b6a325 --- /dev/null +++ b/bytheway/src/beta/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + ByTheWay + + + diff --git a/bytheway/src/main/AndroidManifest.xml b/bytheway/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9880515 --- /dev/null +++ b/bytheway/src/main/AndroidManifest.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/App.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/App.java new file mode 100644 index 0000000..93628ba --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/App.java @@ -0,0 +1,397 @@ +package cc.winboll.studio.bytheway; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; +import cc.winboll.studio.libaes.utils.AESThemeUtil; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; +import cc.winboll.studio.bytheway.services.GpsReceiveService; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.Thread.UncaughtExceptionHandler; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class App extends GlobalApplication { + + private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); + private BroadcastReceiver screenReceiver; + + @Override + public void onCreate() { + try { + super.onCreate(); + WinBoLLActivityManager.init(this); + + // 初始化主题样式ID列表 + AESThemeUtil.init(null); + + // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) + ToastUtils.init(getApplicationContext()); + + // 检查GPS服务SP标记,系统内存清理后自动恢复服务运行 + GpsReceiveService.checkAndStartService(this); + + // 动态注册屏幕广播,息屏/亮屏时检查GPS服务运行状态 + registerScreenReceiver(); + } catch (Throwable e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + pw.close(); + String stackTraceStr = sw.toString(); + CrashHandleNotifyUtils.handleUncaughtException( + this, + getPackageName(), + stackTraceStr, + CrashActivity.class + ); + } + } + + private void registerScreenReceiver() { + screenReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + GpsReceiveService.checkAndStartService(context); + } + }; + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + registerReceiver(screenReceiver, filter); + } + + @Override + public void onTrimMemory(final int level) { + super.onTrimMemory(level); + if (level >= TRIM_MEMORY_UI_HIDDEN) { + GpsReceiveService.checkAndStartService(this); + } + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + GpsReceiveService.checkAndStartService(this); + } + + public static void write(InputStream input, OutputStream output) throws IOException { + byte[] buf = new byte[1024 * 8]; + int len; + while ((len = input.read(buf)) != -1) { + output.write(buf, 0, len); + } + } + + public static void write(File file, byte[] data) throws IOException { + File parent = file.getParentFile(); + if (parent != null && !parent.exists()) parent.mkdirs(); + + ByteArrayInputStream input = new ByteArrayInputStream(data); + FileOutputStream output = new FileOutputStream(file); + try { + write(input, output); + } finally { + closeIO(input, output); + } + } + + public static String toString(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + write(input, output); + try { + return output.toString("UTF-8"); + } finally { + closeIO(input, output); + } + } + + public static void closeIO(Closeable... closeables) { + for (Closeable closeable : closeables) { + try { + if (closeable != null) closeable.close(); + } catch (IOException ignored) {} + } + } + + public static class CrashHandler { + + public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler(); + + private static CrashHandler sInstance; + + private PartCrashHandler mPartCrashHandler; + + public static CrashHandler getInstance() { + if (sInstance == null) { + sInstance = new CrashHandler(); + } + return sInstance; + } + + public void registerGlobal(Context context) { + registerGlobal(context, null); + } + + public void registerGlobal(Context context, String crashDir) { + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir)); + } + + public void unregister() { + Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER); + } + + public void registerPart(Context context) { + unregisterPart(context); + mPartCrashHandler = new PartCrashHandler(context.getApplicationContext()); + MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler); + } + + public void unregisterPart(Context context) { + if (mPartCrashHandler != null) { + mPartCrashHandler.isRunning.set(false); + mPartCrashHandler = null; + } + } + + private static class PartCrashHandler implements Runnable { + + private final Context mContext; + + public AtomicBoolean isRunning = new AtomicBoolean(true); + + public PartCrashHandler(Context context) { + this.mContext = context; + } + + @Override + public void run() { + while (isRunning.get()) { + try { + Looper.loop(); + } catch (final Throwable e) { + e.printStackTrace(); + if (isRunning.get()) { + MAIN_HANDLER.post(new Runnable(){ + + @Override + public void run() { + Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show(); + } + }); + } else { + if (e instanceof RuntimeException) { + throw (RuntimeException)e; + } else { + throw new RuntimeException(e); + } + } + } + } + } + } + + private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler { + + private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss"); + + private final Context mContext; + + private final File mCrashDir; + + public UncaughtExceptionHandlerImpl(Context context, String crashDir) { + this.mContext = context; + this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir); + } + + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + try { + + String log = buildLog(throwable); + writeLog(log); + + try { + Intent intent = new Intent(mContext, CrashActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_TEXT, log); + mContext.startActivity(intent); + } catch (Throwable e) { + e.printStackTrace(); + writeLog(e.toString()); + } + + throwable.printStackTrace(); + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(0); + + } catch (Throwable e) { + if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable); + } + } + + private String buildLog(Throwable throwable) { + String time = DATE_FORMAT.format(new Date()); + + String versionName = "unknown"; + long versionCode = 0; + try { + PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0); + versionName = packageInfo.versionName; + versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode; + } catch (Throwable ignored) {} + + LinkedHashMap head = new LinkedHashMap(); + head.put("Time Of Crash", time); + head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL)); + head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); + head.put("App Version", String.format("%s (%d)", versionName, versionCode)); + head.put("Kernel", getKernel()); + head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown"); + head.put("Fingerprint", Build.FINGERPRINT); + + StringBuilder builder = new StringBuilder(); + + for (String key : head.keySet()) { + if (builder.length() != 0) builder.append("\n"); + builder.append(key); + builder.append(" : "); + builder.append(head.get(key)); + } + + builder.append("\n\n"); + builder.append(Log.getStackTraceString(throwable)); + + return builder.toString(); + } + + private void writeLog(String log) { + String time = DATE_FORMAT.format(new Date()); + File file = new File(mCrashDir, "crash_" + time + ".txt"); + try { + write(file, log.getBytes("UTF-8")); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private static String getKernel() { + try { + return App.toString(new FileInputStream("/proc/version")).trim(); + } catch (Throwable e) { + return e.getMessage(); + } + } + } + } + + public static final class CrashActivity extends Activity { + + private String mLog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setTheme(android.R.style.Theme_DeviceDefault); + setTitle("App Crash"); + + mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT); + + ScrollView contentView = new ScrollView(this); + contentView.setFillViewport(true); + + HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this); + + TextView textView = new TextView(this); + int padding = dp2px(16); + textView.setPadding(padding, padding, padding, padding); + textView.setText(mLog); + textView.setTextIsSelectable(true); + textView.setTypeface(Typeface.DEFAULT); + textView.setLinksClickable(true); + + horizontalScrollView.addView(textView); + contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + + setContentView(contentView); + } + + private void restart() { + Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName()); + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + finish(); + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(0); + } + + private static int dp2px(float dpValue) { + final float scale = Resources.getSystem().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, android.R.id.copy, 0, android.R.string.copy) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.copy: + ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog)); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + restart(); + } + } +} diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/MainActivity.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/MainActivity.java new file mode 100644 index 0000000..6c13d46 --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/MainActivity.java @@ -0,0 +1,146 @@ +package cc.winboll.studio.bytheway; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import cc.winboll.studio.bytheway.activities.AboutActivity; +import cc.winboll.studio.bytheway.activities.BaseWinBoLLActivity; +import cc.winboll.studio.bytheway.activities.GpsListActivity; +import cc.winboll.studio.bytheway.activities.SettingsActivity; +import cc.winboll.studio.libaes.utils.AESThemeUtil; +import cc.winboll.studio.libaes.utils.DevelopUtils; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; + +public class MainActivity extends BaseWinBoLLActivity { + + public static final String TAG = "MainActivity"; + private static final long SPLASH_DURATION = 5000L; + private static final long SPLASH_FADE_IN = 800L; + private static final long SPLASH_FADE_OUT = 500L; + private View splashOverlay; + private final Handler splashHandler = new Handler(); + + @Override + public String getTag() { + return TAG; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + //AESThemeUtil.applyAppCompatTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + initSplash(); + ToastUtils.show("onCreate"); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onDestroy() { + splashHandler.removeCallbacksAndMessages(null); + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + AESThemeUtil.inflateCompatThemeMenu(this, menu); + if (GlobalApplication.isDebugging()) { + DevelopUtils.inflateMenu(this, menu); + } + getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_drawerbase, menu); + getMenuInflater().inflate(R.menu.activity_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (AESThemeUtil.onWinBoLLThemeItemSelected(this, item)) { + recreate(); + } + if (item.getItemId() == R.id.action_gps_list) { + startActivity(new Intent(this, GpsListActivity.class)); + return true; + } + if (item.getItemId() == R.id.action_settings) { + startActivity(new Intent(this, SettingsActivity.class)); + return true; + } + if (item.getItemId() == R.id.action_about) { + startActivity(new Intent(this, AboutActivity.class)); + return true; + } + if (DevelopUtils.onDevelopItemSelected(this, item)) { + LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId())); + } else { + return super.onOptionsItemSelected(item); + } + return true; + } + + private void initSplash() { + splashOverlay = findViewById(R.id.splash_overlay); + final TextView splashTitle = findViewById(R.id.splash_title); + final TextView splashSubtitle = findViewById(R.id.splash_subtitle); + + TypedValue typedValue = new TypedValue(); + if (getTheme().resolveAttribute(androidx.appcompat.R.attr.colorPrimaryDark, typedValue, true)) { + splashOverlay.setBackgroundColor(typedValue.data); + } + + splashOverlay.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, android.view.MotionEvent event) { + dismissSplash(); + return true; + } + }); + + AlphaAnimation fadeInTitle = new AlphaAnimation(0f, 1f); + fadeInTitle.setDuration(SPLASH_FADE_IN); + fadeInTitle.setFillAfter(true); + splashTitle.startAnimation(fadeInTitle); + + AlphaAnimation fadeInSubtitle = new AlphaAnimation(0f, 1f); + fadeInSubtitle.setDuration(SPLASH_FADE_IN); + fadeInSubtitle.setStartOffset(SPLASH_FADE_IN); + fadeInSubtitle.setFillAfter(true); + splashSubtitle.startAnimation(fadeInSubtitle); + + splashHandler.postDelayed(new Runnable() { + @Override + public void run() { + dismissSplash(); + } + }, SPLASH_DURATION); + } + + private void dismissSplash() { + splashHandler.removeCallbacksAndMessages(null); + if (splashOverlay == null || splashOverlay.getVisibility() != View.VISIBLE) { + return; + } + splashOverlay.clearAnimation(); + for (int i = 0; i < ((android.view.ViewGroup) splashOverlay).getChildCount(); i++) { + ((android.view.ViewGroup) splashOverlay).getChildAt(i).clearAnimation(); + } + splashOverlay.setVisibility(View.GONE); + } +} diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/AboutActivity.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/AboutActivity.java new file mode 100644 index 0000000..dd2215a --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/AboutActivity.java @@ -0,0 +1,79 @@ +package cc.winboll.studio.bytheway.activities; + +import android.os.Bundle; +import android.view.View; +import androidx.appcompat.widget.Toolbar; +import cc.winboll.studio.bytheway.MainActivity; +import cc.winboll.studio.bytheway.R; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.models.APPInfo; +import cc.winboll.studio.libappbase.views.AboutView; + +/** + * @Author 豆包&BigPickle&ZhanGSKen + * @Date 2026/07/07 00:17 + * @Describe 应用介绍窗口 + */ +public class AboutActivity extends BaseWinBoLLActivity { + + public static final String TAG = "AboutActivity"; + + private Toolbar mToolbar; + + @Override + public String getTag() { + return TAG; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + + // 设置工具栏 + initToolbar(); + + AboutView aboutView = findViewById(R.id.aboutview); + aboutView.setAPPInfo(genDefaultAppInfo()); + } + + private void initToolbar() { + LogUtils.d(TAG, "initToolbar() 开始初始化"); + mToolbar = findViewById(R.id.toolbar); + if (mToolbar == null) { + LogUtils.e(TAG, "initToolbar() | Toolbar未找到"); + return; + } + setSupportActionBar(mToolbar); + mToolbar.setSubtitle(getTag()); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + mToolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "导航栏 点击返回按钮"); + WinBoLLActivityManager.getInstance().resumeActivity(MainActivity.class); + WinBoLLActivityManager.getInstance().finish(AboutActivity.this); + } + }); + LogUtils.d(TAG, "initToolbar() 配置完成"); + } + + private APPInfo genDefaultAppInfo() { + LogUtils.d(TAG, "genDefaultAppInfo() 调用"); + String branchName = "bytheway"; + APPInfo appInfo = new APPInfo(); + appInfo.setAppName(getString(R.string.app_name)); + appInfo.setAppIcon(R.drawable.ic_winboll); + appInfo.setAppDescription(getString(R.string.app_description)); + appInfo.setAppGitName("MyWinBoLL"); + appInfo.setAppGitOwner("ZhanGSKen"); + appInfo.setAppGitAPPBranch(branchName); + appInfo.setAppGitAPPSubProjectFolder(branchName); + appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=ByTheWay"); + appInfo.setAppAPKName("ByTheWay"); + appInfo.setAppAPKFolderName("ByTheWay"); + LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成"); + return appInfo; + } +} diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/BaseWinBoLLActivity.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/BaseWinBoLLActivity.java new file mode 100644 index 0000000..fbd1edf --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/BaseWinBoLLActivity.java @@ -0,0 +1,46 @@ +package cc.winboll.studio.bytheway.activities; + +import android.app.Activity; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libaes.models.AESThemeBean; +import cc.winboll.studio.libaes.utils.AESThemeUtil; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; + +/** + * @Author 豆包&BigPickle&ZhanGSKen + * @Date 2026/07/07 00:21 + * @Describe BaseWinBollActivity 【继承AppCompatActivity,保留核心能力,不额外暴露方法】 + * 继承链路:BaseWinBoLLActivity → AppCompatActivity → FragmentActivity,AppCompat能力天然继承可用 + */ +public abstract class BaseWinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity { + public static final String TAG = "BaseWinBoLLActivity"; + + protected volatile AESThemeBean.ThemeType mThemeType; + + @Override + protected void onCreate(Bundle savedInstanceState) { + //AESThemeUtil.applyAppCompatTheme(this); + mThemeType = AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext())); + setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext())); + super.onCreate(savedInstanceState); + WinBoLLActivityManager.getInstance().add(this); + } + + @Override + protected void onDestroy() { + WinBoLLActivityManager.getInstance().registeRemove(this); + super.onDestroy(); + } + + // 子类必须实现getTag(),确保唯一标识 + @Override + public abstract String getTag(); + + @Override + public Activity getActivity() { + return this; + } +} + diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/GpsListActivity.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/GpsListActivity.java new file mode 100644 index 0000000..37b1f45 --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/GpsListActivity.java @@ -0,0 +1,76 @@ +package cc.winboll.studio.bytheway.activities; + +import android.os.Bundle; +import android.os.Handler; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import cc.winboll.studio.bytheway.R; +import cc.winboll.studio.bytheway.adapters.GpsAdapter; +import cc.winboll.studio.bytheway.models.GpsBean; +import cc.winboll.studio.bytheway.utils.GpsUtil; +import cc.winboll.studio.libappbase.LogUtils; +import java.util.List; + +/** + * GPS定位数据列表展示页面 + * 支持下拉刷新SQLite本地数据库定位轨迹信息 + * @author 豆包&ZhanGSKen + * 创建时间:2026-07-02 14:49:00 + * 最后编辑时间:2026-07-02 16:32:13 + */ +public class GpsListActivity extends AppCompatActivity { + + private SwipeRefreshLayout swipeRefresh; + private RecyclerView rvGps; + private GpsAdapter adapter; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_gps_list); + LogUtils.i("GpsListActivity", "页面初始化"); + + initView(); + initData(); + setSwipeRefreshListener(); + } + + private void initView() { + swipeRefresh = findViewById(R.id.swipe_refresh_layout); + rvGps = findViewById(R.id.rv_gps_list); + rvGps.setLayoutManager(new LinearLayoutManager(this)); + LogUtils.i("GpsListActivity", "控件初始化完成"); + } + + private void initData() { + LogUtils.i("GpsListActivity", "开始加载GPS数据库数据"); + refreshGpsData(); + } + + private void setSwipeRefreshListener() { + swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + refreshGpsData(); + } + }); + } + + private void refreshGpsData() { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + final List gpsDataList = GpsUtil.queryAllGps(GpsListActivity.this); + LogUtils.i("GpsListActivity", "查询GPS数据条数:" + gpsDataList.size()); + + adapter = new GpsAdapter(GpsListActivity.this, gpsDataList); + rvGps.setAdapter(adapter); + swipeRefresh.setRefreshing(false); + } + }, 500); + } + +} + diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/SettingsActivity.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/SettingsActivity.java new file mode 100644 index 0000000..feb3d3c --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/activities/SettingsActivity.java @@ -0,0 +1,33 @@ +package cc.winboll.studio.bytheway.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.CompoundButton; +import android.widget.Switch; +import cc.winboll.studio.bytheway.R; +import cc.winboll.studio.bytheway.services.GpsReceiveService; + +public class SettingsActivity extends Activity { + + public static final String TAG = "SettingsActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + Switch swGpsReceiveService = findViewById(R.id.sw_gps_receive_service); + swGpsReceiveService.setChecked(GpsReceiveService.isServiceShouldRun(SettingsActivity.this)); + swGpsReceiveService.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + GpsReceiveService.startGpsReceiveService(SettingsActivity.this); + } else { + GpsReceiveService.stopGpsReceiveService(SettingsActivity.this); + } + } + }); + } + +} diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/adapters/GpsAdapter.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/adapters/GpsAdapter.java new file mode 100644 index 0000000..f3b2be9 --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/adapters/GpsAdapter.java @@ -0,0 +1,64 @@ +package cc.winboll.studio.bytheway.adapters; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import cc.winboll.studio.bytheway.R; +import cc.winboll.studio.bytheway.models.GpsBean; +import cc.winboll.studio.bytheway.utils.GpsUtil; +import java.util.List; + +/** + * @Author 豆包&BigPickle&ZhanGSKen + * @Date 2026/07/02 15:10 + */ +public class GpsAdapter extends RecyclerView.Adapter { + + private final Context context; + private final List gpsList; + + public GpsAdapter(Context context, List gpsList) { + this.context = context; + this.gpsList = gpsList; + } + + @NonNull + @Override + public GpsHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(context).inflate(R.layout.item_gps, parent, false); + return new GpsHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull GpsHolder holder, int position) { + GpsBean bean = gpsList.get(position); + + String time = GpsUtil.formatTime(bean.getReceiveTime()); + holder.tvTime.setText("接收时间:" + time); + + holder.tvLatLng.setText(GpsUtil.formatLatLng(bean.getLatitude(), bean.getLongitude())); + + holder.tvOther.setText(GpsUtil.formatOther(bean.getAccuracy(), bean.getAltitude(), bean.getSpeed())); + } + + @Override + public int getItemCount() { + return gpsList.size(); + } + + public static class GpsHolder extends RecyclerView.ViewHolder { + TextView tvTime, tvLatLng, tvOther; + + public GpsHolder(@NonNull View itemView) { + super(itemView); + tvTime = itemView.findViewById(R.id.tv_time); + tvLatLng = itemView.findViewById(R.id.tv_lat_lng); + tvOther = itemView.findViewById(R.id.tv_other); + } + } +} + diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/models/AnchorPositionBean.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/models/AnchorPositionBean.java new file mode 100644 index 0000000..f951d14 --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/models/AnchorPositionBean.java @@ -0,0 +1,80 @@ +package cc.winboll.studio.bytheway.models; + +/** + * @Author 豆包&BigPickle&ZhanGSKen + * @Date 2026/07/10 19:46 + * @Describe 锚点位置数据模型 + * 存储轨迹标记锚点:经纬度、排序序号、自定义备注信息 + * @author 豆包&ZhanGSKen + * 创建时间:2026-07-10 13:45:00 + * 最后编辑时间:2026-07-10 13:48:00 + */ +public class AnchorPositionBean { + + public static final String TAG = "AnchorPositionBean"; + + // 数据库自增主键ID + private int id; + // 锚点纬度 + private double latitude; + // 锚点经度 + private double longitude; + // 锚点排序序号,用于轨迹先后展示 + private int sortIndex; + // 锚点备注文字 + private String remark; + + // 无参构造(SQLite反射实例化必需) + public AnchorPositionBean() { + + } + + // 全参构造 + public AnchorPositionBean(final double latitude, final double longitude, final int sortIndex, final String remark) { + this.latitude = latitude; + this.longitude = longitude; + this.sortIndex = sortIndex; + this.remark = remark; + } + + public int getId() { + return id; + } + + public void setId(final int id) { + this.id = id; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(final double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(final double longitude) { + this.longitude = longitude; + } + + public int getSortIndex() { + return sortIndex; + } + + public void setSortIndex(final int sortIndex) { + this.sortIndex = sortIndex; + } + + public String getRemark() { + return remark; + } + + public void setRemark(final String remark) { + this.remark = remark; + } +} + diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/models/GpsBean.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/models/GpsBean.java new file mode 100644 index 0000000..0960ee1 --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/models/GpsBean.java @@ -0,0 +1,44 @@ +package cc.winboll.studio.bytheway.models; + +import java.util.Date; +/** + * @Author 豆包&BigPickle&ZhanGSKen + * @Date 2026/07/02 14:56 + */ +public class GpsBean { + private int id; + private double latitude; //纬度 + private double longitude; //经度 + private float accuracy; //精度 + private double altitude; //海拔 + private float speed; //速度 + private long receiveTime; //接收时间戳 + + public GpsBean() {} + + public GpsBean(double latitude, double longitude, float accuracy, double altitude, float speed, long receiveTime) { + this.latitude = latitude; + this.longitude = longitude; + this.accuracy = accuracy; + this.altitude = altitude; + this.speed = speed; + this.receiveTime = receiveTime; + } + + //get set 方法 + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public double getLatitude() { return latitude; } + public void setLatitude(double latitude) { this.latitude = latitude; } + public double getLongitude() { return longitude; } + public void setLongitude(double longitude) { this.longitude = longitude; } + public float getAccuracy() { return accuracy; } + public void setAccuracy(float accuracy) { this.accuracy = accuracy; } + public double getAltitude() { return altitude; } + public void setAltitude(double altitude) { this.altitude = altitude; } + public float getSpeed() { return speed; } + public void setSpeed(float speed) { this.speed = speed; } + public long getReceiveTime() { return receiveTime; } + public void setReceiveTime(long receiveTime) { this.receiveTime = receiveTime; } +} + diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/receivers/ServiceCheckReceiver.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/receivers/ServiceCheckReceiver.java new file mode 100644 index 0000000..fe1dcc4 --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/receivers/ServiceCheckReceiver.java @@ -0,0 +1,22 @@ +package cc.winboll.studio.bytheway.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import cc.winboll.studio.bytheway.services.GpsReceiveService; +import cc.winboll.studio.libappbase.LogUtils; + +/** + * 开机广播接收器,系统启动完成后检查GPS服务SP标记并自动恢复运行 + */ +public class ServiceCheckReceiver extends BroadcastReceiver { + + public static final String TAG = "ServiceCheckReceiver"; + + @Override + public void onReceive(final Context context, final Intent intent) { + final String action = intent.getAction(); + LogUtils.i(TAG, "收到广播: " + action); + GpsReceiveService.checkAndStartService(context); + } +} diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/services/GpsReceiveService.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/services/GpsReceiveService.java new file mode 100644 index 0000000..04d337c --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/services/GpsReceiveService.java @@ -0,0 +1,282 @@ +package cc.winboll.studio.bytheway.services; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.widget.Toast; +import cc.winboll.studio.bytheway.MainActivity; +import cc.winboll.studio.bytheway.R; +import cc.winboll.studio.bytheway.models.GpsBean; +import cc.winboll.studio.bytheway.utils.GpsUtil; +import cc.winboll.studio.libappbase.LogUtils; + +/** + * @Author 豆包&BigPickle&ZhanGSKen + * @Date 2026/07/02 15:33 + * @Describe GPS定位接收服务,通过LocationManager获取GPS数据并持久化存入SQLite数据库。单例独占模式,自带重复启动拦截,对外静态统一启停接口 + */ +public class GpsReceiveService extends Service { + + public static final String TAG = "GpsReceiveService"; + private static final int NOTIFICATION_ID = 1001; + private static final String CHANNEL_ID = "gps_receive_service_channel"; + private static final String SP_NAME = "gps_receive_service"; + private static final String KEY_SHOULD_RUN = "service_should_run"; + + private static boolean serviceRunning = false; + private static GpsReceiveService instance; + private int gpsReceiveCount = 0; + private GpsBean currentGpsBean; + private LocationManager locationManager; + private LocationListener locationListener; + + /** + * 获取GPS定位服务运行状态 + * @return true=服务正在运行, false=服务未运行 + */ + public static boolean isServiceRunning() { + return serviceRunning; + } + + /** + * 获取当前最新的GPS定位数据 + * @return 当前GPS数据,未收到定位时返回null + */ + public static GpsBean getCurrentGpsBean() { + return serviceRunning ? instance.currentGpsBean : null; + } + + private static SharedPreferences getSp(final Context context) { + return context.getApplicationContext().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); + } + + public static boolean isServiceShouldRun(final Context context) { + return getSp(context).getBoolean(KEY_SHOULD_RUN, false); + } + + private static void setServiceShouldRun(final Context context, final boolean shouldRun) { + getSp(context).edit().putBoolean(KEY_SHOULD_RUN, shouldRun).apply(); + } + + /** + * 检查SP标记,若设置为运行且服务未运行则自动启动 + */ + public static void checkAndStartService(final Context context) { + if (isServiceShouldRun(context) && !serviceRunning) { + startGpsReceiveService(context); + } + } + + /** + * 外部统一静态启动服务 + */ + public static void startGpsReceiveService(final Context context) { + if (serviceRunning && instance != null) { + LogUtils.i("GpsReceiveService", "服务已运行,无需重复启动"); + return; + } + setServiceShouldRun(context, true); + final Intent intent = new Intent(context, GpsReceiveService.class); + context.startService(intent); + LogUtils.i("GpsReceiveService", "GPS定位消息接收服务启动请求发送"); + } + + /** + * 外部统一静态关闭服务 + */ + public static void stopGpsReceiveService(final Context context) { + setServiceShouldRun(context, false); + if (!serviceRunning) { + LogUtils.i("GpsReceiveService", "服务未运行,无需关闭"); + return; + } + final Intent intent = new Intent(context, GpsReceiveService.class); + context.stopService(intent); + LogUtils.i("GpsReceiveService", "GPS定位消息接收服务停止请求发送"); + } + + @Override + public void onCreate() { + super.onCreate(); + if (serviceRunning && instance != null) { + LogUtils.i("GpsReceiveService", "检测到重复实例,停止当前实例"); + stopSelf(); + return; + } + instance = this; + serviceRunning = true; + gpsReceiveCount = 0; + currentGpsBean = null; + startGpsLocationUpdates(); + startForeground(NOTIFICATION_ID, createNotification()); + LogUtils.i("GpsReceiveService", "GPS定位消息接收服务创建完成"); + Toast.makeText(this, "GPS定位消息接收服务已开启", Toast.LENGTH_SHORT).show(); + } + + private Notification createNotification() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "GPS定位消息接收服务", + NotificationManager.IMPORTANCE_LOW + ); + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.createNotificationChannel(channel); + } + String contentText; + if (currentGpsBean != null) { + contentText = "已接收 " + gpsReceiveCount + " 次 | " + + "纬度:" + currentGpsBean.getLatitude() + + " 经度:" + currentGpsBean.getLongitude() + + " 精度:" + currentGpsBean.getAccuracy() + "m"; + } else { + contentText = "等待GPS定位数据..."; + } + return new Notification.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.app_name)) + .setContentText(contentText) + .setSmallIcon(android.R.drawable.ic_menu_mylocation) + .setContentIntent(createContentIntent()) + .setOngoing(true) + .build(); + } + + /** + * 创建通知点击跳转主窗口的PendingIntent + */ + private PendingIntent createContentIntent() { + final Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE); + } + + /** + * 更新前台服务通知栏显示 + */ + private void updateNotification() { + final NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.notify(NOTIFICATION_ID, createNotification()); + } + + /** + * 启动GPS定位更新监听 + */ + private void startGpsLocationUpdates() { + locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); + locationListener = new LocationListener() { + @Override + public void onLocationChanged(final Location location) { + handleGpsLocation(location); + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) { + LogUtils.d("GpsReceiveService", "GPS状态变化: " + provider + ", status=" + status); + } + + @Override + public void onProviderEnabled(final String provider) { + LogUtils.d("GpsReceiveService", "GPS提供者已启用: " + provider); + } + + @Override + public void onProviderDisabled(final String provider) { + LogUtils.d("GpsReceiveService", "GPS提供者已禁用: " + provider); + } + }; + try { + // 最小时间间隔1秒,最小距离间隔0米,确保每次位置更新都能接收 + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 5000L, + 0f, + locationListener + ); + LogUtils.i("GpsReceiveService", "GPS定位监听已启动"); + } catch (SecurityException e) { + LogUtils.e("GpsReceiveService", "缺少GPS定位权限: " + e.getMessage()); + } catch (Exception e) { + LogUtils.e("GpsReceiveService", "启动GPS定位监听失败: " + e.getMessage()); + } + } + + /** + * 处理GPS定位数据,写入SQLite数据库并更新通知栏 + */ + private void handleGpsLocation(final Location location) { + final GpsBean bean = new GpsBean(); + bean.setLatitude(location.getLatitude()); + bean.setLongitude(location.getLongitude()); + bean.setAccuracy(location.getAccuracy()); + bean.setAltitude(location.getAltitude()); + bean.setSpeed(location.getSpeed()); + bean.setReceiveTime(System.currentTimeMillis()); + currentGpsBean = bean; + GpsUtil.insertGps(getApplicationContext(), bean); + gpsReceiveCount++; + updateNotification(); + LogUtils.d("GpsReceiveService", "收到GPS定位,已保存数据,累计接收 " + gpsReceiveCount + " 次"); + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (!isServiceShouldRun(this)) { + LogUtils.i("GpsReceiveService", "SP标记为不运行,停止服务"); + stopSelf(); + return START_NOT_STICKY; + } + if (serviceRunning && instance != null) { + LogUtils.i("GpsReceiveService", "服务已在运行,忽略重复启动命令"); + return START_STICKY; + } + return START_STICKY; + } + + @Override + public IBinder onBind(final Intent intent) { + return null; + } + + @Override + public void onTaskRemoved(final Intent rootIntent) { + super.onTaskRemoved(rootIntent); + LogUtils.i("GpsReceiveService", "应用窗口被移除,前台服务继续运行"); + } + + @Override + public void onDestroy() { + stopGpsLocationUpdates(); + stopForeground(true); + serviceRunning = false; + instance = null; + LogUtils.i("GpsReceiveService", "GPS接收服务已销毁,定位监听已停止"); + Toast.makeText(this, "GPS定位服务已关闭", Toast.LENGTH_SHORT).show(); + super.onDestroy(); + } + + /** + * 停止GPS定位更新监听 + */ + private void stopGpsLocationUpdates() { + if (locationManager != null && locationListener != null) { + try { + locationManager.removeUpdates(locationListener); + LogUtils.i("GpsReceiveService", "GPS定位监听已移除"); + } catch (SecurityException e) { + LogUtils.e("GpsReceiveService", "移除GPS监听异常: " + e.getMessage()); + } + } + } + +} diff --git a/bytheway/src/main/java/cc/winboll/studio/bytheway/utils/GpsUtil.java b/bytheway/src/main/java/cc/winboll/studio/bytheway/utils/GpsUtil.java new file mode 100644 index 0000000..8307359 --- /dev/null +++ b/bytheway/src/main/java/cc/winboll/studio/bytheway/utils/GpsUtil.java @@ -0,0 +1,247 @@ +package cc.winboll.studio.bytheway.utils; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import cc.winboll.studio.bytheway.models.GpsBean; +import cc.winboll.studio.libappbase.LogUtils; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * GPS数据处理模块 + * 集合GPS数据解析、格式化、数据库存取等所有操作,外部统一通过本类访问 + * @author ZhanGSKen + */ +public class GpsUtil { + + private static final String TAG = "GpsUtil"; + + // Intent广播中的GPS数据键名 + public static final String KEY_LATITUDE = "latitude"; + public static final String KEY_LONGITUDE = "longitude"; + public static final String KEY_ACCURACY = "accuracy"; + public static final String KEY_ALTITUDE = "altitude"; + public static final String KEY_SPEED = "speed"; + public static final String KEY_RECEIVE_TIME = "receive_time"; + + // 时间格式 + private static final String FORMAT_TIME = "yyyy-MM-dd HH:mm:ss"; + + // ======================== 数据解析方法 ======================== + + /** + * 从Intent广播数据中解析GPS定位信息,构造GpsBean对象 + * @param intent GPS定位广播Intent + * @return 解析后的GpsBean对象 + */ + public static GpsBean parseFromIntent(final Intent intent) { + final double latitude = intent.getDoubleExtra(KEY_LATITUDE, 0); + final double longitude = intent.getDoubleExtra(KEY_LONGITUDE, 0); + final float accuracy = intent.getFloatExtra(KEY_ACCURACY, 0); + final double altitude = intent.getDoubleExtra(KEY_ALTITUDE, 0); + final float speed = intent.getFloatExtra(KEY_SPEED, 0); + final long time = intent.getLongExtra(KEY_RECEIVE_TIME, System.currentTimeMillis()); + + final GpsBean bean = new GpsBean(); + bean.setLatitude(latitude); + bean.setLongitude(longitude); + bean.setAccuracy(accuracy); + bean.setAltitude(altitude); + bean.setSpeed(speed); + bean.setReceiveTime(time); + return bean; + } + + /** + * 从数据库游标中解析GPS定位信息,构造GpsBean对象 + * 游标字段顺序:id, latitude, longitude, accuracy, altitude, speed, receive_time + * @param cursor 数据库查询游标(需已定位到目标行) + * @return 解析后的GpsBean对象 + */ + public static GpsBean parseFromCursor(final Cursor cursor) { + final GpsBean bean = new GpsBean(); + bean.setId(cursor.getInt(0)); + bean.setLatitude(cursor.getDouble(1)); + bean.setLongitude(cursor.getDouble(2)); + bean.setAccuracy(cursor.getFloat(3)); + bean.setAltitude(cursor.getDouble(4)); + bean.setSpeed(cursor.getFloat(5)); + bean.setReceiveTime(cursor.getLong(6)); + return bean; + } + + // ======================== 格式化方法 ======================== + + /** + * 将时间戳格式化为可读时间字符串 + * @param timestamp 毫秒时间戳 + * @return 格式化后的时间字符串 + */ + public static String formatTime(final long timestamp) { + final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT_TIME, Locale.getDefault()); + return sdf.format(new Date(timestamp)); + } + + /** + * 格式化经纬度显示文本 + * @param latitude 纬度 + * @param longitude 经度 + * @return 格式化后的经纬度字符串 + */ + public static String formatLatLng(final double latitude, final double longitude) { + return "纬度:" + latitude + " 经度:" + longitude; + } + + /** + * 格式化精度、海拔、速度显示文本 + * @param accuracy 精度(米) + * @param altitude 海拔(米) + * @param speed 速度(m/s) + * @return 格式化后的字符串 + */ + public static String formatOther(final float accuracy, final double altitude, final float speed) { + return "精度:" + accuracy + "m 海拔:" + altitude + "m 速度:" + speed + "m/s"; + } + + /** + * 将速度从m/s转换为km/h + * @param speedMps 速度(m/s) + * @return 速度(km/h) + */ + public static float speedToKmh(final float speedMps) { + return speedMps * 3.6f; + } + + /** + * 格式化速度显示文本(km/h) + * @param speedMps 速度(m/s) + * @return 格式化后的速度字符串 + */ + public static String formatSpeed(final float speedMps) { + return speedToKmh(speedMps) + "km/h"; + } + + // ======================== 数据库操作方法 ======================== + + /** + * 插入单条GPS定位数据 + * @param context 上下文 + * @param bean GPS数据对象 + */ + public static void insertGps(final Context context, final GpsBean bean) { + final GpsDbHelper helper = new GpsDbHelper(context); + final SQLiteDatabase db = helper.getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put("latitude", bean.getLatitude()); + values.put("longitude", bean.getLongitude()); + values.put("accuracy", bean.getAccuracy()); + values.put("altitude", bean.getAltitude()); + values.put("speed", bean.getSpeed()); + values.put("receive_time", bean.getReceiveTime()); + db.insert(GpsDbHelper.TABLE_GPS, null, values); + LogUtils.i(TAG, "GPS数据插入成功"); + db.close(); + } + + /** + * 查询全部GPS记录,按时间倒序排列 + * @param context 上下文 + * @return GPS数据列表 + */ + public static List queryAllGps(final Context context) { + final List list = new ArrayList(); + final GpsDbHelper helper = new GpsDbHelper(context); + final SQLiteDatabase db = helper.getReadableDatabase(); + final Cursor cursor = db.query(GpsDbHelper.TABLE_GPS, null, null, null, null, null, "receive_time DESC"); + if (cursor.moveToFirst()) { + do { + list.add(parseFromCursor(cursor)); + } while (cursor.moveToNext()); + } + LogUtils.i(TAG, "查询全部GPS条数:" + list.size()); + cursor.close(); + db.close(); + return list; + } + + /** + * 根据时间区间查询GPS轨迹 + * @param context 上下文 + * @param startTime 起始时间戳 + * @param endTime 结束时间戳 + * @return GPS数据列表 + */ + public static List queryByTime(final Context context, final long startTime, final long endTime) { + final List list = new ArrayList(); + final GpsDbHelper helper = new GpsDbHelper(context); + final SQLiteDatabase db = helper.getReadableDatabase(); + final String where = "receive_time >= ? AND receive_time <= ?"; + final String[] args = {String.valueOf(startTime), String.valueOf(endTime)}; + final Cursor cursor = db.query(GpsDbHelper.TABLE_GPS, null, where, args, null, null, "receive_time ASC"); + if (cursor.moveToFirst()) { + do { + list.add(parseFromCursor(cursor)); + } while (cursor.moveToNext()); + } + LogUtils.i(TAG, "时间段查询GPS条数:" + list.size()); + cursor.close(); + db.close(); + return list; + } + + /** + * 清空所有本地GPS历史数据 + * @param context 上下文 + */ + public static void clearAllGps(final Context context) { + final GpsDbHelper helper = new GpsDbHelper(context); + final SQLiteDatabase db = helper.getWritableDatabase(); + db.delete(GpsDbHelper.TABLE_GPS, null, null); + LogUtils.i(TAG, "已清空全部GPS数据库数据"); + db.close(); + } + + // ======================== 内部类:数据库管理 ======================== + + /** + * SQLite数据库创建、版本升级管理 + */ + private static class GpsDbHelper extends SQLiteOpenHelper { + + private static final String DB_NAME = "GpsLocation.db"; + private static final int DB_VERSION = 1; + private static final String TABLE_GPS = "gps_table"; + + private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_GPS + " (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "latitude REAL," + + "longitude REAL," + + "accuracy REAL," + + "altitude REAL," + + "speed REAL," + + "receive_time INTEGER" + + ")"; + + public GpsDbHelper(final Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(final SQLiteDatabase db) { + db.execSQL(CREATE_TABLE); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_GPS); + onCreate(db); + } + } +} diff --git a/bytheway/src/main/res/drawable-v24/ic_launcher_foreground.xml b/bytheway/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/bytheway/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/bytheway/src/main/res/drawable/ic_launcher_background.xml b/bytheway/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/bytheway/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bytheway/src/main/res/layout/activity_about.xml b/bytheway/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..5cbe3fd --- /dev/null +++ b/bytheway/src/main/res/layout/activity_about.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/bytheway/src/main/res/layout/activity_gps_list.xml b/bytheway/src/main/res/layout/activity_gps_list.xml new file mode 100644 index 0000000..fa608fa --- /dev/null +++ b/bytheway/src/main/res/layout/activity_gps_list.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/bytheway/src/main/res/layout/activity_main.xml b/bytheway/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..5fb6836 --- /dev/null +++ b/bytheway/src/main/res/layout/activity_main.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bytheway/src/main/res/layout/activity_settings.xml b/bytheway/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..3cd40e5 --- /dev/null +++ b/bytheway/src/main/res/layout/activity_settings.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/bytheway/src/main/res/layout/item_gps.xml b/bytheway/src/main/res/layout/item_gps.xml new file mode 100644 index 0000000..b60c6b3 --- /dev/null +++ b/bytheway/src/main/res/layout/item_gps.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/bytheway/src/main/res/menu/activity_main.xml b/bytheway/src/main/res/menu/activity_main.xml new file mode 100644 index 0000000..fafb033 --- /dev/null +++ b/bytheway/src/main/res/menu/activity_main.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/bytheway/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/bytheway/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/bytheway/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bytheway/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/bytheway/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/bytheway/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bytheway/src/main/res/mipmap-hdpi/ic_launcher.png b/bytheway/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a2f5908 Binary files /dev/null and b/bytheway/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/bytheway/src/main/res/mipmap-hdpi/ic_launcher_round.png b/bytheway/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/bytheway/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/bytheway/src/main/res/mipmap-mdpi/ic_launcher.png b/bytheway/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ff10afd Binary files /dev/null and b/bytheway/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/bytheway/src/main/res/mipmap-mdpi/ic_launcher_round.png b/bytheway/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/bytheway/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/bytheway/src/main/res/mipmap-xhdpi/ic_launcher.png b/bytheway/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..dcd3cd8 Binary files /dev/null and b/bytheway/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/bytheway/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/bytheway/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..459ca60 Binary files /dev/null and b/bytheway/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/bytheway/src/main/res/mipmap-xxhdpi/ic_launcher.png b/bytheway/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..8ca12fe Binary files /dev/null and b/bytheway/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/bytheway/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/bytheway/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e19b41 Binary files /dev/null and b/bytheway/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/bytheway/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/bytheway/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/bytheway/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/bytheway/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/bytheway/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4c19a13 Binary files /dev/null and b/bytheway/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/bytheway/src/main/res/values-night/colors.xml b/bytheway/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..f0319d6 --- /dev/null +++ b/bytheway/src/main/res/values-night/colors.xml @@ -0,0 +1,15 @@ + + + #42A5F5 + #1565C0 + #FFD740 + #121212 + #E0E0E0 + #FFFFFF + #1E3A5F + #00E676 + #121212 + #1565C0 + #FFFFFF + #B3FFFFFF + \ No newline at end of file diff --git a/bytheway/src/main/res/values-night/styles.xml b/bytheway/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..ae07b68 --- /dev/null +++ b/bytheway/src/main/res/values-night/styles.xml @@ -0,0 +1,29 @@ + + + + + + + + +