diff --git a/aes/build.properties b/aes/build.properties index 597a06b..2e302cb 100644 --- a/aes/build.properties +++ b/aes/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat May 03 11:34:58 GMT 2025 +#Sun May 04 06:42:28 GMT 2025 stageCount=1 libraryProject=libaes baseVersion=15.6 publishVersion=15.6.0 -buildCount=9 +buildCount=10 baseBetaVersion=15.6.1 diff --git a/libaes/build.properties b/libaes/build.properties index 597a06b..2e302cb 100644 --- a/libaes/build.properties +++ b/libaes/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat May 03 11:34:58 GMT 2025 +#Sun May 04 06:42:28 GMT 2025 stageCount=1 libraryProject=libaes baseVersion=15.6 publishVersion=15.6.0 -buildCount=9 +buildCount=10 baseBetaVersion=15.6.1 diff --git a/settings.gradle-demo b/settings.gradle-demo index 2dd7da1..38915bf 100644 --- a/settings.gradle-demo +++ b/settings.gradle-demo @@ -37,6 +37,10 @@ //include ':mymessagemanager' //rootProject.name = "mymessagemanager" +// TimeStamp 项目编译设置 +//include ':timestamp' +//rootProject.name = "timestamp" + // AndroidDemo 项目编译设置 //include ':androiddemo' //rootProject.name = "androiddemo" diff --git a/timestamp/.gitignore b/timestamp/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/timestamp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/timestamp/app_update_description.txt b/timestamp/app_update_description.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/timestamp/app_update_description.txt @@ -0,0 +1 @@ + diff --git a/timestamp/build.gradle b/timestamp/build.gradle new file mode 100644 index 0000000..03359df --- /dev/null +++ b/timestamp/build.gradle @@ -0,0 +1,72 @@ +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 { + compileSdkVersion 32 + buildToolsVersion "32.0.0" + + defaultConfig { + applicationId "cc.winboll.studio.timestamp" + minSdkVersion 24 + targetSdkVersion 30 + versionCode 1 + // versionName 更新后需要手动设置 + // .winboll/winbollBuildProps.properties 文件的 stageCount=0 + // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" + versionName "15.0" + if(true) { + versionName = genVersionName("${versionName}") + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar']) + api 'cc.winboll.studio:libaes:15.6.0' + api 'cc.winboll.studio:libapputils:15.3.4' + api 'cc.winboll.studio:libappbase:15.7.6' + + // 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.github.getActivity:ToastUtils:10.5' + // 网络连接类库 + api 'com.squareup.okhttp3:okhttp:4.4.1' + // 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' +} diff --git a/timestamp/build.properties b/timestamp/build.properties new file mode 100644 index 0000000..e6ed333 --- /dev/null +++ b/timestamp/build.properties @@ -0,0 +1,8 @@ +#Created by .winboll/winboll_app_build.gradle +#Tue May 06 11:17:44 HKT 2025 +stageCount=1 +libraryProject= +baseVersion=15.0 +publishVersion=15.0.0 +buildCount=0 +baseBetaVersion=15.0.1 diff --git a/timestamp/proguard-rules.pro b/timestamp/proguard-rules.pro new file mode 100644 index 0000000..64b4a05 --- /dev/null +++ b/timestamp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/timestamp/src/beta/AndroidManifest.xml b/timestamp/src/beta/AndroidManifest.xml new file mode 100644 index 0000000..ee78d9f --- /dev/null +++ b/timestamp/src/beta/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/timestamp/src/beta/res/values/strings.xml b/timestamp/src/beta/res/values/strings.xml new file mode 100644 index 0000000..fbae746 --- /dev/null +++ b/timestamp/src/beta/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + TimeStamp + + + diff --git a/timestamp/src/main/AndroidManifest.xml b/timestamp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6d48e55 --- /dev/null +++ b/timestamp/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/App.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/App.java new file mode 100644 index 0000000..430bc2c --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/App.java @@ -0,0 +1,345 @@ +package cc.winboll.studio.timestamp; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +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.Gravity; +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.timestamp.R; +import cc.winboll.studio.libappbase.GlobalApplication; +import com.hjq.toast.ToastUtils; +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.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()); + + @Override + public void onCreate() { + super.onCreate(); + + // 初始化 Toast 框架 + ToastUtils.init(this); + // 设置 Toast 布局样式 + //ToastUtils.setView(R.layout.view_toast); + //ToastUtils.setStyle(new WhiteToastStyle()); + ToastUtils.setGravity(Gravity.BOTTOM, 0, 200); + + //CrashHandler.getInstance().registerGlobal(this); + //CrashHandler.getInstance().registerPart(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/timestamp/src/main/java/cc/winboll/studio/timestamp/AssistantService.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/AssistantService.java new file mode 100644 index 0000000..462a8ac --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/AssistantService.java @@ -0,0 +1,113 @@ +package cc.winboll.studio.timestamp; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 09:49 + * @Describe MainService 守护进程服务 + */ +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import cc.winboll.studio.timestamp.AssistantService; +import cc.winboll.studio.timestamp.MainService; +import cc.winboll.studio.timestamp.models.AppConfigsModel; +import cc.winboll.studio.timestamp.utils.AppConfigsUtil; +import cc.winboll.studio.timestamp.utils.ServiceUtil; + +public class AssistantService extends Service { + + public static final String TAG = "AssistantService"; + + //MyBinder mMyBinder; + MyServiceConnection mMyServiceConnection; + volatile boolean mIsThreadAlive; + + @Override + public IBinder onBind(Intent intent) { + //return mMyBinder; + return null; + } + + @Override + public void onCreate() { + //LogUtils.d(TAG, "call onCreate()"); + super.onCreate(); + + //mMyBinder = new MyBinder(); + if (mMyServiceConnection == null) { + mMyServiceConnection = new MyServiceConnection(); + } + // 设置运行参数 + mIsThreadAlive = false; + run(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + //LogUtils.d(TAG, "call onStartCommand(...)"); + run(); + AppConfigsModel appConfigs = AppConfigsUtil.getInstance(AssistantService.this).loadAppConfigs(); + return appConfigs.isEnableService() ? Service.START_STICKY: super.onStartCommand(intent, flags, startId); + } + + /*class MyBinder extends IMyAidlInterface.Stub { + @Override + public String getServiceName() { + return AssistantService.class.getSimpleName(); + } + }*/ + + @Override + public void onDestroy() { + //LogUtils.d(TAG, "call onDestroy()"); + mIsThreadAlive = false; + super.onDestroy(); + } + + // 运行服务内容 + // + void run() { + //LogUtils.d(TAG, "call run()"); + AppConfigsModel appConfigs = AppConfigsUtil.getInstance(AssistantService.this).loadAppConfigs(); + if (appConfigs.isEnableService()) { + if (mIsThreadAlive == false) { + // 设置运行状态 + mIsThreadAlive = true; + // 唤醒和绑定主进程 + wakeupAndBindMain(); + } + } + } + + // 唤醒和绑定主进程 + // + void wakeupAndBindMain() { + if (ServiceUtil.isServiceAlive(getApplicationContext(), MainService.class.getName()) == false) { + //LogUtils.d(TAG, "wakeupAndBindMain() Wakeup... ControlCenterService"); + startForegroundService(new Intent(AssistantService.this, MainService.class)); + } + //LogUtils.d(TAG, "wakeupAndBindMain() Bind... ControlCenterService"); + bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT); + } + + // 主进程与守护进程连接时需要用到此类 + // + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + //LogUtils.d(TAG, "call onServiceConnected(...)"); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + //LogUtils.d(TAG, "call onServiceDisconnected(...)"); + AppConfigsModel appConfigs = AppConfigsUtil.getInstance(AssistantService.this).loadAppConfigs(); + if (appConfigs.isEnableService()) { + wakeupAndBindMain(); + } + } + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/MainActivity.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/MainActivity.java new file mode 100644 index 0000000..2236bb2 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/MainActivity.java @@ -0,0 +1,100 @@ +package cc.winboll.studio.timestamp; + +import android.os.Bundle; +import android.view.View; +import android.widget.EditText; +import android.widget.Switch; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.LogView; +import cc.winboll.studio.timestamp.MainService; +import cc.winboll.studio.timestamp.R; +import cc.winboll.studio.timestamp.utils.AppConfigsUtil; +import com.hjq.toast.ToastUtils; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class MainActivity extends AppCompatActivity { + + public static final String TAG = "MainActivity"; + + EditText metTimeStampFormatString; + TextView mtvTimeStampFormatString; + EditText metTimeStampCopyFormatString; + TextView mtvTimeStampCopyFormatString; + + LogView mLogView; + Switch mswEnableMainService; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + metTimeStampFormatString = findViewById(R.id.et_timestampformatstring); + mtvTimeStampFormatString = findViewById(R.id.tv_timestampformatstring); + metTimeStampCopyFormatString = findViewById(R.id.et_timestampcopyformatstring); + mtvTimeStampCopyFormatString = findViewById(R.id.tv_timestampcopyformatstring); + + metTimeStampFormatString.setText(AppConfigsUtil.getInstance(this).getAppConfigsModel().getTimeStampFormatString()); + showPreViewResult(metTimeStampFormatString, mtvTimeStampFormatString); + metTimeStampCopyFormatString.setText(AppConfigsUtil.getInstance(this).getAppConfigsModel().getTimeStampCopyFormatString()); + showPreViewResult(metTimeStampCopyFormatString, mtvTimeStampCopyFormatString); + + mswEnableMainService = findViewById(R.id.activitymainSwitch1); + mswEnableMainService.setChecked(AppConfigsUtil.getInstance(this).loadAppConfigs().isEnableService()); + + mLogView = findViewById(R.id.logview); + + ToastUtils.show("onCreate"); + } + + + + @Override + protected void onResume() { + super.onResume(); + mLogView.start(); + } + + public void onSetMainServiceStatus(View view) { + MainService.setMainServiceStatus(this, mswEnableMainService.isChecked()); + } + + public void onSaveFormatString(View view) { + if(showPreViewResult(metTimeStampFormatString, mtvTimeStampFormatString)) { + AppConfigsUtil.getInstance(this).getAppConfigsModel().setTimeStampFormatString(metTimeStampFormatString.getText().toString()); + AppConfigsUtil.getInstance(this).saveAppConfigs(); + } + } + + public void onSaveCopyFormatString(View view) { + if(showPreViewResult(metTimeStampCopyFormatString, mtvTimeStampCopyFormatString)) { + AppConfigsUtil.getInstance(this).getAppConfigsModel().setTimeStampCopyFormatString(metTimeStampCopyFormatString.getText().toString()); + AppConfigsUtil.getInstance(this).saveAppConfigs(); + } + } + + boolean showPreViewResult(EditText etFormat, TextView tvShow) { + try { + long currentMillis = System.currentTimeMillis(); + Instant instant = Instant.ofEpochMilli(currentMillis); + LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + String szTimeStampFormatString = etFormat.getText().toString(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(szTimeStampFormatString); + String formattedDateTime = ldt.format(formatter); + tvShow.setText(formattedDateTime); + return true; + } catch (Exception e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + return false; + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/MainService.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/MainService.java new file mode 100644 index 0000000..b6862c3 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/MainService.java @@ -0,0 +1,227 @@ +package cc.winboll.studio.timestamp; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 09:47 + * @Describe 主要服务 + */ +import android.app.Notification; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.widget.RemoteViews; +import android.widget.TextView; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.timestamp.AssistantService; +import cc.winboll.studio.timestamp.MainService; +import cc.winboll.studio.timestamp.models.AppConfigsModel; +import cc.winboll.studio.timestamp.receivers.ButtonClickReceiver; +import cc.winboll.studio.timestamp.utils.AppConfigsUtil; +import cc.winboll.studio.timestamp.utils.NotificationHelper; +import cc.winboll.studio.timestamp.utils.ServiceUtil; +import cc.winboll.studio.timestamp.utils.TimeStampRemoteViewsUtil; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Timer; +import java.util.TimerTask; + +public class MainService extends Service { + + public static String TAG = "MainService"; + + public static final int MSG_UPDATE_TIMESTAMP = 0; + + ButtonClickReceiver mButtonClickReceiver; + NotificationHelper mNotificationHelper; + Notification mNotification; + RemoteViews mRemoteViews; + TextView mtvTimeStamp; + Timer mTimer; + private static boolean _mIsServiceAlive; + public static final String EXTRA_APKFILEPATH = "EXTRA_APKFILEPATH"; + final static int MSG_INSTALL_APK = 0; + MyHandler mMyHandler; + MyServiceConnection mMyServiceConnection; + MainActivity mInstallCompletedFollowUpActivity; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + // 创建 RemoteViews 对象,并使用包含自定义 View 的布局 + //mRemoteViews = new RemoteViews(getPackageName(), R.layout.remoteviews_timestamp); + + + // 创建广播接收器实例 + mButtonClickReceiver = new ButtonClickReceiver(); + + // 创建 IntentFilter 并设置要接收的广播动作 + IntentFilter filter = new IntentFilter(ButtonClickReceiver.BUTTON_COPYTIMESTAMP_ACTION); + + // 注册广播接收器 + registerReceiver(mButtonClickReceiver, filter); + + LogUtils.d(TAG, "onCreate()"); + _mIsServiceAlive = false; + + mMyHandler = new MyHandler(); + if (mMyServiceConnection == null) { + mMyServiceConnection = new MyServiceConnection(); + } + + run(); + } + + private void run() { + AppConfigsModel appConfigs = AppConfigsUtil.getInstance(MainService.this).loadAppConfigs(); + if (appConfigs.isEnableService()) { + if (_mIsServiceAlive == false) { + // 设置运行状态 + _mIsServiceAlive = true; + + // 显示前台通知栏 +// mNotificationHelper = new NotificationHelper(this); +// //notification = helper.showForegroundNotification(intent, getString(R.string.app_name), getString(R.string.text_aboutservernotification)); +// mNotification = mNotificationHelper.showCustomForegroundNotification(new Intent(this, MainActivity.class), mRemoteViews, mRemoteViews); +// startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, mNotification); + + // 唤醒守护进程 + wakeupAndBindAssistant(); + + LogUtils.d(TAG, "running..."); + + mTimer = new Timer(); + TimerTask task = new TimerTask() { + @Override + public void run() { + //System.out.println("定时任务执行了"); + mMyHandler.sendEmptyMessage(MSG_UPDATE_TIMESTAMP); + } + }; + // 延迟1秒后开始执行,之后每隔100毫秒执行一次 + mTimer.schedule(task, 1000, 100); + + + + } else { + LogUtils.d(TAG, "_mIsServiceAlive is " + Boolean.toString(_mIsServiceAlive)); + + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mTimer != null) { + mTimer.cancel(); + } + + _mIsServiceAlive = false; + LogUtils.d(TAG, "onDestroy()"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "onStartCommand"); + + run(); + AppConfigsModel appConfigs = AppConfigsUtil.getInstance(MainService.this).loadAppConfigs(); + + return appConfigs.isEnableService() ? Service.START_STICKY: super.onStartCommand(intent, flags, startId); + } + + public static void setMainServiceStatus(Context context, boolean isEnable) { + AppConfigsModel appConfigs = AppConfigsUtil.getInstance(context).loadAppConfigs(); + appConfigs.setIsEnableService(isEnable); + AppConfigsUtil.getInstance(context).saveAppConfigs(); + + Intent intent = new Intent(context, MainService.class); + if (isEnable) { + context.startService(intent); + } else { + context.stopService(intent); + } + } + +// 主进程与守护进程连接时需要用到此类 +// + private class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + //LogUtils.d(TAG, "call onServiceConnected(...)"); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + //LogUtils.d(TAG, "call onServiceConnected(...)"); + AppConfigsModel appConfigs = AppConfigsUtil.getInstance(MainService.this).loadAppConfigs(); + if (appConfigs.isEnableService()) { + // 唤醒守护进程 + wakeupAndBindAssistant(); + } + } + } + + // 唤醒和绑定守护进程 + // + void wakeupAndBindAssistant() { + if (ServiceUtil.isServiceAlive(getApplicationContext(), AssistantService.class.getName()) == false) { + startService(new Intent(MainService.this, AssistantService.class)); + //LogUtils.d(TAG, "call wakeupAndBindAssistant() : Binding... AssistantService"); + bindService(new Intent(MainService.this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT); + } + } + +// void updateTimeStamp() { +// long currentMillis = System.currentTimeMillis(); +// Instant instant = Instant.ofEpochMilli(currentMillis); +// LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); +// String szTimeStampFormatString = AppConfigs.getInstance(this).getTimeStampFormatString(); +// DateTimeFormatter formatter = DateTimeFormatter.ofPattern(szTimeStampFormatString); +// String formattedDateTime = ldt.format(formatter); +// //System.out.println(formattedDateTime); +// mRemoteViews.setTextViewText(R.id.tv_timestamp, formattedDateTime); +// notification = mNotificationHelper.showCustomForegroundNotification(intentMainService, mRemoteViews, mRemoteViews); +// //startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification); +// } +// + // + // 服务事务处理类 + // + class MyHandler extends Handler { + + public void handleMessage(Message message) { + switch (message.what) { + case MSG_UPDATE_TIMESTAMP: + { + long currentMillis = System.currentTimeMillis(); + Instant instant = Instant.ofEpochMilli(currentMillis); + LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + String szTimeStampFormatString = AppConfigsUtil.getInstance(MainService.this).getAppConfigsModel().getTimeStampFormatString(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(szTimeStampFormatString); + String formattedDateTime = ldt.format(formatter); + TimeStampRemoteViewsUtil.getInstance(MainService.this).showNotification(formattedDateTime); + + //LogUtils.d(TAG, "Hello, World"); + break; + } + default: + break; + } + super.handleMessage(message); + } + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/models/AppConfigsModel.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/models/AppConfigsModel.java new file mode 100644 index 0000000..8b87647 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/models/AppConfigsModel.java @@ -0,0 +1,95 @@ +package cc.winboll.studio.timestamp.models; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 09:51 + * @Describe 应用配置数据模型 + */ +import android.content.Context; +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import cc.winboll.studio.timestamp.models.AppConfigsModel; +import java.io.IOException; + +public class AppConfigsModel extends BaseBean { + + public static final String TAG = "AppConfigs"; + + // 是否启动服务 + boolean isEnableService; + // 时间戳显示格式 + String timeStampFormatString; + // 时间戳拷贝格式 + String timeStampCopyFormatString; + + public AppConfigsModel() { + this.isEnableService = false; + this.timeStampFormatString = "yyyy-MM-dd HH:mm:ss"; + this.timeStampCopyFormatString = "yyyy_MM_dd-HH_mm_ss"; + } + + public void setTimeStampCopyFormatString(String timeStampCopyFormatString) { + this.timeStampCopyFormatString = timeStampCopyFormatString; + } + + public String getTimeStampCopyFormatString() { + return timeStampCopyFormatString; + } + + public void setTimeStampFormatString(String timeStampFormatString) { + this.timeStampFormatString = timeStampFormatString; + } + + public String getTimeStampFormatString() { + return timeStampFormatString; + } + + public void setIsEnableService(boolean isEnableService) { + this.isEnableService = isEnableService; + } + + public boolean isEnableService() { + return isEnableService; + } + + @Override + public String getName() { + return AppConfigsModel.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("isEnableService").value(isEnableService()); + jsonWriter.name("timeStampFormatString").value(getTimeStampFormatString()); + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("isEnableService")) { + setIsEnableService(jsonReader.nextBoolean()); + } else if (name.equals("timeStampFormatString")) { + setTimeStampFormatString(jsonReader.nextString()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/receivers/ButtonClickReceiver.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/receivers/ButtonClickReceiver.java new file mode 100644 index 0000000..b5f286b --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/receivers/ButtonClickReceiver.java @@ -0,0 +1,45 @@ +package cc.winboll.studio.timestamp.receivers; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 11:35 + * @Describe ButtonClickReceiver + */ +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.widget.Toast; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.timestamp.utils.AppConfigsUtil; +import cc.winboll.studio.timestamp.utils.ClipboardUtil; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class ButtonClickReceiver extends BroadcastReceiver { + + public static final String TAG = "ButtonClickReceiver"; + + public static final String BUTTON_COPYTIMESTAMP_ACTION = "cc.winboll.studio.timestamp.receivers.ButtonClickReceiver.BUTTON_COPYTIMESTAMP_ACTION"; + + @Override + public void onReceive(Context context, Intent intent) { + LogUtils.d(TAG, "onReceive"); + if (intent.getAction().equals(BUTTON_COPYTIMESTAMP_ACTION)) { + // 在这里编写按钮点击后要执行的代码 + long currentMillis = System.currentTimeMillis(); + Instant instant = Instant.ofEpochMilli(currentMillis); + LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + String szTimeStampFormatString = AppConfigsUtil.getInstance(context).getAppConfigsModel().getTimeStampCopyFormatString(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(szTimeStampFormatString); + String formattedDateTime = ldt.format(formatter); + + ClipboardUtil.copyTextToClipboard(context, formattedDateTime); + + // 比如显示一个Toast + Toast.makeText(context, formattedDateTime + " 已复制", Toast.LENGTH_SHORT).show(); + } + } + +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/AppConfigsUtil.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/AppConfigsUtil.java new file mode 100644 index 0000000..55a9927 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/AppConfigsUtil.java @@ -0,0 +1,58 @@ +package cc.winboll.studio.timestamp.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 14:00 + * @Describe AppConfigsUtil + */ +import android.content.Context; +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import cc.winboll.studio.timestamp.models.AppConfigsModel; +import java.io.IOException; + +public class AppConfigsUtil { + + public static final String TAG = "AppConfigsUtil"; + + volatile static AppConfigsUtil _AppConfigsUtil; + Context mContext; + AppConfigsModel mAppConfigsModel; + + AppConfigsUtil(Context context) { + this.mContext = context; + } + + public synchronized static AppConfigsUtil getInstance(Context context){ + if(_AppConfigsUtil == null) { + _AppConfigsUtil = new AppConfigsUtil(context); + _AppConfigsUtil.loadAppConfigs(); + } + return _AppConfigsUtil; + } + + public AppConfigsModel getAppConfigsModel() { + return mAppConfigsModel; + } + + public AppConfigsModel loadAppConfigs() { + AppConfigsModel appConfigsModel = null; + appConfigsModel = AppConfigsModel.loadBean(mContext, AppConfigsModel.class); + if (appConfigsModel != null) { + mAppConfigsModel = appConfigsModel; + } else { + saveAppConfigs(new AppConfigsModel()); + _AppConfigsUtil = this; + } + return mAppConfigsModel; + } + + public void saveAppConfigs(AppConfigsModel appConfigsModel) { + AppConfigsModel.saveBean(mContext, appConfigsModel); + } + + public void saveAppConfigs() { + AppConfigsModel.saveBean(mContext, mAppConfigsModel); + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/ClipboardUtil.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/ClipboardUtil.java new file mode 100644 index 0000000..5e9be5c --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/ClipboardUtil.java @@ -0,0 +1,37 @@ +package cc.winboll.studio.timestamp.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/05/06 10:53 + * @Describe 剪贴板工具集 + */ +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Handler; + +public class ClipboardUtil { + public static final String TAG = "ClipboardUtil"; + + private static final long COPY_DELAY = 500; // 延迟 500 毫秒 + private static Handler handler = new Handler(); + + /** + * 拷贝文本到剪贴板 + * @param context 上下文 + * @param text 要拷贝的文本 + */ + public static void copyTextToClipboard(final Context context, final String text) { + handler.postDelayed(new Runnable() { + @Override + public void run() { + ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboardManager != null) { + ClipData clipData = ClipData.newPlainText("label", text); + clipboardManager.setPrimaryClip(clipData); + } + } + }, COPY_DELAY); + } +} + diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/FileUtil.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/FileUtil.java new file mode 100644 index 0000000..883e443 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/FileUtil.java @@ -0,0 +1,47 @@ +package cc.winboll.studio.timestamp.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 09:52 + * @Describe 文件管理工具类 + */ +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; + +public class FileUtil { + + public static final String TAG = "FileUtil"; + + // + // 把字符串写入文件,指定 UTF-8 编码 + // + public static void writeFile(String filePath, String content) throws IOException { + File file = new File(filePath); + FileOutputStream outputStream = new FileOutputStream(file); + OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + writer.write(content); + writer.close(); + } + + // + // 读取文件到字符串,指定 UTF-8 编码 + // + public static String readFile(String filePath) throws IOException { + File file = new File(filePath); + FileInputStream inputStream = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + StringBuilder content = new StringBuilder(); + int character; + while ((character = reader.read()) != -1) { + content.append((char) character); + } + reader.close(); + return content.toString(); + } + +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/NotificationHelper.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/NotificationHelper.java new file mode 100644 index 0000000..39c826c --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/NotificationHelper.java @@ -0,0 +1,223 @@ +package cc.winboll.studio.timestamp.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 10:36 + * @Describe 应用通知工具类 + */ +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.widget.RemoteViews; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.timestamp.R; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class NotificationHelper { + public static final String TAG = "NotificationHelper"; + + // 渠道ID和名称 + private static final String CHANNEL_ID_FOREGROUND = "foreground_channel"; + private static final String CHANNEL_NAME_FOREGROUND = "Foreground Service"; + private static final String CHANNEL_ID_TEMPORARY = "temporary_channel"; + private static final String CHANNEL_NAME_TEMPORARY = "Temporary Notifications"; + + // 通知ID + public static final int FOREGROUND_NOTIFICATION_ID = 1001; + public static final int TEMPORARY_NOTIFICATION_ID = 2001; + + private final Context mContext; + private final NotificationManager mNotificationManager; + + // 示例:维护当前使用的渠道ID列表 + // 键:渠道ID,值:渠道重要性级别 + Map activeChannelConfigs = new HashMap<>(); + + public NotificationHelper(Context context) { + mContext = context; + mNotificationManager = context.getSystemService(NotificationManager.class); + + // 初始化配置 + activeChannelConfigs.put( + CHANNEL_ID_FOREGROUND, + NotificationManager.IMPORTANCE_HIGH + ); + activeChannelConfigs.put( + CHANNEL_ID_TEMPORARY, + NotificationManager.IMPORTANCE_DEFAULT + ); + + createNotificationChannels(); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private void createNotificationChannels() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createForegroundChannel(); + createTemporaryChannel(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private void createForegroundChannel() { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID_FOREGROUND, + CHANNEL_NAME_FOREGROUND, + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("Persistent service notifications"); + channel.setSound(null, null); + channel.enableVibration(false); + mNotificationManager.createNotificationChannel(channel); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private void createTemporaryChannel() { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID_TEMPORARY, + CHANNEL_NAME_TEMPORARY, + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("Temporary alert notifications"); + channel.setSound(null, null); + channel.enableVibration(true); + channel.setVibrationPattern(new long[]{100, 200, 300, 400}); + channel.setBypassDnd(true); + mNotificationManager.createNotificationChannel(channel); + } + + // 显示常驻通知(通常用于前台服务) + public Notification showForegroundNotification(Intent intent, String title, String content) { + PendingIntent pendingIntent = createPendingIntent(intent); + + Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_FOREGROUND) + .setSmallIcon(R.drawable.ic_launcher) + .setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher)) + //.setContentTitle(title) + .setContentTitle(content) + //.setContentText(content) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .build(); + + mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification); + return notification; + } + + + // 显示常驻通知(通常用于前台服务) + public Notification showCustomForegroundNotification(Intent intent, RemoteViews contentView, RemoteViews bigContentView) { + PendingIntent pendingIntent = createPendingIntent(intent); + + + Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY) + .setSmallIcon(R.drawable.ic_launcher) + .setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher)) + //.setContentTitle(title) + .setContentIntent(pendingIntent) + .setContent(contentView) + .setCustomBigContentView(bigContentView) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .setOngoing(true) + .build(); + + mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification); + return notification; + } + + // 显示临时通知(自动消失) + public void showTemporaryNotification(Intent intent, String title, String content) { + showTemporaryNotification(intent, TEMPORARY_NOTIFICATION_ID, title, content); + } + + // 显示临时通知(自动消失) + public void showTemporaryNotification(Intent intent, int notificationID, String title, String content) { + PendingIntent pendingIntent = createPendingIntent(intent); + + Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY) + .setSmallIcon(R.drawable.ic_launcher) + .setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher)) + .setContentTitle(title) + .setContentText(content) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .setVibrate(new long[]{100, 200, 300, 400}) + .build(); + + mNotificationManager.notify(notificationID, notification); + } + + // 创建自定义布局通知(可扩展) + public void showCustomNotification(Intent intent, RemoteViews contentView, RemoteViews bigContentView) { + PendingIntent pendingIntent = createPendingIntent(intent); + + Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY) + .setSmallIcon(R.drawable.ic_launcher) + .setContentIntent(pendingIntent) + .setContent(contentView) + .setCustomBigContentView(bigContentView) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .build(); + + mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID + 1, notification); + } + + // 取消所有通知 + public void cancelAllNotifications() { + mNotificationManager.cancelAll(); + } + + // 取消指定通知 + public void cancelNotification(int notificationID) { + mNotificationManager.cancel(notificationID); + } + + // 创建PendingIntent(兼容不同API版本) + private PendingIntent createPendingIntent(Intent intent) { + int flags = PendingIntent.FLAG_UPDATE_CURRENT; +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { +// flags |= PendingIntent.FLAG_IMMUTABLE; +// } + return PendingIntent.getActivity( + mContext, + 0, + intent, + flags + ); + } + +// public void sendSMSReceivedMessage(int notificationID, String szPhone, String szBody) { +// Intent intent = new Intent(mContext, SMSActivity.class); +// intent.putExtra(SMSActivity.EXTRA_PHONE, szPhone); +// String szTitle = mContext.getString(R.string.text_smsfrom) + "<" + szPhone + ">"; +// String szContent = "[ " + szBody + " ]"; +// showTemporaryNotification(intent, notificationID, szTitle, szContent); +// } + + public void cleanOldChannels() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + List allChannels = mNotificationManager.getNotificationChannels(); + for (NotificationChannel channel : allChannels) { + LogUtils.d(TAG, "Clean channel : " + channel.getId()); + if (!activeChannelConfigs.containsKey(channel.getId())) { + // 安全删除渠道 + mNotificationManager.deleteNotificationChannel(channel.getId()); + LogUtils.d(TAG, String.format("Deleted Channel %s", channel.getId())); + } + } + } + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/ServiceUtil.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/ServiceUtil.java new file mode 100644 index 0000000..113c239 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/ServiceUtil.java @@ -0,0 +1,35 @@ +package cc.winboll.studio.timestamp.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 09:58 + * @Describe 应用服务管理类 + */ +import android.app.ActivityManager; +import android.content.Context; +import java.util.List; + +public class ServiceUtil { + + public final static String TAG = "ServiceUtil"; + + public static boolean isServiceAlive(Context context, String szServiceName) { + // 获取Activity管理者对象 + ActivityManager manager = (ActivityManager) context + .getSystemService(Context.ACTIVITY_SERVICE); + // 获取正在运行的服务(此处设置最多取1000个) + List runningServices = manager + .getRunningServices(1000); + if (runningServices.size() <= 0) { + return false; + } + // 遍历,若存在名字和传入的serviceName的一致则说明存在 + for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) { + if (runningServiceInfo.service.getClassName().equals(szServiceName)) { + return true; + } + } + + return false; + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/TimeStampRemoteViewsUtil.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/TimeStampRemoteViewsUtil.java new file mode 100644 index 0000000..0aee3d6 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/utils/TimeStampRemoteViewsUtil.java @@ -0,0 +1,96 @@ +package cc.winboll.studio.timestamp.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 21:10 + * @Describe TimeStampRemoteViewsUtil + */ +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.widget.RemoteViews; +import android.widget.TextView; +import androidx.core.app.NotificationCompat; +import cc.winboll.studio.timestamp.R; +import cc.winboll.studio.timestamp.receivers.ButtonClickReceiver; + +public class TimeStampRemoteViewsUtil { + + public static final String TAG = "TimeStampRemoteViewsUtil"; + + public static final String CHANNEL_ID = "TimeStampChannel"; + + static volatile TimeStampRemoteViewsUtil _TimeStampRemoteViewsUtil; + Context mContext; + RemoteViews mRemoteViews; + TextView mtvMessage; + + TimeStampRemoteViewsUtil(Context context) { + mContext = context; + createNotificationChannel(); + } + + public static synchronized TimeStampRemoteViewsUtil getInstance(Context context) { + if (_TimeStampRemoteViewsUtil == null) { + _TimeStampRemoteViewsUtil = new TimeStampRemoteViewsUtil(context); + } + return _TimeStampRemoteViewsUtil; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "自定义视图通知通道"; + String description = "用于展示自定义视图的通知通道"; + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + channel.setDescription(description); + NotificationManager notificationManager = mContext.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + public void showNotification(String msg) { + if (mRemoteViews == null) { + // 创建 RemoteViews 对象,加载布局 + mRemoteViews = new RemoteViews(mContext.getPackageName(), R.layout.custom_notification_layout); + + } + // 自定义 TextView 的文本 + mRemoteViews.setTextViewText(R.id.tv_timestamp, msg); + // 自定义 TextView 的文本颜色 + mRemoteViews.setTextColor(R.id.tv_timestamp, mContext.getResources().getColor(R.color.colorAccent, null)); + // 这里虽然不能直接设置字体大小,但可以通过反射等方式尝试(不推荐,且有兼容性问题) + + // 创建点击通知后的意图 + //Intent intent = new Intent(mContext, MainActivity.class); + //PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + // 设置通知的点击事件 + //mRemoteViews.setOnClickPendingIntent(R.id.btn_copytimestamp, pendingIntent); + + // 创建点击按钮后要发送的广播 Intent + Intent broadcastIntent = new Intent(ButtonClickReceiver.BUTTON_COPYTIMESTAMP_ACTION); + android.app.PendingIntent pendingIntent = android.app.PendingIntent.getBroadcast( + mContext, + 0, + broadcastIntent, + android.app.PendingIntent.FLAG_UPDATE_CURRENT + ); + + // 为按钮设置点击事件 + mRemoteViews.setOnClickPendingIntent(R.id.btn_copytimestamp, pendingIntent); + + // 构建通知 + NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContent(mRemoteViews) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setOngoing(true) + .setAutoCancel(true); + + // 显示通知 + NotificationManager notificationManager = mContext.getSystemService(NotificationManager.class); + notificationManager.notify(1, builder.build()); + } +} diff --git a/timestamp/src/main/java/cc/winboll/studio/timestamp/views/TimeStampView.java b/timestamp/src/main/java/cc/winboll/studio/timestamp/views/TimeStampView.java new file mode 100644 index 0000000..7f22f55 --- /dev/null +++ b/timestamp/src/main/java/cc/winboll/studio/timestamp/views/TimeStampView.java @@ -0,0 +1,99 @@ +package cc.winboll.studio.timestamp.views; + +/** + * @Author ZhanGSKen + * @Date 2025/05/05 12:53 + * @Describe TimeStampView + */ +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; + +public class TimeStampView extends View { + + public static final String TAG = "TimeStampView"; + + public static final int MSG_UPDATE_TIMESTAMP = 0; + + //private Paint circlePaint; + //private Paint textPaint; + +// Context mContext; +// Timer mTimer; +// TextView mtvTimeStamp; +// MyHandler mMyHandler; + public TimeStampView(Context context) { + super(context); + //initView(context); + init(); + } + + public TimeStampView(Context context, AttributeSet attrs) { + super(context, attrs); + //initView(context); + init(); + } + + public TimeStampView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + //initView(context); + init(); + } + + public TimeStampView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + //initView(context); + init(); + } + + private void init() { +// circlePaint = new Paint(); +// circlePaint.setColor(Color.BLUE); +// circlePaint.setStyle(Paint.Style.FILL); +// +// textPaint = new Paint(); +// textPaint.setColor(Color.WHITE); +// textPaint.setTextSize(40); +// textPaint.setTextAlign(Paint.Align.CENTER); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + //int width = getWidth(); + //int height = getHeight(); +// int width = 50; +// int height = 50; +// float radius = Math.min(width, height) / 2; +// canvas.drawCircle(width / 2, height / 2, radius, circlePaint); +// String text = "自定义"; +// RectF rect = new RectF(0, 0, width, height); +// Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt(); +// int baseline =(int)((rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2); +// canvas.drawText(text, width / 2, baseline, textPaint); + } + +// void initView(Context context) { +// View viewMain = inflate(context, R.layout.view_timestamp, null); +// this.mContext = context; +// mtvTimeStamp = viewMain.findViewById(R.id.tv_timestamp); +// addView(viewMain); +// +// mMyHandler = new MyHandler(); +// + +// } + +// public void updateTimeStamp() { +// try { +// +// } catch (Exception e) { +// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); +// ToastUtils.show(e); +// } +// } + + + +} diff --git a/timestamp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/timestamp/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/timestamp/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/timestamp/src/main/res/drawable/ic_launcher_background.xml b/timestamp/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/timestamp/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/timestamp/src/main/res/layout/activity_main.xml b/timestamp/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f7bf52a --- /dev/null +++ b/timestamp/src/main/res/layout/activity_main.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +