So the final step in this application is to show the receipt to the user.
We are going to do this in a very lame way to keep it simple because by now I am very bored of this and just trying to connect the dots and make it clear that we actually have a real application.
The first step is to create a new activity, ReceiptActivity, using the Android Studio wizard. We can then couple this to our notification using a PendingIntent as follows:
private void showNotification(long notificationId) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Intent intent = new Intent(context, ReceiptActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(context, ReceiptApplication.CHANNEL_ID)
.setSmallIcon(R.drawable.credit_card)
.setContentTitle("NFC")
.setContentText("NFC Contact Made")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
notificationManager.notify((int)notificationId, notificationBuilder.build());
}
ANDROID_BASIC_RECEIPT_ACTIVITY:android_hce/app/src/main/java/com/example/android_hce/ReceiptSessionController.java
Having allowed Android Studio to create my activity for me, I promptly regretted that because of all the gobbledegook it generated and basically replaced it all by cutting and pasting large amounts of the contents of MainActivity which at least seemed sane.package com.example.android_hce;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.example.android_hce.databinding.ActivityReceiptBinding;
public class ReceiptActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_receipt);
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;
});
}
}
ANDROID_BASIC_RECEIPT_ACTIVITY:android_hce/app/src/main/java/com/example/android_hce/ReceiptActivity.java
Along with an XML file that describes the window layout:<?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=".ReceiptActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Receipt Goes Here"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
ANDROID_BASIC_RECEIPT_ACTIVITY:android_hce/app/src/main/res/layout/activity_receipt.xml
And when we run the app again and tap on the reader, we receive a notification that opens an activity up on the screen that clearly says that the receipt should appear.Generating the Receipt
We have a text window up on the screen, so I think all we need to do is to set the text element of that to an appropriate set of text. What I'm going to do here is obviously lame but it's simple. (The correct solution would involve a list of rows and you'd be able to click on things, etc but this is not a real application - I just wanted to deal with NFC.)So we need to create a database helper as we did on the back end.
public ReceiptActivity() {
dbHelper = new ReceiptDatabaseHelper(this);
}
ANDROID_MINIMAL_RECEIPT:android_hce/app/src/main/java/com/example/android_hce/ReceiptActivity.java
We need to extract the notification id from the Intent.protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
rowId = extras.getLong("row_id");
Log.w("ReceiptActivity", "want to show receipt " + rowId);
ANDROID_MINIMAL_RECEIPT:android_hce/app/src/main/java/com/example/android_hce/ReceiptActivity.java
Which reminds me we didn't actually put the notification id in the receipt when we created the intent, so let's go back and do that:private void showNotification(long notificationId) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Intent intent = new Intent(context, ReceiptActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra("row_id", notificationId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(context, ReceiptApplication.CHANNEL_ID)
.setSmallIcon(R.drawable.credit_card)
.setContentTitle("NFC")
.setContentText("NFC Contact Made")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
notificationManager.notify((int)notificationId, notificationBuilder.build());
}
ANDROID_MINIMAL_RECEIPT:android_hce/app/src/main/java/com/example/android_hce/ReceiptSessionController.java
And then we can generate the receipt text based on SQLite queries and a StringBuffer and shove it into the text view. All of this happens in onResume() because I think that's the most appropriate place for it.protected void onResume() {
super.onResume();
SQLiteDatabase db = dbHelper.getReadableDatabase();
StringBuilder receiptText = new StringBuilder();
try (Cursor c = db.query(ReceiptDatabaseContract.ReceiptEntry.TABLE_NAME, ReceiptDatabaseContract.ReceiptEntry.ALL_COLUMNS, ReceiptDatabaseContract.ReceiptEntry._ID + " = ?", new String[] { Long.toString(rowId) }, null, null, null)) {
if (!c.moveToFirst()) {
Log.e("ReceiptActivity", "there is no row " + rowId);
return;
}
Log.w("ReceiptActivity", "have receipt with row " + rowId);
String merchant = c.getString(0);
int total = c.getInt(1);
receiptText.append("Receipt " + rowId + " for " + merchant + " amount = " + total);
}
TextView tv = findViewById(R.id.receipt_text);
tv.setText(receiptText.toString());
}
}
ANDROID_MINIMAL_RECEIPT:android_hce/app/src/main/java/com/example/android_hce/ReceiptActivity.java
Having checked that we can all the plumbing works, we can work through all the hairy details of querying the database and joining things up and we'll end up with this:protected void onResume() {
super.onResume();
SQLiteDatabase db = dbHelper.getReadableDatabase();
StringBuilder receiptText = new StringBuilder();
try (Cursor c = db.query(ReceiptDatabaseContract.ReceiptEntry.TABLE_NAME, ReceiptDatabaseContract.ReceiptEntry.ALL_COLUMNS, ReceiptDatabaseContract.ReceiptEntry._ID + " = ?", new String[] { Long.toString(rowId) }, null, null, null)) {
if (!c.moveToFirst()) {
Log.e("ReceiptActivity", "there is no row " + rowId);
return;
}
Log.w("ReceiptActivity", "have receipt with row " + rowId);
String merchant = c.getString(0);
int total = c.getInt(1);
receiptText.append(String.format("Receipt %d for %s, total = %s\n", rowId, merchant, pounds(total)));
try (Cursor lines = db.query(ReceiptDatabaseContract.ReceiptLineEntry.TABLE_NAME, ReceiptDatabaseContract.ReceiptLineEntry.ALL_COLUMNS, ReceiptDatabaseContract.ReceiptLineEntry.COLUMN_NAME_RECEIPT + " = ?", new String[] { Long.toString(rowId) }, null, null, ReceiptDatabaseContract.ReceiptLineEntry.COLUMN_NAME_TYPE + ", " +ReceiptDatabaseContract.ReceiptLineEntry.COLUMN_NAME_INDEX)) {
if (!lines.moveToFirst()) {
Log.e("ReceiptActivity", "there is no row " + rowId);
return;
}
while (true) {
long lineId = lines.getLong(0);
int ty = lines.getInt(1);
switch (ty) {
case 0x11:
receiptText.append(lines.getString(6) + "\n"); // text_
break;
case 0x12: {
String title = lines.getString(7);
String value = lines.getString(8);
receiptText.append(String.format("%-12s %s\n", title + ":", value));
break;
}
case 0x21: {
String desc = lines.getString(4);
int price = lines.getInt(5);
receiptText.append(String.format("%-20s %s\n", desc + ":", pounds(price)));
break;
}
case 0x31:
case 0x32:
case 0x33:
case 0x3f: {
String text = lines.getString(6);
int amount = lines.getInt(3);
receiptText.append(String.format("%-20s %s\n", text + ":", pounds(amount)));
break;
}
case 0x41: {
String text = lines.getString(6);
int amount = lines.getInt(3);
receiptText.append(String.format("%-20s %s\n", "Paid by " + text + ":", pounds(amount)));
break;
}
case 0x51:
receiptText.append(lines.getString(6) + "\n"); // text_
break;
default:
Log.e("ReceiptActivity", "did not expect type " + ty);
break;
}
try (Cursor comments = db.query(ReceiptDatabaseContract.ReceiptLineCommentEntry.TABLE_NAME, ReceiptDatabaseContract.ReceiptLineCommentEntry.ALL_COLUMNS, ReceiptDatabaseContract.ReceiptLineCommentEntry.COLUMN_NAME_ENTRY + " = ?", new String[] { Long.toString(lineId) }, null, null, ReceiptDatabaseContract.ReceiptLineCommentEntry.COLUMN_NAME_INDEX)) {
if (comments.moveToFirst()) {
while (true) {
int cty = comments.getInt(0);
switch (cty) {
case 0x22: {
int quant = comments.getInt(4);
int unit = comments.getInt(5);
receiptText.append(String.format(" %d @ %s\n", quant, pounds(unit)));
break;
}
case 0x23: {
int disc = comments.getInt(2);
String expl = comments.getString(3);
receiptText.append(String.format(" %s: %s\n", expl, pounds(disc)));
break; }
default:
Log.e("ReceiptActivity", "did not expect comment type " + cty);
break;
}
if (!comments.moveToNext())
break;
}
}
}
if (!lines.moveToNext())
break;
}
}
}
TextView tv = findViewById(R.id.receipt_text);
tv.setText(receiptText.toString());
}
public String pounds(int total) {
String pounds = String.format("£%d", total/100);
return String.format("%6s.%02d", pounds, total%100);
}
}
ANDROID_FINAL_RECEIPT:android_hce/app/src/main/java/com/example/android_hce/ReceiptActivity.java
And we are done.Conclusion
We have clearly shown that it is possible to transmit data from a terminal with a Go application to an Android phone in such a way that it can be maintained there by an arbitrary app. The app itself (on both sides) is severely lacking in quality and features, but this is not a blog about that, it's about experimentation.Having said that, I would be happy for anyone who wants to take this code and work on it to make a more compelling demonstration - or indeed use it as the basis of a real application - to do so and to send back pull requests as appropriate.
I also feel that the structure of this project makes it a suitable "ground zero" for anyone who wants to run some kind of refactoring/code quality/code simplification class or indeed a practical interview. Clearly everything here is a mess and there is duplication up the wazoo. The use of two different languages just makes things worse. I would be interested to see what simplifications and refactorings people would come up with.
No comments:
Post a Comment