added remote smart card reader app
This commit is contained in:
34
remote-reader/app/src/main/AndroidManifest.xml
Normal file
34
remote-reader/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.vsmartcard.remotesmartcardreader.app" >
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="true" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
<activity
|
||||
android:name="com.vsmartcard.remotesmartcardreader.app.MainActivity"
|
||||
android:label="@string/app_name" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.TECH_DISCOVERED" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.nfc.action.TECH_DISCOVERED"
|
||||
android:resource="@xml/nfc_tech_filter" />
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
remote-reader/app/src/main/ic_launcher-web.png
Normal file
BIN
remote-reader/app/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,39 @@
|
||||
package com.vsmartcard.remotesmartcardreader.app;
|
||||
|
||||
public class Hex {
|
||||
|
||||
private static final char[] BYTE2HEX=(
|
||||
"000102030405060708090A0B0C0D0E0F"+
|
||||
"101112131415161718191A1B1C1D1E1F"+
|
||||
"202122232425262728292A2B2C2D2E2F"+
|
||||
"303132333435363738393A3B3C3D3E3F"+
|
||||
"404142434445464748494A4B4C4D4E4F"+
|
||||
"505152535455565758595A5B5C5D5E5F"+
|
||||
"606162636465666768696A6B6C6D6E6F"+
|
||||
"707172737475767778797A7B7C7D7E7F"+
|
||||
"808182838485868788898A8B8C8D8E8F"+
|
||||
"909192939495969798999A9B9C9D9E9F"+
|
||||
"A0A1A2A3A4A5A6A7A8A9AAABACADAEAF"+
|
||||
"B0B1B2B3B4B5B6B7B8B9BABBBCBDBEBF"+
|
||||
"C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF"+
|
||||
"D0D1D2D3D4D5D6D7D8D9DADBDCDDDEDF"+
|
||||
"E0E1E2E3E4E5E6E7E8E9EAEBECEDEEEF"+
|
||||
"F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF").toCharArray();
|
||||
|
||||
public static String getHexString(byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
return "";
|
||||
}
|
||||
final int len=bytes.length;
|
||||
final char[] chars=new char[len<<1];
|
||||
int hexIndex;
|
||||
int idx=0;
|
||||
int ofs=0;
|
||||
while (ofs<len) {
|
||||
hexIndex=(bytes[ofs++] & 0xFF)<<1;
|
||||
chars[idx++]=BYTE2HEX[hexIndex++];
|
||||
chars[idx++]=BYTE2HEX[hexIndex];
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Frank Morgner
|
||||
*
|
||||
* This file is part of RemoteSmartCardReader.
|
||||
*
|
||||
* RemoteSmartCardReader is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* RemoteSmartCardReader is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* RemoteSmartCardReader. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.vsmartcard.remotesmartcardreader.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Layout;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.vsmartcard.remotesmartcardreader.app.screaders.DummyReader;
|
||||
import com.vsmartcard.remotesmartcardreader.app.screaders.NFCReader;
|
||||
import com.vsmartcard.remotesmartcardreader.app.screaders.SCReader;
|
||||
import android.text.ClipboardManager;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
class VPCDHandler extends Handler {
|
||||
VPCDHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
switch (message.what) {
|
||||
case MessageSender.MESSAGE_CONNECTED:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_connected_to)+" "+message.obj+"\n");
|
||||
spinner.setVisibility(View.INVISIBLE);
|
||||
break;
|
||||
case MessageSender.MESSAGE_DISCONNECTED:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_disconnected)+"\n");
|
||||
testing = false;
|
||||
updateLabels();
|
||||
break;
|
||||
case MessageSender.MESSAGE_ATR:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_atr)+": "+message.obj+"\n");
|
||||
break;
|
||||
case MessageSender.MESSAGE_ON:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_on)+"\n");
|
||||
break;
|
||||
case MessageSender.MESSAGE_OFF:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_off)+"\n");
|
||||
break;
|
||||
case MessageSender.MESSAGE_RESET:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_reset)+"\n");
|
||||
break;
|
||||
case MessageSender.MESSAGE_CAPDU:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_capdu)+": "+message.obj+"\n");
|
||||
break;
|
||||
case MessageSender.MESSAGE_RAPDU:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_rapdu)+": "+message.obj+"\n");
|
||||
break;
|
||||
case MessageSender.MESSAGE_ERROR:
|
||||
if (testing) {
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_error)+": "+message.obj+"\n");
|
||||
testing = false;
|
||||
vpcdDisconnect();
|
||||
updateLabels();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_unknown)+" "+Integer.toString(message.what)+"\n");
|
||||
break;
|
||||
}
|
||||
Layout layout = textViewVPCDStatus.getLayout();
|
||||
if (layout != null) {
|
||||
int scrollAmount = layout.getLineTop(textViewVPCDStatus.getLineCount()) - textViewVPCDStatus.getHeight();
|
||||
if (scrollAmount > 0)
|
||||
textViewVPCDStatus.scrollTo(0, scrollAmount);
|
||||
else
|
||||
textViewVPCDStatus.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private EditText editTextVPCDHost;
|
||||
private EditText editTextVPCDPort;
|
||||
private TextView textViewVPCDStatus;
|
||||
private TextView textViewInfo;
|
||||
private Button button;
|
||||
private ProgressBar spinner;
|
||||
private VPCDHandler handler;
|
||||
private VPCDWorker vpcdTest;
|
||||
private boolean testing;
|
||||
private String hostname;
|
||||
private String port;
|
||||
private String savedLog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
textViewInfo = (TextView) findViewById(R.id.textViewInfo);
|
||||
editTextVPCDHost = (EditText) findViewById(R.id.editTextHostname);
|
||||
editTextVPCDPort = (EditText) findViewById(R.id.editTextPort);
|
||||
textViewVPCDStatus = (TextView) findViewById(R.id.textViewLog);
|
||||
textViewVPCDStatus.setMovementMethod(new ScrollingMovementMethod());
|
||||
spinner = (ProgressBar) findViewById(R.id.progressBar);
|
||||
button = (Button) findViewById(R.id.buttonDisConnect);
|
||||
|
||||
EditText.OnEditorActionListener vpcdWatcher = new EditText.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
|
||||
if (i == EditorInfo.IME_ACTION_DONE) {
|
||||
editTextOnEditorDone();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
editTextVPCDHost.setOnEditorActionListener(vpcdWatcher);
|
||||
editTextVPCDPort.setOnEditorActionListener(vpcdWatcher);
|
||||
|
||||
handler = new VPCDHandler(Looper.getMainLooper());
|
||||
|
||||
PendingIntent.getActivity(this, 0, new Intent(this, this.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
|
||||
|
||||
loadSettings();
|
||||
updateLabels();
|
||||
}
|
||||
|
||||
private static String saved_status_key = "textViewVPCDStatus";
|
||||
@Override
|
||||
public void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
textViewVPCDStatus.setText(savedInstanceState.getCharSequence(saved_status_key));
|
||||
}
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
savedInstanceState.putCharSequence(saved_status_key, textViewVPCDStatus.getText());
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
testing = false;
|
||||
vpcdDisconnect();
|
||||
}
|
||||
|
||||
|
||||
public void buttonOnClickDisConnect(View view) {
|
||||
if (testing) {
|
||||
testing = false;
|
||||
vpcdDisconnect();
|
||||
} else {
|
||||
saveSettings();
|
||||
testing = true;
|
||||
spinner.setVisibility(View.VISIBLE);
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_dummy_start)+"\n");
|
||||
vpcdConnect(new DummyReader());
|
||||
}
|
||||
updateLabels();
|
||||
}
|
||||
|
||||
private void editTextOnEditorDone() {
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_dummy_start)+"\n");
|
||||
forceConnect(new DummyReader());
|
||||
}
|
||||
|
||||
private void vpcdConnect(SCReader scReader) {
|
||||
vpcdTest = new VPCDWorker(hostname, Integer.parseInt(port), scReader, handler);
|
||||
new Thread(vpcdTest).start();
|
||||
}
|
||||
|
||||
private void vpcdDisconnect() {
|
||||
if (vpcdTest != null) {
|
||||
vpcdTest.close();
|
||||
vpcdTest = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLabels() {
|
||||
if (testing) {
|
||||
button.setText(getResources().getString(R.string.button_stop_testing));
|
||||
} else {
|
||||
button.setText(getResources().getString(R.string.button_test_configuration));
|
||||
spinner.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
Settings settings = new Settings(this);
|
||||
hostname = settings.getVPCDHost();
|
||||
port = settings.getVPCDPort();
|
||||
editTextVPCDHost.setText(hostname);
|
||||
editTextVPCDPort.setText(port);
|
||||
}
|
||||
|
||||
private void saveSettings() {
|
||||
Editable textHostname = editTextVPCDHost.getText();
|
||||
Editable textPort = editTextVPCDPort.getText();
|
||||
if (textHostname != null) {
|
||||
hostname = textHostname.toString();
|
||||
}
|
||||
if (textPort != null) {
|
||||
port = textPort.toString();
|
||||
}
|
||||
Settings settings = new Settings(this);
|
||||
settings.setVPCDSettings(hostname, port);
|
||||
}
|
||||
|
||||
private void forceConnect(SCReader scReader) {
|
||||
if (testing) {
|
||||
testing = false;
|
||||
vpcdDisconnect();
|
||||
}
|
||||
saveSettings();
|
||||
testing = true;
|
||||
spinner.setVisibility(View.VISIBLE);
|
||||
vpcdConnect(scReader);
|
||||
updateLabels();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Check to see that the Activity started due to a discovered tag
|
||||
if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(getIntent().getAction())) {
|
||||
NFCReader nfcReader = NFCReader.processIntent(getIntent());
|
||||
if (nfcReader != null) {
|
||||
textViewVPCDStatus.append(getResources().getString(R.string.status_tag_discovered)+"\n");
|
||||
forceConnect(nfcReader);
|
||||
} else {
|
||||
super.onNewIntent(getIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
// onResume gets called after this to handle the intent
|
||||
setIntent(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Frank Morgner
|
||||
*
|
||||
* This file is part of RemoteSmartCardReader.
|
||||
*
|
||||
* RemoteSmartCardReader is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* RemoteSmartCardReader is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* RemoteSmartCardReader. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.vsmartcard.remotesmartcardreader.app;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
|
||||
public class MessageSender {
|
||||
public static final int MESSAGE_CONNECTED = 0;
|
||||
public static final int MESSAGE_DISCONNECTED = 1;
|
||||
public static final int MESSAGE_ERROR = 2;
|
||||
public static final int MESSAGE_ATR = 3;
|
||||
public static final int MESSAGE_ON = 4;
|
||||
public static final int MESSAGE_OFF = 5;
|
||||
public static final int MESSAGE_RESET = 6;
|
||||
public static final int MESSAGE_CAPDU = 7;
|
||||
public static final int MESSAGE_RAPDU = 8;
|
||||
private Handler handler;
|
||||
|
||||
public MessageSender(Handler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
private void sendMessage(int messageCode, Object object) {
|
||||
Message message = handler.obtainMessage(messageCode, object);
|
||||
handler.sendMessage(message);
|
||||
}
|
||||
|
||||
public void sendConnected(String details) {
|
||||
sendMessage(MESSAGE_CONNECTED, details);
|
||||
}
|
||||
|
||||
public void sendATR(String details) {
|
||||
sendMessage(MESSAGE_ATR, details);
|
||||
}
|
||||
|
||||
public void sendCAPDU(String details) {
|
||||
sendMessage(MESSAGE_CAPDU, details);
|
||||
}
|
||||
|
||||
public void sendDisconnected(String details) {
|
||||
sendMessage(MESSAGE_DISCONNECTED, details);
|
||||
}
|
||||
|
||||
public void sendError(String details) {
|
||||
sendMessage(MESSAGE_ERROR, details);
|
||||
}
|
||||
|
||||
public void sendOff() {
|
||||
sendMessage(MESSAGE_OFF, "");
|
||||
}
|
||||
|
||||
public void sendOn() {
|
||||
sendMessage(MESSAGE_ON, "");
|
||||
}
|
||||
|
||||
public void sendRAPDU(String details) {
|
||||
sendMessage(MESSAGE_RAPDU, details);
|
||||
}
|
||||
|
||||
public void sendReset() {
|
||||
sendMessage(MESSAGE_RESET, "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Frank Morgner
|
||||
*
|
||||
* This file is part of RemoteSmartCardReader.
|
||||
*
|
||||
* RemoteSmartCardReader is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* RemoteSmartCardReader is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* RemoteSmartCardReader. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.vsmartcard.remotesmartcardreader.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class Settings {
|
||||
|
||||
private static final String KEY_VPCD_HOST = "hostname";
|
||||
private static final String KEY_VPCD_PORT = "port";
|
||||
|
||||
private static final String DEFAULT_VPCD_HOST = "10.0.2.2";
|
||||
private static final String DEFAULT_VPCD_PORT = "35963";
|
||||
|
||||
private final SharedPreferences settings;
|
||||
|
||||
public Settings (Context activity) {
|
||||
settings = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
}
|
||||
|
||||
private void setString(String key, String value) {
|
||||
Editor editor = settings.edit();
|
||||
editor.putString(key, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void setBoolean(String key, boolean value) {
|
||||
Editor editor = settings.edit();
|
||||
editor.putBoolean(key, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public void setVPCDHost(String hostname) {
|
||||
setString(KEY_VPCD_HOST, hostname);
|
||||
}
|
||||
|
||||
public void setVPCDPort(String port) {
|
||||
setString(KEY_VPCD_PORT, port);
|
||||
}
|
||||
|
||||
public void setVPCDSettings(String hostname, String port) {
|
||||
Editor editor = settings.edit();
|
||||
editor.putString(KEY_VPCD_HOST, hostname);
|
||||
editor.putString(KEY_VPCD_PORT, port);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public String getVPCDHost() {
|
||||
return settings.getString(KEY_VPCD_HOST, DEFAULT_VPCD_HOST);
|
||||
}
|
||||
|
||||
public String getVPCDPort() {
|
||||
return settings.getString(KEY_VPCD_PORT, DEFAULT_VPCD_PORT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Frank Morgner
|
||||
*
|
||||
* This file is part of RemoteSmartCardReader.
|
||||
*
|
||||
* RemoteSmartCardReader is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* RemoteSmartCardReader is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* RemoteSmartCardReader. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.vsmartcard.remotesmartcardreader.app;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import com.vsmartcard.remotesmartcardreader.app.screaders.SCReader;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
public class VPCDWorker implements Runnable, Closeable {
|
||||
private int port;
|
||||
private String hostname;
|
||||
private SCReader reader;
|
||||
private Socket socket;
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private boolean doRun;
|
||||
private MessageSender messageSender;
|
||||
|
||||
public VPCDWorker(String hostname, int port, SCReader reader, Handler handler) {
|
||||
this.hostname = hostname;
|
||||
this.port = port;
|
||||
this.reader = reader;
|
||||
this.messageSender = new MessageSender(handler);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
doRun = false;
|
||||
try {
|
||||
vpcdDisconnect();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static final int VPCD_CTRL_LEN = 1;
|
||||
private static final byte VPCD_CTRL_OFF = 0;
|
||||
private static final byte VPCD_CTRL_ON = 1;
|
||||
private static final byte VPCD_CTRL_RESET = 2;
|
||||
private static final byte VPCD_CTRL_ATR = 4;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
|
||||
doRun = true;
|
||||
|
||||
try {
|
||||
vpcdConnect();
|
||||
messageSender.sendConnected(hostname+":"+Integer.toString(port));
|
||||
|
||||
while (doRun) {
|
||||
byte[] in = receiveFromVPCD();
|
||||
if (in == null) {
|
||||
doRun = false;
|
||||
break;
|
||||
}
|
||||
byte[] out = null;
|
||||
|
||||
if (in.length == VPCD_CTRL_LEN) {
|
||||
switch (in[0]) {
|
||||
case VPCD_CTRL_OFF:
|
||||
reader.powerOff();
|
||||
messageSender.sendOff();
|
||||
break;
|
||||
case VPCD_CTRL_ON:
|
||||
reader.powerOn();
|
||||
messageSender.sendOn();
|
||||
break;
|
||||
case VPCD_CTRL_RESET:
|
||||
reader.reset();
|
||||
messageSender.sendReset();
|
||||
break;
|
||||
case VPCD_CTRL_ATR:
|
||||
out = reader.getATR();
|
||||
//messageSender.sendATR(Hex.getHexString(out));
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unhandled command from VPCD.");
|
||||
}
|
||||
} else {
|
||||
messageSender.sendCAPDU(Hex.getHexString(in));
|
||||
out = reader.transmit(in);
|
||||
messageSender.sendRAPDU(Hex.getHexString(out));
|
||||
}
|
||||
if (out != null) {
|
||||
sendToVPCD(out);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (doRun) {
|
||||
e.printStackTrace();
|
||||
messageSender.sendError(e.getMessage());
|
||||
} else {
|
||||
messageSender.sendDisconnected("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] receiveFromVPCD() throws IOException {
|
||||
/* convert length from network byte order.
|
||||
Note that Java always uses network byte order internally. */
|
||||
int length1 = inputStream.read();
|
||||
int length2 = inputStream.read();
|
||||
if (length1 == -1 || length2 == -1) {
|
||||
// EOF
|
||||
return null;
|
||||
}
|
||||
int length = (length1 << 8) + length2;
|
||||
|
||||
byte[] data = new byte[length];
|
||||
|
||||
int offset = 0;
|
||||
while (length > 0) {
|
||||
int read = inputStream.read(data, offset, length);
|
||||
if (read == -1) {
|
||||
// EOF
|
||||
return null;
|
||||
}
|
||||
offset += read;
|
||||
length -= read;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private void sendToVPCD(byte[] data) throws IOException {
|
||||
/* convert length to network byte order.
|
||||
Note that Java always uses network byte order internally. */
|
||||
byte[] length = new byte[2];
|
||||
length[0] = (byte) (data.length >> 8);
|
||||
length[1] = (byte) (data.length & 0xff);
|
||||
outputStream.write(length);
|
||||
|
||||
outputStream.write(data, 0, data.length);
|
||||
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
private void vpcdConnect() throws IOException {
|
||||
socket = new Socket(InetAddress.getByName(hostname), port);
|
||||
outputStream = socket.getOutputStream();
|
||||
inputStream = socket.getInputStream();
|
||||
}
|
||||
|
||||
private void vpcdDisconnect() throws IOException {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Frank Morgner
|
||||
*
|
||||
* This file is part of RemoteSmartCardReader.
|
||||
*
|
||||
* RemoteSmartCardReader is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* RemoteSmartCardReader is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* RemoteSmartCardReader. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.vsmartcard.remotesmartcardreader.app.screaders;
|
||||
|
||||
public class DummyReader implements SCReader {
|
||||
|
||||
@Override
|
||||
public void eject() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean present() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void powerOn() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void powerOff() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
}
|
||||
|
||||
private static final byte[] dummyATR = {0x3B, 0x68, 0x00, (byte) 0xFF, 56, 43, 0x41, 0x52, 0x44, 0x6E, 0x73, 0x73};
|
||||
@Override
|
||||
public byte[] getATR() {
|
||||
return dummyATR;
|
||||
}
|
||||
|
||||
private static final byte[] dummyResponse = {0x6F, 0x00};
|
||||
@Override
|
||||
public byte[] transmit(byte[] apdu) {
|
||||
return dummyResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Frank Morgner
|
||||
*
|
||||
* This file is part of RemoteSmartCardReader.
|
||||
*
|
||||
* RemoteSmartCardReader is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* RemoteSmartCardReader is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* RemoteSmartCardReader. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.vsmartcard.remotesmartcardreader.app.screaders;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.util.Log;
|
||||
|
||||
import com.vsmartcard.remotesmartcardreader.app.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class NFCReader implements SCReader {
|
||||
|
||||
private IsoDep card;
|
||||
|
||||
public NFCReader(IsoDep sc) {
|
||||
this.card = sc;
|
||||
}
|
||||
@Override
|
||||
public void eject() throws IOException {
|
||||
card.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean present() {
|
||||
return card.isConnected();
|
||||
}
|
||||
|
||||
private static int TIMEOUT = 10000;
|
||||
@Override
|
||||
public void powerOn() throws IOException {
|
||||
card.connect();
|
||||
card.setTimeout(TIMEOUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void powerOff() throws IOException {
|
||||
card.close();
|
||||
}
|
||||
|
||||
private static byte[] SELECT_MF = {(byte) 0x00, (byte) 0xa4, (byte) 0x00, (byte) 0x0C};
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
byte[] response = card.transceive(SELECT_MF);
|
||||
if (response.length == 2 && response[0] == 0x90 && response[1] == 0x00) {
|
||||
Log.d(this.getClass().getName(), "Resetting the card by selecting the MF results in " + Hex.getHexString(response));
|
||||
}
|
||||
}
|
||||
|
||||
/* calculation based on https://code.google.com/p/ifdnfc/source/browse/src/atr.c */
|
||||
@Override
|
||||
public byte[] getATR() {
|
||||
byte[] historicalBytes = card.getHistoricalBytes();
|
||||
if (historicalBytes == null) {
|
||||
historicalBytes = new byte[0];
|
||||
}
|
||||
|
||||
/* copy historical bytes if available */
|
||||
byte[] atr = new byte[4+historicalBytes.length+1];
|
||||
atr[0] = (byte)0x3b;
|
||||
atr[1] = (byte) (0x80+historicalBytes.length);
|
||||
atr[2] = (byte) 0x80;
|
||||
atr[3] = (byte)0x01;
|
||||
System.arraycopy(historicalBytes, 0, atr, 4, historicalBytes.length);
|
||||
|
||||
/* calculate TCK */
|
||||
byte tck = atr[1];
|
||||
for (int idx = 2; idx < atr.length; idx++) {
|
||||
tck ^= atr[idx];
|
||||
}
|
||||
atr[atr.length-1] = tck;
|
||||
|
||||
return atr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] transmit(byte[] apdu) throws IOException {
|
||||
return card.transceive(apdu);
|
||||
}
|
||||
|
||||
public static NFCReader processIntent(Intent intent) {
|
||||
NFCReader nfcReader = null;
|
||||
|
||||
if (intent.getExtras() != null) {
|
||||
Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||
if (tag != null) {
|
||||
nfcReader = new NFCReader(IsoDep.get(tag));
|
||||
}
|
||||
}
|
||||
|
||||
return nfcReader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Frank Morgner
|
||||
*
|
||||
* This file is part of RemoteSmartCardReader.
|
||||
*
|
||||
* RemoteSmartCardReader is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* RemoteSmartCardReader is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* RemoteSmartCardReader. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.vsmartcard.remotesmartcardreader.app.screaders;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface SCReader {
|
||||
|
||||
public void eject() throws IOException;
|
||||
|
||||
public boolean present();
|
||||
public void powerOn() throws IOException;
|
||||
public void powerOff() throws IOException;
|
||||
public void reset() throws IOException;
|
||||
|
||||
/**
|
||||
* Receive ATR from the smart card.
|
||||
*
|
||||
* @return ATR on success or null in case of an error
|
||||
*/
|
||||
public byte[] getATR();
|
||||
|
||||
/**
|
||||
* Send an APDU to the smart card
|
||||
*
|
||||
* @param apdu Data to be sent
|
||||
* @return response data or null in case of an error
|
||||
*/
|
||||
public byte[] transmit(byte[] apdu) throws IOException;
|
||||
}
|
||||
BIN
remote-reader/app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
remote-reader/app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
remote-reader/app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
remote-reader/app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
remote-reader/app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
remote-reader/app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
remote-reader/app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
remote-reader/app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,73 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context="com.vsmartcard.remotesmartcardreader.app.MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="@string/vpcd_config_info"
|
||||
android:id="@+id/textViewInfo"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toLeftOf="@+id/buttonDisConnect" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/editTextHostname"
|
||||
android:hint="@string/hint_vpcd_hostname"
|
||||
android:inputType="textUri"
|
||||
android:layout_below="@+id/textViewInfo"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toLeftOf="@+id/editTextPort" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:ems="10"
|
||||
android:id="@+id/editTextPort"
|
||||
android:hint="@string/hint_vpcd_port"
|
||||
android:layout_below="@+id/textViewInfo"
|
||||
android:layout_toLeftOf="@+id/buttonDisConnect" />
|
||||
|
||||
<Button
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/buttonDisConnect"
|
||||
android:onClick="buttonOnClickDisConnect"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true" />
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_below="@+id/buttonDisConnect"
|
||||
android:layout_alignRight="@+id/buttonDisConnect"
|
||||
android:layout_alignEnd="@+id/buttonDisConnect" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:id="@+id/textViewLog"
|
||||
android:typeface="monospace"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/editTextHostname" />
|
||||
|
||||
</RelativeLayout>
|
||||
75
remote-reader/app/src/main/res/layout/activity_main.xml
Normal file
75
remote-reader/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,75 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context="com.vsmartcard.remotesmartcardreader.app.MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="@string/vpcd_config_info"
|
||||
android:id="@+id/textViewInfo"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/editTextHostname"
|
||||
android:layout_below="@+id/textViewInfo"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:hint="@string/hint_vpcd_hostname"
|
||||
android:inputType="textUri"
|
||||
android:layout_toLeftOf="@+id/editTextPort" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:ems="10"
|
||||
android:id="@+id/editTextPort"
|
||||
android:hint="@string/hint_vpcd_port"
|
||||
android:layout_below="@+id/textViewInfo"
|
||||
android:layout_alignRight="@+id/textViewInfo"
|
||||
android:layout_alignEnd="@+id/textViewInfo" />
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/buttonDisConnect"
|
||||
android:onClick="buttonOnClickDisConnect"
|
||||
android:layout_below="@+id/editTextPort"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toLeftOf="@+id/progressBar" />
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignTop="@+id/buttonDisConnect" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:id="@+id/textViewLog"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:typeface="monospace"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_below="@+id/buttonDisConnect" />
|
||||
|
||||
</RelativeLayout>
|
||||
10
remote-reader/app/src/main/res/menu/main.xml
Normal file
10
remote-reader/app/src/main/res/menu/main.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<menu 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"
|
||||
tools:context="com.vsmartcard.remotesmartcardreader.app.MainActivity" >
|
||||
|
||||
<item android:id="@+id/action_settings"
|
||||
android:title="@string/action_settings"
|
||||
android:orderInCategory="100"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
6
remote-reader/app/src/main/res/values/dimens.xml
Normal file
6
remote-reader/app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
||||
</resources>
|
||||
33
remote-reader/app/src/main/res/values/strings.xml
Normal file
33
remote-reader/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Remote Smart Card Reader</string>
|
||||
|
||||
<string name="button_test_configuration">Connect</string>
|
||||
<string name="button_stop_testing">Disconnect</string>
|
||||
|
||||
<string name="hint_vpcd_port">port</string>
|
||||
<string name="hint_vpcd_hostname">VPCD hostname</string>
|
||||
|
||||
<string name="vpcd_config_info">Settings for <a href="http://frankmorgner.github.io/vsmartcard/virtualsmartcard/README.html">remote smart card reader driver (VPCD)</a> of the host computer.</string>
|
||||
|
||||
<string name="status_error">Error</string>
|
||||
<string name="status_disconnected">Disconnected</string>
|
||||
<string name="status_connected_to">Connected to</string>
|
||||
<string name="status_atr">ATR</string>
|
||||
<string name="status_capdu">C-APDU</string>
|
||||
<string name="status_rapdu">R-APDU</string>
|
||||
<string name="status_on">On</string>
|
||||
<string name="status_off">Off</string>
|
||||
<string name="status_reset">Reset</string>
|
||||
<string name="status_unknown">Unknown Event</string>
|
||||
<string name="status_tag_discovered">Contact-less card discovered.</string>
|
||||
<string name="status_dummy_start">Using a dummy card to test the connection.</string>
|
||||
|
||||
<string name="menu_copy_to_clipboard">Copy</string>
|
||||
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="verbose">Verbose logging</string>
|
||||
<string name="error_tag_lost">Data transmission failed. Is the card still present?</string>
|
||||
|
||||
</resources>
|
||||
8
remote-reader/app/src/main/res/values/styles.xml
Normal file
8
remote-reader/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
6
remote-reader/app/src/main/res/xml/nfc_tech_filter.xml
Normal file
6
remote-reader/app/src/main/res/xml/nfc_tech_filter.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<tech-list>
|
||||
<!-- catches both, NfcA and NfcB -->
|
||||
<tech>android.nfc.tech.IsoDep</tech>
|
||||
</tech-list>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user