Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 10 additions & 17 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
plugins {
alias(libs.plugins.android.application)
id 'com.android.application'
}

android {
namespace 'de.androidcrypto.android_hce_beginner_app'
compileSdk 34
compileSdk 34 // Atualizado para Android 14

defaultConfig {
applicationId "de.androidcrypto.android_hce_beginner_app"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
minSdk 26 // Android 8.0 (compatível com maioria)
targetSdk 34 // Atualizado para Android 14 (Obrigatório)
versionCode 2
versionName "2.0"
}

buildTypes {
Expand All @@ -29,12 +27,7 @@ android {
}

dependencies {

implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
Original file line number Diff line number Diff line change
@@ -1,69 +1,53 @@
package de.androidcrypto.android_hce_beginner_app;

import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.nfc.NfcAdapter;
import android.nfc.cardemulation.CardEmulation;
import android.os.Bundle;

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 androidx.fragment.app.Fragment;

import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.navigation.NavigationBarView;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

EditText etTrackData;
Button btnGravar;
TextView tvStatus;

@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;
});

*/

BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
//bottomNav.setOnNavigationItemSelectedListener(navListener);
bottomNav.setOnItemSelectedListener(navListener);

// as soon as the application opens the first
// fragment should be shown to the user
// in this case it is algorithm fragment
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new HomeFragment()).commit();
// Busca os elementos do layout (que vamos criar no próximo passo)
etTrackData = findViewById(R.id.etTrackData);
btnGravar = findViewById(R.id.btnGravar);
tvStatus = findViewById(R.id.tvStatus);

// 1. Recuperar track salva anteriormente
SharedPreferences prefs = getSharedPreferences("NfcData", MODE_PRIVATE);
// Valor padrão de exemplo
String savedTrack = prefs.getString("TRACK_DATA", "00A4040007F00102030405069000");
etTrackData.setText(savedTrack);

// 2. Configura o botão de Gravar
btnGravar.setOnClickListener(view -> {
String newData = etTrackData.getText().toString().trim();
if (newData.isEmpty()) {
Toast.makeText(this, "A track não pode estar vazia!", Toast.LENGTH_SHORT).show();
return;
}

private final NavigationBarView.OnItemSelectedListener navListener = item -> {
// By using switch we can easily get
// the selected fragment
// by using there id.
Fragment selectedFragment = null;
int itemId = item.getItemId();
if (itemId == R.id.home) {
selectedFragment = new HomeFragment();
} else if (itemId == R.id.read) {
selectedFragment = new ReadFragment();
} else if (itemId == R.id.write) {
selectedFragment = new WriteFragment();
}

// It will help to replace the
// one fragment to other.
if (selectedFragment != null) {
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, selectedFragment).commit();
}
return true;
};
// Salva na memória permanente (SharedPreferences)
SharedPreferences.Editor editor = getSharedPreferences("NfcData", MODE_PRIVATE).edit();
editor.putString("TRACK_DATA", newData);
editor.apply();


}
tvStatus.setText("Track Gravada com Sucesso!\nPronto para aproximar da maquininha.");
Toast.makeText(this, "Dados NFC atualizados!", Toast.LENGTH_SHORT).show();
});

// Exibe o status inicial
tvStatus.setText("Última Track carregada. Clique em Gravar para confirmar.");
}
}
Original file line number Diff line number Diff line change
@@ -1,190 +1,67 @@
package de.androidcrypto.android_hce_beginner_app;/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.androidcrypto.android_hce_beginner_app;

import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.content.SharedPreferences;
import android.util.Log;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
// Você precisará de uma classe Utils.java para a conversão de Hex, se ela não existir
// Por enquanto, vamos assumir que as conversões básicas estão em um arquivo auxiliar.

public class MyHostApduServiceSimple extends HostApduService {
private static final String TAG = "HceBeginnerApp";
// ISO-DEP command HEADER for selecting an AID.
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
private static final byte[] SELECT_APPLICATION_APDU = hexStringToByteArray("00a4040006f2233445566700");
private static final String GET_DATA_APDU_HEADER = "00CA0000";
private static final String PUT_DATA_APDU_HEADER = "00DA0000";
// "OK" status word sent in response to SELECT AID command (0x9000)
private static final byte[] SELECT_OK_SW = hexStringToByteArray("9000");
// "UNKNOWN" status word sent in response to invalid APDU command (0x0000)
private static final byte[] UNKNOWN_CMD_SW = hexStringToByteArray("0000");
public class MyHostApduService extends HostApduService {

private byte[] fileContent01 = "HCE Beginner App 1".getBytes(StandardCharsets.UTF_8);
private byte[] fileContent02 = "HCE Beginner App 2".getBytes(StandardCharsets.UTF_8);
private byte[] fileContentUnknown = "HCE Beginner App Unknown".getBytes(StandardCharsets.UTF_8);

/**
* Called if the connection to the NFC card is lost, in order to let the application know the
* cause for the disconnection (either a lost link, or another AID being selected by the
* reader).
*
* @param reason Either DEACTIVATION_LINK_LOSS or DEACTIVATION_DESELECTED
*/
@Override
public void onDeactivated(int reason) {
// Função de conversão de Hex para Byte Array (Se o projeto original não tiver)
// Se o Android Studio reclamar de Utils, crie um arquivo chamado Utils.java e coloque essas funções lá.
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}

/**
* This method will be called when a command APDU has been received from a remote device. A
* response APDU can be provided directly by returning a byte-array in this method. In general
* response APDUs must be sent as quickly as possible, given the fact that the user is likely
* holding his device over an NFC reader when this method is called.
*
* <p class="note">If there are multiple services that have registered for the same AIDs in
* their meta-data entry, you will only get called if the user has explicitly selected your
* service, either as a default or just for the next tap.
*
* <p class="note">This method is running on the main thread of your application. If you
* cannot return a response APDU immediately, return null and use the {@link
* #sendResponseApdu(byte[])} method later.
*
* @param commandApdu The APDU that received from the remote device
* @param extras A bundle containing extra data. May be null.
* @return a byte-array containing the response APDU, or null if no response APDU can be sent
* at this point.
*/

@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
// The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow"
// in the NFC Forum specification
Log.i(TAG, "Received APDU: " + byteArrayToHexString(commandApdu));

// First command: Application select (Section 5.5.2 in NFC Forum spec)
if (Arrays.equals(SELECT_APPLICATION_APDU, commandApdu)) {
Log.i(TAG, "This is: 01 SELECT_APPLICATION_APDU");
return SELECT_OK_SW;
if (commandApdu == null) {
return hexStringToByteArray("6F00"); // Erro genérico
}

// Second command: GetData command, here returning any data and not from file01
} else if (arraysStartWith(commandApdu, hexStringToByteArray(GET_DATA_APDU_HEADER))) {
Log.i(TAG, "This is: 02 GET_DATA_APDU");
// Simulação de SELECT AID (APDU inicial)
// Se a maquininha mandar um SELECT AID (00A40400...), respondemos com a track gravada
String hexCommand = bytesToHex(commandApdu);
Log.d("HCE_LOG", "Recebido da Maquininha: " + hexCommand);

// get the file number from commandApdu
int fileNumber;
byte[] fileContent;
if (commandApdu.length == 7) {
fileNumber = (int) commandApdu[5];
if (fileNumber == 1) {
fileContent = fileContent01.clone();
} else if (fileNumber == 2) {
fileContent = fileContent02.clone();
} else {
fileContent = fileContentUnknown.clone();
}
} else {
fileContent = "Unknown Request".getBytes(StandardCharsets.UTF_8);
}
byte[] response = new byte[fileContent.length + SELECT_OK_SW.length];
System.arraycopy(fileContent, 0, response, 0, fileContent.length);
System.arraycopy(SELECT_OK_SW, 0, response, fileContent.length, SELECT_OK_SW.length);
Log.i(TAG, "GET_DATA_APDU Our Response: " + byteArrayToHexString(response));
return response;
// write data to the emulated tag
} else if (arraysStartWith(commandApdu, hexStringToByteArray(PUT_DATA_APDU_HEADER))) {
Log.i(TAG, "This is: 03 PUT_DATA_APDU");
// get the data length and file number from commandApdu
int dataLength;
int fileNumber;
byte[] fileContent;
if (commandApdu.length >= 7) {
dataLength = (int) commandApdu[4];
fileNumber = (int) commandApdu[5];
// System.out.println("commandApdu l: " + commandApdu.length + " data l: " + dataLength + " fnr: " + fileNumber);
// get the data
if (commandApdu.length != (6 + dataLength)) {
return UNKNOWN_CMD_SW;
}
fileContent = Arrays.copyOfRange(commandApdu, 6, (5 + dataLength));
Log.i(TAG, "fileNr: " + fileNumber + " content: " + new String(fileContent, StandardCharsets.UTF_8));
if (fileNumber == 1) {
fileContent01 = fileContent.clone();
} else if (fileNumber == 2) {
fileContent02 = fileContent.clone();
} else {
fileContentUnknown = fileContent.clone();
}
} else {
fileContent = "Unknown Request".getBytes(StandardCharsets.UTF_8);
}
return SELECT_OK_SW;
// O comando de seleção geralmente começa com 00A40400
if (hexCommand.startsWith("00A40400")) {
// Aqui pegamos a TRACK que você gravou na tela principal
SharedPreferences prefs = getSharedPreferences("NfcData", MODE_PRIVATE);
// Valor padrão: 9000 (Status OK). Se não tiver track gravada, o app continua funcionando.
String trackData = prefs.getString("TRACK_DATA", "9000");

Log.d("HCE_LOG", "Respondendo com Track gravada: " + trackData);
return hexStringToByteArray(trackData);
}

// We're doing something outside our scope
} else
Log.wtf(TAG, "processCommandApdu() | I don't know what's going on!!!.");
//return "Can I help you?".getBytes();
return UNKNOWN_CMD_SW;
// Resposta padrão para outros comandos (Status OK)
return hexStringToByteArray("9000");
}

boolean arraysStartWith(byte[] completeArray, byte[] compareArray) {
int n = compareArray.length;
// Log.d(TAG, "completeArray length: " + completeArray.length + " data: " + ByteArrayToHexString(completeArray));
// Log.d(TAG, "compareArray length: " + compareArray.length + " data: " + ByteArrayToHexString(compareArray));
return ByteBuffer.wrap(completeArray, 0, n).equals(ByteBuffer.wrap(compareArray, 0, n));
@Override
public void onDeactivated(int reason) {
Log.d("HCE_LOG", "Conexão terminada. Razão: " + reason);
}

/**
* Utility method to convert a byte array to a hexadecimal string.
*
* @param bytes Bytes to convert
* @return String, containing hexadecimal representation.
*/
public static String byteArrayToHexString(byte[] bytes) {
final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex characters (nibbles)
int v;
// Função de conversão de Byte Array para Hex String (Para Log)
private static String bytesToHex(byte[] bytes) {
final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned value
hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from upper nibble
hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character from lower nibble
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}

/**
* Utility method to convert a hexadecimal string to a byte string.
*
* <p>Behavior with input strings containing non-hexadecimal characters is undefined.
*
* @param s String containing hexadecimal characters to convert
* @return Byte array generated from input
* @throws IllegalArgumentException if input length is incorrect
*/
public static byte[] hexStringToByteArray(String s) throws IllegalArgumentException {
int len = s.length();
if (len % 2 == 1) {
throw new IllegalArgumentException("Hex string must have even number of characters");
}
byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
for (int i = 0; i < len; i += 2) {
// Convert each character into a integer (base-16), then bit-shift into place
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
}
Loading