feat: 添加蓝牙读卡功能- 新增蓝牙帮助类和读卡相关 Activity

- 实现蓝牙设备扫描、连接和读卡功能
- 添加卡信息处理和展示逻辑
- 集成第三方库实现 EMV 进程和数据解析
This commit is contained in:
2025-07-04 17:18:37 +08:00
commit d8f8327e28
58 changed files with 1984 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+44
View File
@@ -0,0 +1,44 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace 'com.serverapk.i9_bl_test'
compileSdk 36
defaultConfig {
applicationId "com.serverapk.i9_bl_test"
minSdk 24
targetSdk 36
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
implementation 'com.google.code.gson:gson:2.8.9'
}
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -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
@@ -0,0 +1,26 @@
package com.serverapk.i9_bl_test;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.serverapk.i9_bl_test", appContext.getPackageName());
}
}
+107
View File
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_PRIVILEGED"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_AIRPLANE_MODE" />
<uses-permission
android:name="android.permission.BIND_JOB_SERVICE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission
android:name="android.permission.INTERACT_ACROSS_USERS_FULL"
tools:ignore="ProtectedPermissions" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature android:name="android.hardware.usb.host" />
<uses-permission
android:name="android.permission.DELETE_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.INSTALL_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="app.permission.install" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.USB_PERMISSION" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
<uses-permission android:name="android.permission.RESTART_PACKAGES" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.SET_TIME_ZONE"
tools:ignore="ProtectedPermissions" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.I9bltest">
<activity
android:name=".ReadCardActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name="com.company.i9sslclient.service.BluetoothLeService"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedService" />
</application>
</manifest>
@@ -0,0 +1,54 @@
package com.serverapk.i9_bl_test;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@SuppressLint("MissingPermission")
public class BluetoothHelper {
private static final String TAG = "BluetoothHelper";
public static Set<BluetoothDevice> getConnectedBluetoothDevices(Context context, ServiceListener listener) {
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Log.w(TAG, "蓝牙未启用");
return new HashSet<>();
}
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
Log.d(TAG, "已配对设备: " + device.getName() + " - " + device.getAddress());
}
bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
List<BluetoothDevice> connectedDevices = proxy.getConnectedDevices();
for (BluetoothDevice device : connectedDevices) {
Log.d(TAG, "已连接: " + device.getName() + " - " + device.getAddress());
}
listener.onServiceConnected(connectedDevices);
}
@Override
public void onServiceDisconnected(int profile) {
Log.d(TAG, "HEADSET profile 已断开");
}
}, BluetoothProfile.HEADSET);
return pairedDevices;
}
public interface ServiceListener {
void onServiceConnected(List<BluetoothDevice> devices);
}
}
@@ -0,0 +1,143 @@
package com.serverapk.i9_bl_test;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.company.i9sslclient.bluetooth.DeviceInfo;
import java.util.ArrayList;
import java.util.List;
@SuppressLint("MissingPermission")
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final long SCAN_PERIOD = 10000;
Handler handler = new Handler(Looper.getMainLooper());
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;
private List<String> devices;
private List<DeviceInfo> deviceInfoList;
private String mac;
BluetoothAdapter.LeScanCallback leScanCallback = (device, rssi, scanRecord) -> setMac(device);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
findViewById(R.id.test).setOnClickListener(view -> {
if (mac == null || mac.isEmpty() || mac.isBlank()) {
Log.e(TAG, "未找到读卡器");
return;
}
Intent intent = new Intent(MainActivity.this, ReadCardActivity.class);
intent.putExtra("mac", mac);
startActivity(intent);
});
init();
}
public void setMac(BluetoothDevice device) {
int size = devices.size();
if (size > 0) {
boolean tmp = false;
for (int i = 0; i < size; i++) {
if (device.getAddress().equalsIgnoreCase(devices.get(i))) {
tmp = true;
break;
}
}
if (!tmp) {
if (device.getName() != null) {
Log.d(TAG, "device.getName()==" + device.getName());
devices.add(device.getAddress());
deviceInfoList.add(new DeviceInfo(device.getName(), device.getAddress()));
for (DeviceInfo info : deviceInfoList) {
if (info.getName().contains("I9")) {
mac = info.getIdentifier();
}
}
}
}
} else {
if (device.getName() != null) {
Log.d(TAG, "device.getName()==" + device.getName());
devices.add(device.getAddress());
deviceInfoList.add(new DeviceInfo(device.getName(), device.getAddress()));
for (DeviceInfo info : deviceInfoList) {
if (info.getName().contains("I9")) {
mac = info.getIdentifier();
}
}
}
}
}
public void scanLeDevice(final boolean enable) {
Log.d(TAG, "扫描 LE 设备 " + enable);
BluetoothHelper.getConnectedBluetoothDevices(this, devices -> {
devices.forEach(this::setMac);
}).forEach(this::setMac);
if (enable) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
if ((mac == null || mac.isEmpty() || mac.isBlank()) && !mScanning) {
handler.post(this);
return;
}
Log.d(TAG, "停止扫描,已扫描到设备 " + mac);
mScanning = false;
mBluetoothAdapter.stopLeScan(leScanCallback);
}
}, SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(leScanCallback);
} else {
mScanning = false;
mBluetoothAdapter.stopLeScan(leScanCallback);
}
}
public boolean isScanning() {
return mScanning;
}
void init() {
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.e(TAG, "Bluetooth not supported");
return;
}
devices = new ArrayList<>();
deviceInfoList = new ArrayList<>();
scanLeDevice(true);
}
}
@@ -0,0 +1,619 @@
package com.serverapk.i9_bl_test;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.company.i9sslclient.TLVprocess.Field55Test;
import com.company.i9sslclient.TLVprocess.TlvData;
import com.company.i9sslclient.bean.CardInfo;
import com.company.i9sslclient.bean.KernelInfo;
import com.company.i9sslclient.bean.TerminalInfo;
import com.company.i9sslclient.ble.BleController;
import com.company.i9sslclient.lib.TradeTag;
import com.company.i9sslclient.lib.Util;
import com.company.i9sslclient.listener.DeviceConnectionListener;
import com.company.i9sslclient.listener.TerminalInfoListstener;
import com.company.i9sslclient.service.BluetoothLeService;
import com.google.gson.Gson;
import com.serverapk.i9_bl_test.model.CardData;
import com.serverapk.i9_bl_test.model.CardQuery;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
*
*/
public class ReadCardActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
public String errMsg, readerErrMsg, mac, tusn, appVersion, kernelVersion, tradeData, money = "1", countryCode = "0702", currencyCode = "0702", cardNum;
Handler handler = new Handler(Looper.getMainLooper());
private BluetoothAdapter mBluetoothAdapter;
private BluetoothLeService mBluetoothLeService;
private MyTerminalInfoListstener mMyTerminalInfoListstener;
private BleController mBluetoothController;
private CardData cardData;
private int keyType = 0;
private final Runnable readRunnable = new Runnable() {
@Override
public void run() {
try {
StringBuilder tradeDataStr = new StringBuilder();
tradeDataStr.append(TradeTag.TAG_1F01).append("01").append("71");
StringBuilder key = new StringBuilder();
key.append(Util.toHexString(keyType + 1));
String keyIndexStr = "0";
key.append(Util.toHexString(Integer.parseInt(keyIndexStr)));
tradeDataStr.append(TradeTag.TAG_1F04).append("06").append("020100000200");
tradeDataStr.append(TradeTag.TAG_1F02).append("04").append("10000000");
String date = Util.getCurrentDate() + Util.getCurrentTime();
tradeDataStr.append(TradeTag.TAG_1F03).append("07").append(date);
tradeDataStr.append(TradeTag.TAG_1F07).append("03").append("313233");
int amount = Integer.parseInt(money) * 100;
String amountStr = String.valueOf(amount);
String emvinfo = getString(amountStr, tradeDataStr);
tradeDataStr.append(TradeTag.TAG_1F09).append(Util.toHexString(emvinfo.length() / 2)).append(emvinfo);
Log.d(TAG, "tradeDataStr: " + tradeDataStr);
mBluetoothController.startEmvProcess(50, Util.HexToBin(tradeDataStr.toString()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@NonNull
private String getString(String amountStr, StringBuilder tradeDataStr) {
String lenStr = Util.toHexString(amountStr.length());
tradeDataStr.append(TradeTag.TAG_1F08).append(lenStr).append(Util.BinToHex(amountStr.getBytes()));
String transationType = "00";
return TradeTag.TAG_9C + "01" + transationType + TradeTag.TAG_9F1A + "02" + countryCode +
TradeTag.TAG_5F2A + "02" + currencyCode + "9a03" + Util.getCurrentDateYY() + "9f2103" + Util.getCurrentTime();
}
};
private ThreadPoolExecutor executorService;
private boolean isConnected = false;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService();
if (!mBluetoothLeService.initialize()) {
Log.e(TAG, "无法初始化蓝牙");
finish();
return;
}
mBluetoothController = BleController.getInstance(ReadCardActivity.this, null,
new DeviceConnectionListener() {
@Override
public void connected() {
isConnected = true;
Log.d(TAG, "connected1==========");
beginReadCard();
}
@Override
public void connectFail() {
isConnected = false;
Log.e(TAG, "connectFail==========");
}
@Override
public void disConnected() {
isConnected = false;
Log.e(TAG, "disConnected==========");
}
}, mMyTerminalInfoListstener, mBluetoothLeService);
if (mBluetoothController != null) {
Log.d(TAG, "连接设备\n" + mac);
if (mBluetoothController.isConnected()) {
beginReadCard();
return;
}
mBluetoothController.connectDevice(mac);
} else {
Log.e(TAG, "初始化 mBluetoothController 失败");
finish();
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mBluetoothLeService = null;
Log.d(TAG, "onServiceDisconnected=======");
}
};
public static String complement0(String str, int len, boolean bool) {
if (str.length() >= len) {
return str;
} else {
StringBuilder complementstr = new StringBuilder(str);
while (complementstr.length() < len) {
if (bool) {
complementstr.insert(0, "0");
} else {
complementstr.append("0");
}
}
return complementstr.toString();
}
}
private void beginReadCard() {
Log.d(TAG, "开始读卡流程");
currencyCode = complement0(currencyCode, 4, true);
countryCode = complement0(countryCode, 4, true);
if (executorService.getActiveCount() == 0 && executorService.getQueue().isEmpty() && !executorService.isShutdown() && !executorService.isTerminated()) {
executorService.execute(readRunnable);
}
}
private void handlerCard() {
CardQuery query = new CardQuery();
query.setCardInfo(cardData);
String json = new Gson().toJson(query);
Log.d(TAG, "卡信息\t" + json);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_read_card);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
findViewById(R.id.test).setOnClickListener(v -> finish());
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.e(TAG, "不支持蓝牙");
return;
}
executorService = new ThreadPoolExecutor(
1,
1,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
mMyTerminalInfoListstener = new MyTerminalInfoListstener();
mac = getIntent().getStringExtra("mac");
Log.d(TAG, "mac=" + mac);
Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
}
private final class MyTerminalInfoListstener implements TerminalInfoListstener {
@Override
public void onlineResponse(int code, byte[] reScript, byte[] data) {
Log.d(TAG, "在线响应 code: %d" + code);
String sb = "发送在线流程结果 code: " + code +
"reScript: " + Util.BinToHex(reScript) +
"data: " + Util.BinToHex(data);
Log.d(TAG, "在线响应\n" + sb);
}
@Override
public void onSendApdu(List<byte[]> lists) {
Log.d(TAG, "发送APDU数据");
StringBuilder sb = new StringBuilder();
if ((lists != null) && (!lists.isEmpty())) {
int size = lists.size();
for (int i = 0; i < size; i++) {
sb.append("\n").append(i).append(" : ").append(Util.BinToHex(lists.get(i)));
}
}
Log.d(TAG, "在 APDU 响应数据上\n" + sb);
}
@Override
public void onEncryptedData(byte[] data, String ksn) {
StringBuilder sb = new StringBuilder();
sb.append("在加密数据上:").append(Util.BinToHex(data));
if (ksn != null) {
sb.append("ksn:").append(ksn);
}
Log.d(TAG, sb.toString());
}
@Override
public void onPowerOnAPDU(int i, byte[] bytes) {
String sb = "开机 APDU" +
"\ncardType: " + i +
"\ndata: " + Util.BinToHex(bytes);
Log.d(TAG, sb);
}
@Override
public void onGetBatteryPower(int i) {
Log.d(TAG, "电池电量: " + i);
}
@Override
public void onGetTerminalDateTime(String terminalTime) {
Log.d(TAG, "终止时间: " + terminalTime);
}
@Override
public void onGetTerminalStatus(int status) {
Log.d(TAG, "获取终端状态: " + status);
}
@Override
public void onUserImport(String s) {
Log.d(TAG, "用户导入: " + s);
}
@Override
public void onCardInfoImport(String s, String s1) {
String sb = "onCardInfoImport : " + s +
"onCardInfoImport : " + s1;
Log.d(TAG, "卡信息导入: " + sb);
}
@Override
public void onGetTerminalInfo(TerminalInfo devInfo) {
StringBuilder sb = new StringBuilder();
sb.append("在“获取终端信息”上");
if (18 == devInfo.getTerminalType()) {
sb.append("\nModel Name: " + "i9");
} else {
sb.append("\nterminalType: ").append(devInfo.getTerminalType());
}
sb.append("\nTSN: ").append(devInfo.getTerminalNo());
if (devInfo.getKernelVer() != null) {
sb.append("\nKernel Version: ").append(devInfo.getKernelVer());
}
if (devInfo.getSoftVersion() != null) {
sb.append("\nApplicatin Version: ").append(devInfo.getSoftVersion());
appVersion = devInfo.getSoftVersion();
}
if (devInfo.getFirmwareVer() != null) {
sb.append("\nFirmware Version: ").append(devInfo.getFirmwareVer());
}
if (devInfo.getHardwareVer() != null) {
sb.append("\nHardware Version: ").append(devInfo.getHardwareVer());
}
if (devInfo.getSoftVersionDate() != null) {
sb.append("\nSoftware Date: ").append(devInfo.getSoftVersionDate());
}
if (devInfo.getTsn() != null) {
sb.append("\nSN: ").append(devInfo.getTsn().toLowerCase());
}
if (devInfo.getKernelVersion() != null) {
sb.append("\nkernelVersion: ").append(devInfo.getKernelVersion());
kernelVersion = devInfo.getKernelVersion();
}
if (devInfo.getBluetoothName() != null) {
sb.append("\nbluetoothName: ").append(devInfo.getBluetoothName());
}
if (devInfo.getBluetoothMAC() != null) {
sb.append("\nbluetoothMAC: ").append(devInfo.getBluetoothMAC());
}
if (devInfo.getBluetoothVersion() != null) {
sb.append("\nbluetoothVersion: ").append(devInfo.getBluetoothVersion());
}
if (devInfo.getProtocolType() != null) {
sb.append("\nprotocolType: ").append(devInfo.getProtocolType());
}
if (devInfo.getMerchaantNo() != null) {
sb.append("\nmerchaantNo: ").append(devInfo.getMerchaantNo());
}
if (devInfo.getTsn() != null) {
byte[] tsn = Util.hexStringToByteArray(devInfo.getTsn());
boolean isBcd = false;
for (byte b : tsn) {
if ((b <= 32) || (b > 126)) {
isBcd = true;
break;
}
}
if (isBcd) {
sb.append("\nSN: ").append(devInfo.getTsn().toLowerCase());
tusn = devInfo.getTsn().toLowerCase();
} else {
sb.append("\nSN: ").append(new String(tsn));
tusn = new String(tsn);
}
}
Log.d(TAG, String.valueOf(sb));
}
@Override
public void onGetKernelInfo(KernelInfo kernelInfo) {
Log.d(TAG, "获取内核信息:" + kernelInfo);
}
@Override
public void onTimeOut() {
Log.e(TAG, "超时================");
handler.removeCallbacks(readRunnable);
handler.post(readRunnable);
}
@Override
public void onGetCardNo(String cardNo) {
Log.d(TAG, "获取卡号:" + cardNo);
}
@Override
public void onCaculateMac(String mac, String ksn) {
Log.d(TAG, "mac = " + mac + ", ksn = " + ksn);
}
@Override
public void onGetMerchant(String merchant, String terminalNo) {
Log.d(TAG, "merchant = " + merchant + ", terminalNo = " + terminalNo);
}
@Override
public void onPINEntryResult(byte[] pin) {
Log.e(TAG, "onPINEntryResult: " + Util.BinToHex(pin));
}
@Override
public void onError(int code, String msg) {
String sb = new SimpleDateFormat("HH:mm:ss:SSS ").format(new java.util.Date()) + "onError" +
"\ncode: " + code +
"\nmsg: " + msg;
readerErrMsg = msg;
Log.e(TAG, "onError============\n" + sb);
}
@Override
public void onWaitingcard() {
Log.d(TAG, "onWaitingcard");
}
@Override
public void onWaitingPin() {
Log.e(TAG, "onWaitingPin");
}
@Override
public void onICCardInsertion() {
Log.e(TAG, "onICCardInsertion");
}
@Override
public void onNFCCardDetection() {
Log.d(TAG, "onNFCCardDetection");
}
@Override
public void onDecryptData(byte[] bytes) {
Log.e(TAG, "bytes:" + Util.BinToHex(bytes));
}
@Override
public void onReadCard(CardInfo cardInfo) {
Log.d(TAG, "读取卡信息: %s" + cardInfo.getCardNo());
tradeData = cardInfo.getEMVData();
Log.d(TAG, "onReadCard:" + new Gson().toJson(cardInfo));
cardNum = cardInfo.getCardNo();
cardData = new CardData();
StringBuilder sb = new StringBuilder();
sb.append("{");
if (cardInfo.getControlModel() != null) {
cardData.setControlModel(cardInfo.getControlModel());
sb.append("\ncontrolModel: ").append(cardInfo.getControlModel());
}
cardData.setCardType(cardInfo.getCardType() + "");
sb.append("\ncardType: ").append(cardInfo.getCardType());
if (cardInfo.getCardexpiryDate() != null) {
cardData.setCardexpiryDate(cardInfo.getCardexpiryDate());
sb.append("\ncardexpiryDate: ").append(cardInfo.getCardexpiryDate());
}
if (cardInfo.getTracks() != null) {
sb.append("\ntracks: ").append(Util.BinToHex(cardInfo.getTracks()));
}
if (cardInfo.getCardNo() != null) {
cardData.setCardNo(cardInfo.getCardNo());
sb.append("\ncardNo: ").append(cardInfo.getCardNo());
}
if (cardInfo.getCardName() != null) {
cardData.setCardName(cardInfo.getCardName());
sb.append("\nCardName: ").append(cardInfo.getCardName());
}
if (cardInfo.getPAN() != null) {
sb.append("\nPAN: ").append(cardInfo.getPAN());
}
if (cardInfo.getCardSerial() != null) {
cardData.setCardSerial(cardInfo.getCardSerial());
sb.append("\nCardSerial: ").append(cardInfo.getCardSerial());
}
if (cardInfo.getCVM() != null) {
cardData.setCVM(cardInfo.getCVM());
sb.append("\nCVM: ").append(cardInfo.getCVM());
}
if (cardInfo.getIcTracks() != null) {
cardData.setIcTracks(Util.BinToHex(cardInfo.getIcTracks()));
sb.append("\nicTracks: ").append(Util.BinToHex(cardInfo.getIcTracks()));
}
if (cardInfo.getTerimalNo() != null) {
sb.append("\nterimalNo: ").append(cardInfo.getTerimalNo());
}
cardData.setNfcTradeRespose(cardInfo.getNfcTradeRespose() + "");
sb.append("\nnfcTradeRespose: ").append(cardInfo.getNfcTradeRespose());
if (cardInfo.getTracklength() != null) {
cardData.setTracklength(Util.BinToHex(cardInfo.getTracklength()));
sb.append("\ntracklength: ").append(Util.BinToHex(cardInfo.getTracklength()));
}
if (cardInfo.getOriginalTrack() != null) {
cardData.setOriginalTrack(Util.BinToHex(cardInfo.getOriginalTrack()));
sb.append("\noriginalTrack: ").append(Util.BinToHex(cardInfo.getOriginalTrack()));
}
if (cardInfo.getKSN() != null) {
sb.append("\nKSN: ").append(cardInfo.getKSN());
}
if (cardInfo.getServiceCode() != null) {
cardData.setServiceCode(cardInfo.getServiceCode());
sb.append("\nServiceCode: ").append(cardInfo.getServiceCode());
}
if ((cardInfo.getOriginalTrack() != null) && (cardInfo.getTracklength() != null)) {
int trck1len = (cardInfo.getTracklength()[0] & 0xFF);
int trck2len = ((cardInfo.getTracklength()[1] & 0xFF) + 1) / 2;
int trck3len = ((cardInfo.getTracklength()[2] & 0xFF) + 1) / 2;
if (trck1len > 0) {
byte[] trck1 = new byte[trck1len];
System.arraycopy(cardInfo.getOriginalTrack(), 0, trck1, 0, trck1len);
sb.append("\ntrck1:").append(Util.BinToHex(trck1));
}
if (trck2len > 0) {
byte[] trck2 = new byte[trck2len];
System.arraycopy(cardInfo.getOriginalTrack(), trck1len, trck2, 0, trck2len);
sb.append("\ntrck2:").append(Util.BinToHex(trck2).substring(0, ((cardInfo.getTracklength()[1] & 0xFF))));
}
if (trck3len > 0) {
byte[] trck3 = new byte[trck3len];
System.arraycopy(cardInfo.getOriginalTrack(), trck1len + trck2len, trck3, 0, trck3len);
sb.append("\ntrck3:").append(Util.BinToHex(trck3).substring(0, ((cardInfo.getTracklength()[2] & 0xFF))));
}
}
if ("03".equals(cardInfo.getKernel_Type())) {
sb.append("\nKernel Type: ").append(cardInfo.getKernel_Type());
sb.append("\nOutcome Parameter Set: ").append(cardInfo.getOutcome_Parameter_Set());
sb.append("\nUser Interface Request_Data: ").append(cardInfo.getUser_Interface_Request_Data());
sb.append("\nError Indication: ").append(cardInfo.getError_Indication());
}
if (cardInfo.getIcTracks() != null) {
String tags = Util.BinToHex(cardInfo.getIcTracks());
if (!TextUtils.isEmpty(tags)) {
List<TlvData> tlvDataList = Field55Test.parseFiled55Data(tags);
for (TlvData tlvData : tlvDataList) {
if ("57".equals(tlvData.getTag())) {
Log.d(TAG, "tag_57 value: " + tlvData.getValue());
}
}
}
}
Log.d(TAG, "读取卡信息=======" + new Gson().toJson(cardData));
int cardType = cardInfo.getCardType();
if (cardType != -1) {
cardType = cardType & 0x0F;
switch (cardType) {
case 1:
case 13:
mBluetoothController.sendOnlineProcessResult(null);
break;
case 0:
case 2:
default:
break;
}
}
handlerCard();
}
@Override
public void onShowLog(String l) {
Log.d(TAG, new SimpleDateFormat("HH:mm:ss:SSS ").format(new java.util.Date()) + "onShowLog log: " + l);
}
@Override
public void onStopTradeSuccess() {
Log.e(TAG, "onStopTradeSuccess");
}
@Override
public void onExecuteSuccess(int code) {
Log.e(TAG, "onExecuteSuccess code: " + code);
if (code == 14) {
Log.e(TAG, "app download success");
} else if (code == 15) {
Log.e(TAG, "kernel download success");
} else if (code == 16) {
Log.e(TAG, "key download success");
}
}
@Override
public void onData(int type, int code, byte[]... pin) {
StringBuilder sb = new StringBuilder();
sb.append("type: ").append(type);
sb.append("\ncode: ").append(code);
if (pin != null) {
int len = pin.length;
for (int i = 0; i < len; i++) {
sb.append("\n").append(i).append(": ").append(Util.BinToHex(pin[i]));
}
}
Log.d(TAG, sb.toString());
}
}
}
@@ -0,0 +1,149 @@
package com.serverapk.i9_bl_test.model;
import java.io.Serializable;
/**
* 查询卡的使用扇区和密码保存扇区密码的请求实体
*/
public class CardData implements Serializable {
private static final long serialVersionUID = 1L;
private String controlModel;
private String cardType;
private String cardexpiryDate;
private String cardNo;
private String sign;
private String cardName;
private String cardSerial;
private String CVM;
private String icTracks;
private String nfcTradeRespose;
private String tracklength;
private String originalTrack;
private String serviceCode;
private String trck2;
private String cvv;
public String getControlModel() {
return controlModel;
}
public void setControlModel(String controlModel) {
this.controlModel = controlModel;
}
public String getCardexpiryDate() {
return cardexpiryDate;
}
public void setCardexpiryDate(String cardexpiryDate) {
this.cardexpiryDate = cardexpiryDate;
}
public String getCardNo() {
return cardNo;
}
public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getCardName() {
return cardName;
}
public void setCardName(String cardName) {
this.cardName = cardName;
}
public String getCardSerial() {
return cardSerial;
}
public void setCardSerial(String cardSerial) {
this.cardSerial = cardSerial;
}
public String getCVM() {
return CVM;
}
public void setCVM(String CVM) {
this.CVM = CVM;
}
public String getIcTracks() {
return icTracks;
}
public void setIcTracks(String icTracks) {
this.icTracks = icTracks;
}
public String getCardType() {
return cardType;
}
public void setCardType(String cardType) {
this.cardType = cardType;
}
public String getNfcTradeRespose() {
return nfcTradeRespose;
}
public void setNfcTradeRespose(String nfcTradeRespose) {
this.nfcTradeRespose = nfcTradeRespose;
}
public String getTracklength() {
return tracklength;
}
public void setTracklength(String tracklength) {
this.tracklength = tracklength;
}
public String getOriginalTrack() {
return originalTrack;
}
public void setOriginalTrack(String originalTrack) {
this.originalTrack = originalTrack;
}
public String getServiceCode() {
return serviceCode;
}
public void setServiceCode(String serviceCode) {
this.serviceCode = serviceCode;
}
public String getTrck2() {
return trck2;
}
public void setTrck2(String trck2) {
this.trck2 = trck2;
}
public String getCvv() {
return cvv;
}
public void setCvv(String cvv) {
this.cvv = cvv;
}
}
@@ -0,0 +1,16 @@
package com.serverapk.i9_bl_test.model;
import java.io.Serializable;
public class CardQuery implements Serializable {
private static final long serialVersionUID = 111L;
private CardData cardInfo;
public CardData getCardInfo() {
return cardInfo;
}
public void setCardInfo(CardData cardInfo) {
this.cardInfo = cardInfo;
}
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跳转读卡"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ReadCardActivity">
<TextView
android:id="@+id/test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="返回"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+7
View File
@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.I9bltest" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">i9-bl-test</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.I9bltest" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.I9bltest" parent="Base.Theme.I9bltest" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.serverapk.i9_bl_test;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}