diff --git a/autonfc/.gitignore b/autonfc/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/autonfc/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/autonfc/README.md b/autonfc/README.md new file mode 100644 index 0000000..7421e1d --- /dev/null +++ b/autonfc/README.md @@ -0,0 +1,34 @@ +# AutoNFC + +#### 介绍 +NFC 卡应用,主要管理 NFC 卡接触手机的动作响应,NFC 接触状态用于作为其他应用激活活动动作的启动令牌。 + +#### 软件架构 +适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。 +也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。 + + +#### Gradle 编译说明 +调试版编译命令 :gradle assembleBetaDebug +阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh autonfc + +#### 使用说明 + +#### 参与贡献 + +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/autonfc/app_update_description.txt b/autonfc/app_update_description.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/autonfc/app_update_description.txt @@ -0,0 +1 @@ + diff --git a/autonfc/build.gradle b/autonfc/build.gradle new file mode 100644 index 0000000..92c012a --- /dev/null +++ b/autonfc/build.gradle @@ -0,0 +1,119 @@ +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.autonfc" + minSdkVersion 23 + // 适配MIUI12 + targetSdkVersion 30 + versionCode 1 + // versionName 更新后需要手动设置 + // .winboll/winbollBuildProps.properties 文件的 stageCount=0 + // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" + versionName "15.11" + if(true) { + versionName = genVersionName("${versionName}") + } + } + + // 米盟 SDK + packagingOptions { + doNotStrip "*/*/libmimo_1011.so" + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下 + } + } +} + +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' + // OkHttp网络请求 + implementation 'com.squareup.okhttp3:okhttp:3.14.9' + // 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.google.code.gson:gson:2.8.5' + 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' + + // WinBoLL库 nexus.winboll.cc 地址 + api 'cc.winboll.studio:libaes:15.15.2' + api 'cc.winboll.studio:libappbase:15.15.11' + + // WinBoLL备用库 jitpack.io 地址 + //api 'com.github.ZhanGSKen:AES:aes-v15.15.7' + //api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4' + + api fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/autonfc/build.properties b/autonfc/build.properties new file mode 100644 index 0000000..4599e56 --- /dev/null +++ b/autonfc/build.properties @@ -0,0 +1,8 @@ +#Created by .winboll/winboll_app_build.gradle +#Mon Mar 16 18:30:19 GMT 2026 +stageCount=0 +libraryProject= +baseVersion=15.11 +publishVersion=15.0.0 +buildCount=54 +baseBetaVersion=15.0.1 diff --git a/autonfc/proguard-rules.pro b/autonfc/proguard-rules.pro new file mode 100644 index 0000000..64b4a05 --- /dev/null +++ b/autonfc/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/autonfc/src/beta/AndroidManifest.xml b/autonfc/src/beta/AndroidManifest.xml new file mode 100644 index 0000000..ee78d9f --- /dev/null +++ b/autonfc/src/beta/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/autonfc/src/beta/res/values/strings.xml b/autonfc/src/beta/res/values/strings.xml new file mode 100644 index 0000000..3c165aa --- /dev/null +++ b/autonfc/src/beta/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + AutoNFC✌ + + diff --git a/autonfc/src/main/AndroidManifest.xml b/autonfc/src/main/AndroidManifest.xml new file mode 100644 index 0000000..97234a5 --- /dev/null +++ b/autonfc/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/App.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/App.java new file mode 100644 index 0000000..d64ac77 --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/App.java @@ -0,0 +1,344 @@ +package cc.winboll.studio.autonfc; + +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.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.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/autonfc/src/main/java/cc/winboll/studio/autonfc/MainActivity.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/MainActivity.java new file mode 100644 index 0000000..03743b7 --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/MainActivity.java @@ -0,0 +1,180 @@ +package cc.winboll.studio.autonfc; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.nfc.NfcAdapter; +import android.os.Bundle; +import android.os.IBinder; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import cc.winboll.studio.autonfc.nfc.ActionDialog; +import cc.winboll.studio.autonfc.nfc.AutoNFCService; +import cc.winboll.studio.autonfc.nfc.NFCInterfaceActivity; +import cc.winboll.studio.libappbase.LogActivity; +import cc.winboll.studio.libappbase.LogUtils; + +public class MainActivity extends AppCompatActivity { + + public static final String TAG = "MainActivity"; + + private NfcAdapter mNfcAdapter; + private PendingIntent mPendingIntent; + private AutoNFCService mService; + private boolean mBound = false; + + // 服务连接 + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + AutoNFCService.LocalBinder binder = (AutoNFCService.LocalBinder) service; + mService = binder.getService(); + mBound = true; + LogUtils.d(TAG, "onServiceConnected: 服务已绑定"); + + // 关键:把 Activity 传给 Service,用于回调 + mService.attachActivity(MainActivity.this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mBound = false; + mService = null; + LogUtils.d(TAG, "onServiceDisconnected: 服务已断开"); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + // 初始化 NFC + mNfcAdapter = NfcAdapter.getDefaultAdapter(this); + Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + mPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, 0); + + LogUtils.d(TAG, "onCreate() -> NFC 监听已绑定到 MainActivity"); + } + + @Override + protected void onStart() { + super.onStart(); + // 绑定服务 + Intent intent = new Intent(this, AutoNFCService.class); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + LogUtils.d(TAG, "onStart: 绑定服务"); + } + + @Override + protected void onStop() { + super.onStop(); + // 解绑服务 + if (mBound) { + unbindService(mConnection); + mBound = false; + LogUtils.d(TAG, "onStop: 解绑服务"); + } + } + + @Override + protected void onResume() { + super.onResume(); + LogUtils.d(TAG, "onResume() -> 开启 NFC 前台分发"); + if (mNfcAdapter != null) { + mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null); + } + } + + @Override + protected void onPause() { + super.onPause(); + LogUtils.d(TAG, "onPause() -> 关闭 NFC 前台分发"); + if (mNfcAdapter != null) { + mNfcAdapter.disableForegroundDispatch(this); + } + } + + // NFC 卡片靠近唯一入口 + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + LogUtils.d(TAG, "onNewIntent() -> 检测到 NFC 卡片"); + + // 把 NFC 事件交给 Service 处理 + if (mBound && mService != null) { + mService.handleNfcIntent(intent); + } else { + LogUtils.e(TAG, "服务未绑定,无法处理 NFC"); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.menu_log) { + LogActivity.startLogActivity(this); + return true; + } + + return super.onOptionsItemSelected(item); + } + + public void onNFCInterfaceActivity(View view) { + startActivity(new Intent(this, NFCInterfaceActivity.class)); + } + + // ========================= 【新增】关键方法:由 Service 回调来弹出对话框 ========================= + /** + * Service 解析完 NFC 数据后,回调此方法在 Activity 中弹出对话框 + */ + public void showNfcActionDialog(final String nfcData) { + LogUtils.d(TAG, "showNfcActionDialog() -> Activity 存活,安全弹出对话框"); + + // Activity 正在运行,直接弹框,绝对不会报 BadTokenException + final ActionDialog dialog = new ActionDialog(this); + dialog.setNfcData(nfcData); + dialog.setButtonClickListener(new ActionDialog.OnButtonClickListener() { + @Override + public void onBuildClick() { + LogUtils.d(TAG, "点击 Build"); + if (mService != null) { + mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD, nfcData); + } + dialog.dismiss(); + } + + @Override + public void onViewClick() { + LogUtils.d(TAG, "点击 View"); + if (mService != null) { + mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD_VIEW, nfcData); + } + dialog.dismiss(); + } + + @Override + public void onCancelClick() { + dialog.dismiss(); + } + }); + + dialog.show(); + } +} + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/models/NfcTermuxCmd.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/models/NfcTermuxCmd.java new file mode 100644 index 0000000..faab4cb --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/models/NfcTermuxCmd.java @@ -0,0 +1,66 @@ +package cc.winboll.studio.autonfc.models; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/03/16 09:38 + */ +public class NfcTermuxCmd { + + private String script; // 要执行的预制脚本名(如 auth.sh) + private String[] args; // 脚本参数 + private String workDir; // 工作目录 + private boolean background; // 是否后台执行 + private String resultDir; // 结果输出目录(可为 null) + + public NfcTermuxCmd() { + } + + public NfcTermuxCmd(String script, String[] args, String workDir, boolean background, String resultDir) { + this.script = script; + this.args = args; + this.workDir = workDir; + this.background = background; + this.resultDir = resultDir; + } + + public String getScript() { + return script; + } + + public void setScript(String script) { + this.script = script; + } + + public String[] getArgs() { + return args; + } + + public void setArgs(String[] args) { + this.args = args; + } + + public String getWorkDir() { + return workDir; + } + + public void setWorkDir(String workDir) { + this.workDir = workDir; + } + + public boolean isBackground() { + return background; + } + + public void setBackground(boolean background) { + this.background = background; + } + + public String getResultDir() { + return resultDir; + } + + public void setResultDir(String resultDir) { + this.resultDir = resultDir; + } +} + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/ActionDialog.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/ActionDialog.java new file mode 100644 index 0000000..c3f65d7 --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/ActionDialog.java @@ -0,0 +1,123 @@ +package cc.winboll.studio.autonfc.nfc; + +import android.app.Dialog; +import android.content.Context; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; + +import cc.winboll.studio.autonfc.R; +import cc.winboll.studio.libappbase.LogUtils; + +/** + * 自定义对话框类,用于与用户交互,展示 NFC 相关操作选项 + * 兼容 Java 7 语法 + * + * @author 豆包&ZhanGSKen + * @create 2025-08-15 + * @lastModify 2026-03-17 + */ +public class ActionDialog extends Dialog { + + private static final String TAG = "ActionDialog"; + private String mNfcData; + private OnButtonClickListener mClickListener; + + /** + * 构造函数 + */ + public ActionDialog(Context context) { + super(context); + initDialog(); + } + + /** + * 设置 NFC 数据 + */ + public void setNfcData(String nfcData) { + this.mNfcData = nfcData; + LogUtils.d(TAG, "setNfcData() -> " + nfcData); + } + + /** + * 设置点击监听 + */ + public void setButtonClickListener(OnButtonClickListener listener) { + this.mClickListener = listener; + } + + /** + * 初始化布局 + */ + private void initDialog() { + setTitle("请选择操作"); + + LinearLayout layout = new LinearLayout(getContext()); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(20, 20, 20, 20); + + addButtons(layout); + + setContentView(layout); + } + + /** + * 添加按钮 + */ + private void addButtons(LinearLayout layout) { + // Build 按钮 + Button btnBuild = createButton("Build", new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "点击 Build"); + if (mClickListener != null) { + mClickListener.onBuildClick(); + } + } + }); + layout.addView(btnBuild); + + // View 按钮 + Button btnView = createButton("View", new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "点击 View"); + if (mClickListener != null) { + mClickListener.onViewClick(); + } + } + }); + layout.addView(btnView); + + // 取消按钮 + Button btnCancel = createButton("Cancel", new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "点击 Cancel"); + dismiss(); + } + }); + layout.addView(btnCancel); + } + + /** + * 创建按钮 + */ + private Button createButton(String text, View.OnClickListener listener) { + Button button = new Button(getContext()); + button.setText(text); + button.setPadding(10, 10, 10, 10); + button.setOnClickListener(listener); + return button; + } + + /** + * 回调接口 + */ + public interface OnButtonClickListener { + void onBuildClick(); + void onViewClick(); + void onCancelClick(); + } +} + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/AutoNFCService.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/AutoNFCService.java new file mode 100644 index 0000000..0addf65 --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/AutoNFCService.java @@ -0,0 +1,202 @@ +package cc.winboll.studio.autonfc.nfc; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.Ndef; +import android.os.Binder; +import android.os.IBinder; + +import cc.winboll.studio.autonfc.MainActivity; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; + +import java.nio.charset.Charset; +import java.util.Arrays; + +public class AutoNFCService extends Service { + + public static final String TAG = "AutoNFCService"; + + // ================= 已修改:更新为 Beta 包名 ================= + public static final String ACTION_BUILD = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD"; + public static final String ACTION_BUILD_VIEW = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD_VIEW"; + + private final IBinder mBinder = new LocalBinder(); + private String mNfcData; + private MainActivity mActivity; // 持有 Activity 引用,用于回调 + + // ========================= 生命周期 ========================= + @Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "onCreate() -> 服务创建"); + // 移除:startForeground(NOTIFICATION_ID, buildNotification()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "onDestroy() -> 服务已停止"); + mActivity = null; // 释放引用 + } + + // ========================= 服务绑定 ========================= + @Override + public IBinder onBind(Intent intent) { + LogUtils.d(TAG, "onBind() -> 服务被绑定"); + return mBinder; + } + + @Override + public boolean onUnbind(Intent intent) { + LogUtils.d(TAG, "onUnbind() -> 服务解绑"); + // 移除:stopForeground(true); + stopSelf(); + return super.onUnbind(intent); + } + + // ========================= 对外暴露方法 ========================= + /** + * 绑定 Activity,用于回调显示对话框 + */ + public void attachActivity(MainActivity activity) { + this.mActivity = activity; + } + + /** + * 处理 NFC 意图 + */ + public void handleNfcIntent(Intent intent) { + LogUtils.d(TAG, "handleNfcIntent() -> 开始处理"); + + if (intent == null) { + LogUtils.e(TAG, "handleNfcIntent() -> 参数 intent 为空"); + return; + } + + String action = intent.getAction(); + LogUtils.d(TAG, "handleNfcIntent() -> Action = " + action); + + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action) + || NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) + || NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) { + + LogUtils.d(TAG, "handleNfcIntent() -> 匹配 NFC 动作"); + + Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + if (tag == null) { + LogUtils.e(TAG, "handleNfcIntent() -> Tag 为空"); + return; + } + + LogUtils.d(TAG, "handleNfcIntent() -> Tag ID = " + bytesToHexString(tag.getId())); + LogUtils.d(TAG, "handleNfcIntent() -> Tech List = " + Arrays.toString(tag.getTechList())); + + parseNdefData(tag); + } + } + + // ========================= 内部业务 ========================= + private void parseNdefData(Tag tag) { + LogUtils.d(TAG, "parseNdefData() -> 开始解析"); + + if (tag == null) return; + + Ndef ndef = Ndef.get(tag); + if (ndef == null) { + LogUtils.e(TAG, "parseNdefData() -> 不支持 NDEF 格式"); + return; + } + + try { + ndef.connect(); + NdefMessage msg = ndef.getNdefMessage(); + + if (msg == null || msg.getRecords() == null || msg.getRecords().length == 0) { + LogUtils.w(TAG, "parseNdefData() -> 卡片无数据"); + return; + } + + NdefRecord record = msg.getRecords()[0]; + byte[] payload = record.getPayload(); + + int langLen = payload[0] & 0x3F; + int start = 1 + langLen; + + if (start < payload.length) { + mNfcData = new String(payload, start, payload.length - start, Charset.forName("UTF-8")); + LogUtils.d(TAG, "parseNdefData() -> 读卡成功: " + mNfcData); + + // 关键:回调给 Activity 弹框,此时 Activity 一定是存活状态 + if (mActivity != null) { + mActivity.showNfcActionDialog(mNfcData); + } + } + + } catch (Exception e) { + LogUtils.e(TAG, "parseNdefData() -> 读取失败", e); + } finally { + try { + ndef.close(); + } catch (Exception e) { + // 忽略关闭异常 + } + } + } + + /** + * 执行 Termux 命令 + */ + public void executeTermuxCommand(String action, String nfcData) { + LogUtils.d(TAG, "executeTermuxCommand() -> 开始执行"); + + if (nfcData == null || nfcData.isEmpty()) { + ToastUtils.show("数据错误"); + return; + } + + try { + LogUtils.d(TAG, "executeTermuxCommand() -> 发送指令: " + nfcData); + + Intent bridgeIntent = new Intent(action); + + // ================= 已修改:使用 Beta 包名 ================= + bridgeIntent.setClassName( + "cc.winboll.studio.winboll.beta", + "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity" + ); + + bridgeIntent.putExtra(Intent.EXTRA_TEXT, nfcData); + bridgeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + startActivity(bridgeIntent); + ToastUtils.show("指令已发送"); + } catch (Exception e) { + LogUtils.e(TAG, "executeTermuxCommand() -> 发送失败", e); + ToastUtils.show("发送失败"); + } + } + + // ========================= 工具方法 ========================= + private String bytesToHexString(byte[] bytes) { + if (bytes == null || bytes.length == 0) return ""; + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } + + // ========================= Binder ========================= + public class LocalBinder extends Binder { + public AutoNFCService getService() { + return AutoNFCService.this; + } + } +} + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NFCInterfaceActivity.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NFCInterfaceActivity.java new file mode 100644 index 0000000..28b6f18 --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NFCInterfaceActivity.java @@ -0,0 +1,230 @@ +package cc.winboll.studio.autonfc.nfc; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.os.Bundle; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import cc.winboll.studio.autonfc.R; +import cc.winboll.studio.autonfc.models.NfcTermuxCmd; +import cc.winboll.studio.libappbase.LogUtils; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class NFCInterfaceActivity extends Activity { + + public static final String TAG = "NFCInterfaceActivity"; + + private EditText et_script; + private EditText et_args; + private EditText et_workDir; + private EditText et_background; + private EditText et_resultDir; + + private TextView tvResult; + private TextView tvStatus; + + private NfcAdapter mNfcAdapter; + private PendingIntent mNfcPendingIntent; + private Tag mCurrentTag; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_nfc_interface); + initView(); + initNfc(); + } + + private void initView() { + et_script = findViewById(R.id.et_script); + et_args = findViewById(R.id.et_args); + et_workDir = findViewById(R.id.et_workDir); + et_background = findViewById(R.id.et_background); + et_resultDir = findViewById(R.id.et_resultDir); + + tvResult = findViewById(R.id.tv_result); + tvStatus = findViewById(R.id.tv_status); + } + + private void initNfc() { + mNfcAdapter = NfcAdapter.getDefaultAdapter(this); + + if (mNfcAdapter == null) { + tvStatus.setText("设备不支持NFC"); + return; + } + if (!mNfcAdapter.isEnabled()) { + tvStatus.setText("请开启NFC"); + return; + } + + Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + mNfcPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + tvStatus.setText("NFC已启动,等待卡片靠近"); + } + + @Override + protected void onResume() { + super.onResume(); + if (mNfcAdapter != null && mNfcAdapter.isEnabled()) { + mNfcAdapter.enableForegroundDispatch(this, mNfcPendingIntent, null, null); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (mNfcAdapter != null) { + mNfcAdapter.disableForegroundDispatch(this); + } + mCurrentTag = null; + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + mCurrentTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + if (mCurrentTag == null) return; + + tvStatus.setText("卡片已连接,解析中..."); + readNfc(); + } + + // ------------------------------------------------------------------------- + // 读取 NFC(完全委托给工具类) + // ------------------------------------------------------------------------- + private void readNfc() { + try { + NfcTermuxCmd cmd = NfcUtils.readTag(mCurrentTag); + if (cmd == null) { + tvStatus.setText("读取成功:标签为空"); + tvResult.setText(""); + // 清空窗体 + clearUiFields(); + return; + } + + // 核心改动:读取成功后,同时更新详情显示 和 窗体输入框 + updateUiWithCmd(cmd); + + } catch (Exception e) { + LogUtils.e(TAG, "readNfc 失败", e); + tvStatus.setText("读取失败:" + e.getMessage()); + // 出错时清空窗体 + clearUiFields(); + } + } + + // ------------------------------------------------------------------------- + // 新增:根据读取到的 Cmd 填充 UI(详情 + 窗体) + // ------------------------------------------------------------------------- + private void updateUiWithCmd(NfcTermuxCmd cmd) { + if (cmd == null) return; + + String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date()); + String show = "【读取时间】 " + time + "\n\n" + + "【解析结果】\n" + + "script: " + cmd.getScript() + "\n" + + "args: " + (cmd.getArgs() != null ? String.join(", ", cmd.getArgs()) : "[]") + "\n" + + "workDir: " + cmd.getWorkDir() + "\n" + + "background: " + cmd.isBackground() + "\n" + + "resultDir: " + cmd.getResultDir(); + + tvResult.setText(show); + tvStatus.setText("读取成功!"); + + // 👇 关键逻辑:自动填入窗体(每次读取后都会覆盖输入框) + et_script.setText(cmd.getScript() != null ? cmd.getScript() : ""); + et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : ""); + et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : ""); + et_background.setText(String.valueOf(cmd.isBackground())); + et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : ""); + } + + // ------------------------------------------------------------------------- + // 辅助:清空所有输入框 + // ------------------------------------------------------------------------- + private void clearUiFields() { + et_script.setText(""); + et_args.setText(""); + et_workDir.setText(""); + et_background.setText(""); + et_resultDir.setText(""); + } + + // ------------------------------------------------------------------------- + // 写入按钮(委托给工具类) + // ------------------------------------------------------------------------- + public void onWriteClick(View view) { + if (mCurrentTag == null) { + showToast("请先靠近卡片"); + return; + } + + try { + NfcTermuxCmd cmd = buildCmdFromUI(); + NfcUtils.writeTag(mCurrentTag, cmd); + + tvStatus.setText("写入成功!"); + showToast("写入成功"); + readNfc(); // 写入后重读,此时会自动填入窗体 + + } catch (Exception e) { + LogUtils.e(TAG, "写入失败", e); + tvStatus.setText("写入失败:" + e.getMessage()); + showToast("写入失败"); + } + } + + // ------------------------------------------------------------------------- + // 填充调试数据 + // ------------------------------------------------------------------------- + public void onFillTestDataClick(View view) { + String testJson = "{\"script\":\"BuildWinBoLLProject.sh\",\"args\":[\"DebugTemp\"],\"workDir\":null,\"background\":true,\"resultDir\":null}"; + try { + NfcTermuxCmd cmd = NfcUtils.jsonToCmd(testJson); + et_script.setText(cmd.getScript()); + et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : ""); + et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : ""); + et_background.setText(String.valueOf(cmd.isBackground())); + et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : ""); + + showToast("调试数据已填入"); + } catch (Exception e) { + showToast("解析失败"); + } + } + + // ------------------------------------------------------------------------- + // 从 UI 构建 NfcTermuxCmd + // ------------------------------------------------------------------------- + private NfcTermuxCmd buildCmdFromUI() { + String script = et_script.getText().toString().trim(); + String argsStr = et_args.getText().toString().trim(); + String workDir = et_workDir.getText().toString().trim(); + String bgStr = et_background.getText().toString().trim(); + String resultDir = et_resultDir.getText().toString().trim(); + + NfcTermuxCmd cmd = new NfcTermuxCmd(); + cmd.setScript(script); + cmd.setArgs(argsStr.isEmpty() ? new String[0] : argsStr.split(",")); + cmd.setWorkDir(workDir.isEmpty() ? null : workDir); + cmd.setBackground("true".equalsIgnoreCase(bgStr)); + cmd.setResultDir(resultDir.isEmpty() ? null : resultDir); + + return cmd; + } + + private void showToast(String msg) { + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); + } +} + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NfcStateMonitor.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NfcStateMonitor.java new file mode 100644 index 0000000..a58fead --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NfcStateMonitor.java @@ -0,0 +1,78 @@ +package cc.winboll.studio.autonfc.nfc; + +import java.util.HashMap; +import java.util.Map; + +public class NfcStateMonitor { + private static Map sListenerMap = new HashMap<>(); + private static boolean sIsRunning = false; + + public static void startMonitor() { + if (sIsRunning) return; + sListenerMap = new HashMap<>(); + sIsRunning = true; + } + + public static void stopMonitor() { + if (!sIsRunning) return; + sIsRunning = false; + if (sListenerMap != null) { + sListenerMap.clear(); + sListenerMap = null; + } + } + + // 你原来的方法名:registerListener + public static void registerListener(String key, OnNfcStateListener listener) { + if (!sIsRunning || listener == null) return; + sListenerMap.put(key, listener); + } + + public static void unregisterListener(String key) { + if (!sIsRunning || key == null) return; + sListenerMap.remove(key); + } + + public static void notifyNfcConnected() { + if (!sIsRunning) return; + for (OnNfcStateListener l : sListenerMap.values()) { + l.onNfcConnected(); + } + } + + public static void notifyNfcDisconnected() { + if (!sIsRunning) return; + for (OnNfcStateListener l : sListenerMap.values()) { + l.onNfcDisconnected(); + } + } + + public static void notifyReadSuccess(String data) { + if (!sIsRunning) return; + for (OnNfcStateListener l : sListenerMap.values()) { + l.onNfcReadSuccess(data); + } + } + + public static void notifyReadFail(String error) { + if (!sIsRunning) return; + for (OnNfcStateListener l : sListenerMap.values()) { + l.onNfcReadFail(error); + } + } + + public static void notifyWriteSuccess() { + if (!sIsRunning) return; + for (OnNfcStateListener l : sListenerMap.values()) { + l.onNfcWriteSuccess(); + } + } + + public static void notifyWriteFail(String error) { + if (!sIsRunning) return; + for (OnNfcStateListener l : sListenerMap.values()) { + l.onNfcWriteFail(error); + } + } +} + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NfcUtils.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NfcUtils.java new file mode 100644 index 0000000..65ac597 --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/NfcUtils.java @@ -0,0 +1,136 @@ +package cc.winboll.studio.autonfc.nfc; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/03/16 14:26 + */ +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.Tag; +import android.nfc.tech.Ndef; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import cc.winboll.studio.autonfc.models.NfcTermuxCmd; +import cc.winboll.studio.libappbase.LogUtils; +import java.nio.charset.Charset; +import java.util.Locale; + +public class NfcUtils { + + public static final String TAG = "NfcUtils"; + private static Gson sGson = new Gson(); + + // ------------------------------------------------------------------------- + // 读取 NFC 标签并解析为 NfcTermuxCmd + // ------------------------------------------------------------------------- + public static NfcTermuxCmd readTag(Tag tag) throws Exception { + if (tag == null) { + LogUtils.e(TAG, "readTag: tag is null"); + return null; + } + + Ndef ndef = Ndef.get(tag); + if (ndef == null) { + LogUtils.e(TAG, "readTag: 不支持 NDEF"); + return null; + } + + try { + ndef.connect(); + NdefMessage msg = ndef.getNdefMessage(); + if (msg == null) return null; + + NdefRecord[] records = msg.getRecords(); + if (records == null || records.length == 0) return null; + + byte[] payload = records[0].getPayload(); + int status = payload[0] & 0xFF; + int langLen = status & 0x3F; + int start = 1 + langLen; + + if (start >= payload.length) return null; + + String json = new String(payload, start, payload.length - start, Charset.forName("UTF-8")); + LogUtils.d(TAG, "readTag: 提取 JSON -> " + json); + + return sGson.fromJson(json, NfcTermuxCmd.class); + } finally { + if (ndef != null && ndef.isConnected()) { + ndef.close(); + } + } + } + + // ------------------------------------------------------------------------- + // 写入 NfcTermuxCmd 到 NFC 标签 + // ------------------------------------------------------------------------- + public static void writeTag(Tag tag, NfcTermuxCmd cmd) throws Exception { + if (tag == null) throw new Exception("tag is null"); + + String json = sGson.toJson(cmd); + writeJson(tag, json); + } + + // ------------------------------------------------------------------------- + // 写入原始 JSON 字符串到 NFC + // ------------------------------------------------------------------------- + public static void writeJson(Tag tag, String json) throws Exception { + if (tag == null) throw new Exception("tag is null"); + + Ndef ndef = Ndef.get(tag); + if (ndef == null) throw new Exception("标签不支持 NDEF"); + + try { + ndef.connect(); + int maxSize = ndef.getMaxSize(); + int realSize = json.getBytes(Charset.forName("UTF-8")).length; + + if (realSize > maxSize) { + throw new Exception("数据过大 (" + realSize + ") > 容量 (" + maxSize + ")"); + } + + NdefRecord record = createTextRecord(json, true); + NdefMessage msg = new NdefMessage(new NdefRecord[]{record}); + + ndef.writeNdefMessage(msg); + LogUtils.d(TAG, "writeJson: 写入成功"); + } finally { + if (ndef != null && ndef.isConnected()) { + ndef.close(); + } + } + } + + // ------------------------------------------------------------------------- + // 创建 NFC 文本记录 + // ------------------------------------------------------------------------- + public static NdefRecord createTextRecord(String text, boolean isUtf8) { + byte[] langBytes = "en".getBytes(Charset.forName("US-ASCII")); + byte[] textBytes = text.getBytes(Charset.forName(isUtf8 ? "UTF-8" : "UTF-16")); + + int status = isUtf8 ? 0 : 0x80; + status |= langBytes.length & 0x3F; + + byte[] data = new byte[1 + langBytes.length + textBytes.length]; + data[0] = (byte) status; + System.arraycopy(langBytes, 0, data, 1, langBytes.length); + System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length); + + return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data); + } + + // ------------------------------------------------------------------------- + // 辅助:JSON -> NfcTermuxCmd + // ------------------------------------------------------------------------- + public static NfcTermuxCmd jsonToCmd(String json) throws JsonSyntaxException { + return sGson.fromJson(json, NfcTermuxCmd.class); + } + + // ------------------------------------------------------------------------- + // 辅助:NfcTermuxCmd -> JSON + // ------------------------------------------------------------------------- + public static String cmdToJson(NfcTermuxCmd cmd) { + return sGson.toJson(cmd); + } +} + diff --git a/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/OnNfcStateListener.java b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/OnNfcStateListener.java new file mode 100644 index 0000000..4b149d5 --- /dev/null +++ b/autonfc/src/main/java/cc/winboll/studio/autonfc/nfc/OnNfcStateListener.java @@ -0,0 +1,11 @@ +package cc.winboll.studio.autonfc.nfc; + +public interface OnNfcStateListener { + void onNfcConnected(); // 无参数! + void onNfcDisconnected(); + void onNfcReadSuccess(String data); + void onNfcReadFail(String error); + void onNfcWriteSuccess(); + void onNfcWriteFail(String error); +} + diff --git a/autonfc/src/main/res/drawable-v24/ic_launcher_foreground.xml b/autonfc/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/autonfc/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/autonfc/src/main/res/drawable/ic_launcher_background.xml b/autonfc/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/autonfc/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autonfc/src/main/res/layout/activity_main.xml b/autonfc/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..a6032d0 --- /dev/null +++ b/autonfc/src/main/res/layout/activity_main.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + +