Search code examples
javaandroidandroid-alertdialognfcsmartcard

Displaying Android AlertDialog immediately


I noticed that AlertDialog in Android does not show immediately. The AlertDialog could be rendered after all the codes have fully run it's course and the dialog pops up which is too late in the UI process.

I am trying to build an NFC card reader application for Android and during the NFC communication, the card and phone will exchange cryptographic challenges to each other and the AlertDialog is suppose to display a one-time challenge to allow the end user to physically verify the authenticity of the card they are communicating with.

I noticed that the AlertDialog will only display the challenge code once all the other main codes have fully executed which defeats the purpose of a challenge code.

I would like to know if there are certain ways to force rendering of a just-in-time AlertDialog that will render immediately when asked to do so with the appropriate challenge codes to be displayed ?

Here is a skeleton code of what I am working on:

public class CardPairingActivity extends AppCompatActivity implements InfoDialogCopyDialog.InfoDialogCopyDialogListener {

    private static String username = "";
    private static String password = "";
    private static final byte role = Constants.USER_KEYMAN;
    private ImageView imageView = null;
    private ProgressBar spinner = null;
    private NfcAdapter nfcAdapter = null;
    private IntentFilter[] intentFiltersArray = null;
    private String[][] techListsArray = null;
    private PendingIntent pendingIntent = null;
    private APDUResult apduRes = null;
    private static Handler handler = new Handler();
    private static Runnable runnable = null;
    private static Context myContext = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_card_pairing);

        myContext = this;

        NfcManager nfcManager = (NfcManager) getSystemService(Context.NFC_SERVICE);
        nfcAdapter = NfcAdapter.getDefaultAdapter(this);

        if (nfcManager != null) {
            System.out.println("NFC Manager ready ...");
        }

        if (nfcAdapter != null) {
            System.out.println("NFC Adapter ready ...");
        }

        pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE);
        intentFiltersArray = new IntentFilter[]{new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)};
        techListsArray = new String[][]{new String[]{NfcA.class.getName()}, new String[]{NfcB.class.getName()}, new String[]{IsoDep.class.getName()}};

        if (android.os.Build.VERSION.SDK_INT > 9) {
            StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
            StrictMode.setThreadPolicy(policy);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        Intent intent = new Intent();
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        PendingIntent nfcPendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE);
        if (nfcAdapter != null) {
            nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);
        }
    }

    protected void onPause() {
        super.onPause();

        if (nfcAdapter != null) {
            try {
                nfcAdapter.disableForegroundDispatch(this);
            } catch (IllegalStateException ex) {
                Log.e("ATHTAG", "Error disabling NFC foreground dispatch", ex);
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        System.out.println("Doing onNewIntent() ...");

        // Use NFC to read tag
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        boolean hasIsoDep = false;

        if (tag != null) {
            System.out.println("New tag found !!!");
            System.out.println("Tag ID: " + BinUtils.toHexString(tag.getId()));
            System.out.println("\r\n");
            for (String tagTech : tag.getTechList()) {
                System.out.println("Tech: " + tagTech);
                if (tagTech.equals("android.nfc.tech.IsoDep")) {
                    hasIsoDep = true;
                }
            }
        } else {
            System.out.println("No new tag found !!!");
        }

        if (hasIsoDep) {
            try {
                IsoDep iso14443 = IsoDep.get(tag);
                iso14443.connect();
                SharedResource.setIsoDep(iso14443);
                System.out.println("Selecting KeyVault applet ...");
                HashMap<String, Object> result = API.adminInit();
                boolean isSuccess = (boolean) result.get("Success");

                if (isSuccess) {
                    String toShow = (String) result.get("Message");
                    if (toShow != null) {
                        System.out.println("Showing Secure Session popup ...");

                        runnable = new Runnable() {
                            @Override
                            public void run() {
                                AlertDialog.Builder builder = new AlertDialog.Builder(myContext);
                                builder.setTitle("Secure Card Session");
                                builder.setMessage(toShow);
                                AlertDialog alert = builder.create();
                                alert.show();
                            }
                        };
                        handler.post(runnable);
                    }
                    if (API.cardAuthenticate(Constants.USER_KEYMAN, username, password.toCharArray())) {
                        // Login success
                        System.out.println("[INF] Card Login SUCCESS ...");                        
                    } else {
                        // Login fail
                        System.out.println("[ERR] Card Login FAILED ...");
                        int tries = API.adminCardLoginTriesRemaining(role, null);
                        if (tries != -1) {
                            SharedResource.showNotificationDialog("Card Pairing Failed", "Card Login Failed. \r\nLogin Retries Left: " + tries, "OK", this, MainActivity.class);
                        } else {
                            SharedResource.showNotificationDialog("Card Pairing Failed", "Card Login Failed.", "OK", this, MainActivity.class);
                        }
                    }
                } else {
                    System.out.println("[ERR] Failed to select applet !!!");
                }
                iso14443.close();
            } catch (IOException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (InvalidSizeException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (InvalidAlgorithmParameterException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (NoSuchPaddingException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (ShortBufferException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (IllegalBlockSizeException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (NoSuchAlgorithmException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (InvalidParameterSpecException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (InvalidKeySpecException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (BadPaddingException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (InvalidKeyException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (IllegalFormatException ex) {
                Log.e("ATHTAG", "Error found during tag comms", ex);
            } catch (NoSuchProviderException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void setCredentials(String usr, String pass, String authorName, String authorID) {
        username = usr;
        password = pass;
        serverAuthorName = authorName;
        serverAuthorID = authorID;
    }

    @Override
    public void infoDialogCopyDialog() {
    }
}

What can I do to display a challenge-code issued by the card just-in-time before the user proceeds other activities ?


Solution

  • The problem is you are handling the incoming NFC data in your main thread loop (against recommendations) and then you adding the AlertDialog to end of the queue of items the main thread loop is to process with the handler.post(runnable);

    Thus all the code in the runnable get run after the rest of the code in onNewIntent

    If you want the user to confirm the "Message" before you cardAuthenticate then the flow of the code should go like the below pseudo Code

    • onNewIntent
    • Store Tag object (a simple global variable can work)
    • Connect to Tag and get message
    • Ideally Close Tag connection
    • Display dialog
    • In the confirm dialog callback re-connect to the stored Tag object
    • call cardAuthenticate

    It's not ideal to the the old Intent based method working with NFC, using the newer and better enableReaderMode automatically handles NFC in a background thread so as to prevent ANR messages due to NFC operations.

    Even with enableReaderMode you will still have to store the Tag object for later use by the confirm event from the Dialog.

    Note with Android 12L onwards you should also catch a SecurityException for it the Tag object goes out of date while you are using it.