Android Open Source - irma_android_cardproxy Main Activity






From Project

Back to project page irma_android_cardproxy.

License

The source code is released under:

GNU General Public License

If you think the Android project irma_android_cardproxy listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package org.irmacard.androidcardproxy;
//w  w  w . j a v  a 2s  . c  o  m
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.Timer;
import java.util.TimerTask;

import net.sourceforge.scuba.smartcards.CardServiceException;
import net.sourceforge.scuba.smartcards.IsoDepCardService;
import net.sourceforge.scuba.smartcards.ProtocolCommand;
import net.sourceforge.scuba.smartcards.ProtocolResponse;
import net.sourceforge.scuba.smartcards.ProtocolResponses;
import net.sourceforge.scuba.smartcards.ResponseAPDU;
import org.apache.http.entity.StringEntity;
import org.irmacard.android.util.pindialog.EnterPINDialogFragment;
import org.irmacard.android.util.pindialog.EnterPINDialogFragment.PINDialogListener;
import org.irmacard.androidcardproxy.messages.EventArguments;
import org.irmacard.androidcardproxy.messages.PinResultArguments;
import org.irmacard.androidcardproxy.messages.ReaderMessage;
import org.irmacard.androidcardproxy.messages.ReaderMessageDeserializer;
import org.irmacard.androidcardproxy.messages.ResponseArguments;
import org.irmacard.androidcardproxy.messages.TransmitCommandSetArguments;
import org.irmacard.idemix.IdemixService;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.AsyncHttpResponseHandler;


public class MainActivity extends Activity implements PINDialogListener {
  private String TAG = "CardProxyMainActivity";
  private NfcAdapter nfcA;
  private PendingIntent mPendingIntent;
  private IntentFilter[] mFilters;
  private String[][] mTechLists;

  // PIN handling
  private int tries = -1;
  
  // State variables
  private IsoDep lastTag = null;
  
  private int activityState = STATE_IDLE;
  
  // New states
  private static final int STATE_IDLE = 1;
  private static final int STATE_CONNECTING_TO_SERVER = 2;
  private static final int STATE_CONNECTED = 3;
  private static final int STATE_READY = 4;
  private static final int STATE_COMMUNICATING = 5;
  private static final int STATE_WAITING_FOR_PIN = 6;

  // Timer for testing card connectivity
  Timer timer;
  private static final int CARD_POLL_DELAY = 2000;
  
  // Timer for briefly displaying feedback messages on CardProxy
  CountDownTimer cdt;
  private static final int FEEDBACK_SHOW_DELAY = 10000;
  private boolean showingFeedback = false;

  // Counter for number of connection tries
  private static final int MAX_RETRIES = 3;
  private int retry_counter = 0;

  private void setState(int state) {
      Log.i(TAG,"Set state: " + state);
      activityState = state;

      switch (activityState) {
    case STATE_IDLE:
      lastTag = null;
      break;
    default:
      break;
      }

      setUIForState();
  }

    private void setUIForState() {
      int imageResource = 0;
      int statusTextResource = 0;
      int feedbackTextResource = 0;

      switch (activityState) {
    case STATE_IDLE:
      imageResource = R.drawable.irma_icon_place_card_520px;
      statusTextResource = R.string.status_idle;
      break;
    case STATE_CONNECTING_TO_SERVER:
      imageResource = R.drawable.irma_icon_place_card_520px;
      statusTextResource = R.string.status_connecting;
      break;
    case STATE_CONNECTED:
      imageResource = R.drawable.irma_icon_place_card_520px;
      statusTextResource = R.string.status_connected;
      feedbackTextResource = R.string.feedback_waiting_for_card;
      break;
    case STATE_READY:
      imageResource = R.drawable.irma_icon_card_found_520px;
      statusTextResource = R.string.status_ready;
      break;
    case STATE_COMMUNICATING:
      imageResource = R.drawable.irma_icon_card_found_520px;
      statusTextResource = R.string.status_communicating;
      break;
    case STATE_WAITING_FOR_PIN:
      imageResource = R.drawable.irma_icon_card_found_520px;
      statusTextResource = R.string.status_waitingforpin;
      break;
    default:
      break;
    }
      
      ((TextView)findViewById(R.id.status_text)).setText(statusTextResource);
      if(!showingFeedback)
        ((ImageView)findViewById(R.id.statusimage)).setImageResource(imageResource);

    if(feedbackTextResource != 0)
      ((TextView)findViewById(R.id.status_text)).setText(feedbackTextResource);
  }
  
  private void setFeedback(String message, String state) {
      int imageResource = 0;

      setUIForState();

    if (state.equals("success")) {
      imageResource = R.drawable.irma_icon_ok_520px;
    } if (state.equals("warning")) {
      imageResource = R.drawable.irma_icon_warning_520px;
    } if (state.equals("failure")) {
      imageResource = R.drawable.irma_icon_missing_520px;
    }

      ((TextView)findViewById(R.id.feedback_text)).setText(message);

      if(imageResource != 0) {
        ((ImageView)findViewById(R.id.statusimage)).setImageResource(imageResource);
        showingFeedback = true;
      }

    if(cdt != null)
      cdt.cancel();

    cdt = new CountDownTimer(FEEDBACK_SHOW_DELAY, 1000) {
      public void onTick(long millisUntilFinished) {
      }

      public void onFinish() {
        clearFeedback();
      }
    }.start();
  }

  private void clearFeedback() {
    showingFeedback = false;
    ((TextView)findViewById(R.id.feedback_text)).setText("");
    setUIForState();
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
        // NFC stuff
        nfcA = NfcAdapter.getDefaultAdapter(getApplicationContext());
        mPendingIntent = PendingIntent.getActivity(this, 0,
                new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);

        // Setup an intent filter for all TECH based dispatches
        IntentFilter tech = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
        mFilters = new IntentFilter[] { tech };

        // Setup a tech list for all IsoDep cards
        mTechLists = new String[][] { new String[] { IsoDep.class.getName() } };

      setState(STATE_IDLE);

      timer = new Timer();
      timer.scheduleAtFixedRate(new CardPollerTask(), CARD_POLL_DELAY, CARD_POLL_DELAY);
  }


  @Override
  protected void onPause() {
    super.onPause();
      if (nfcA != null) {
        nfcA.disableForegroundDispatch(this);
      }
  }
  
  @Override
  protected void onResume() {
        super.onResume();
        Log.i(TAG, "Action: " + getIntent().getAction());
        if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(getIntent().getAction())) {
            processIntent(getIntent());
        } else if (Intent.ACTION_VIEW.equals(getIntent().getAction()) && "cardproxy".equals(getIntent().getScheme())) {
          // TODO: this is legacy code to have the cardproxy app respond to cardproxy:// urls. This doesn't
          // work anymore, should check whether we want te re-enable it.
          Uri uri = getIntent().getData();
          String startURL = "http://" + uri.getHost() + ":" + uri.getPort() + uri.getPath();
          gotoConnectingState(startURL);
        }
        if (nfcA != null) {
          nfcA.enableForegroundDispatch(this, mPendingIntent, mFilters, mTechLists);
        }
  }
  
  @Override
  protected void onDestroy() {
    super.onDestroy();
  }
  private static final int MESSAGE_STARTGET = 1;
  String currentReaderURL = "";
  int currentHandlers = 0;
  
  Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
      case MESSAGE_STARTGET:
        Log.i(TAG,"MESSAGE_STARTGET received in handler!");
        AsyncHttpClient client = new AsyncHttpClient();
        client.setTimeout(50000); // timeout of 50 seconds
        client.setUserAgent("org.irmacard.androidcardproxy");
        
        client.get(MainActivity.this, currentReaderURL, new AsyncHttpResponseHandler() {
          @Override
          public void onSuccess(int arg0, String responseData) {
            if (!responseData.equals("")) {
              //Toast.makeText(MainActivity.this, responseData, Toast.LENGTH_SHORT).show();
              handleChannelData(responseData);
            }
            
            // Do a new request, but only if no new requests have started
            // in the mean time
            if (currentHandlers <= 1) {
              Message newMsg = new Message();
              newMsg.what = MESSAGE_STARTGET;
              if(!(activityState == STATE_IDLE))
                  handler.sendMessageDelayed(newMsg, 200);
            }
          }
          @Override
          public void onFailure(Throwable arg0, String arg1) {
            if(activityState != STATE_CONNECTING_TO_SERVER) {
              retry_counter = 0;
              return;
            }

            retry_counter += 1;

            // We should try again, but only if no new requests have started
            // and we should wait a bit longer
            if (currentHandlers <= 1 && retry_counter < MAX_RETRIES) {
              Message newMsg = new Message();
              setFeedback("Trying to reach server again...", "none");
              newMsg.what = MESSAGE_STARTGET;
              handler.sendMessageDelayed(newMsg, 5000);
            } else {
              retry_counter = 0;
              setFeedback("Failed to connect to server", "warning");
              setState(STATE_IDLE);
            }
            
          }
          public void onStart() {
            currentHandlers += 1;
          };
          public void onFinish() {
            currentHandlers -= 1;
          };
        });
      
        break;

      default:
        break;
      }
    }
  };

  private String currentWriteURL = null;
  private ReaderMessage lastReaderMessage = null;
  
  private void handleChannelData(String data) {
    Gson gson = new GsonBuilder().
        registerTypeAdapter(ProtocolCommand.class, new ProtocolCommandDeserializer()).
        registerTypeAdapter(ReaderMessage.class, new ReaderMessageDeserializer()).
        create();
    if (activityState == STATE_CONNECTING_TO_SERVER) {
      // this is the message that containts the url to write to
      JsonParser p = new JsonParser();
      String write_url = p.parse(data).getAsJsonObject().get("write_url").getAsString();
      currentWriteURL = write_url;
      setState(STATE_CONNECTED);
      // Signal to the other end that we we are ready accept commands
      postMessage(
          new ReaderMessage(ReaderMessage.TYPE_EVENT, ReaderMessage.NAME_EVENT_CARDREADERFOUND, null,
              new EventArguments().withEntry("type", "phone")));
    } else {
      ReaderMessage rm;
      try {
        Log.i(TAG, "Length (real): " + data);
        JsonReader reader = new JsonReader(new StringReader(data));
        reader.setLenient(true);
        rm = gson.fromJson(reader, ReaderMessage.class);
      } catch(Exception e) {
        e.printStackTrace();
        return;
      }
      lastReaderMessage = rm;
      if (rm.type.equals(ReaderMessage.TYPE_COMMAND)) {
        Log.i(TAG, "Got command message");

        if (activityState != STATE_READY) {
          // FIXME: Only when ready can we handle commands
          throw new RuntimeException(
              "Illegal command from server, no card currently connected");
        }

        if (rm.name.equals(ReaderMessage.NAME_COMMAND_AUTHPIN)) {
          askForPIN();
        } else {
          setState(STATE_COMMUNICATING);
          new ProcessReaderMessage().execute(new ReaderInput(lastTag, rm));
        }
      } else if (rm.type.equals(ReaderMessage.TYPE_EVENT)) {
        EventArguments ea = (EventArguments)rm.arguments;
        if (rm.name.equals(ReaderMessage.NAME_EVENT_STATUSUPDATE)) {
          String state = ea.data.get("state");
          String feedback = ea.data.get("feedback");
          if (state != null) {
            setFeedback(feedback, state);
          }
        } else if(rm.name.equals(ReaderMessage.NAME_EVENT_TIMEOUT)) {
          setState(STATE_IDLE);
        } else if(rm.name.equals(ReaderMessage.NAME_EVENT_DONE)) {
          setState(STATE_IDLE);
        }
      }
    }
  }
  
  
  private void postMessage(ReaderMessage rm) {
    if (currentWriteURL != null) {
      Gson gson = new GsonBuilder().
          registerTypeAdapter(ProtocolResponse.class, new ProtocolResponseSerializer()).
          create();
      String data = gson.toJson(rm);
      AsyncHttpClient client = new AsyncHttpClient();
      try {
        client.post(MainActivity.this, currentWriteURL, new StringEntity(data) , "application/json",  new AsyncHttpResponseHandler() {
          @Override
          public void onSuccess(int arg0, String arg1) {
            // TODO: Should there be some simple user feedback?
            super.onSuccess(arg0, arg1);
          }
          @Override
          public void onFailure(Throwable arg0, String arg1) {
            // TODO: Give proper feedback to the user that we are unable to send stuff
            super.onFailure(arg0, arg1);
          }
        });
      } catch (UnsupportedEncodingException e) {
        // Ignore, shouldn't happen ;)
        e.printStackTrace();
      }
    }
  }
  
  public void onMainTouch(View v) {
    if (activityState == STATE_IDLE) {
      lastTag = null;
      startQRScanner("Scan the QR image in the browser.");
    }
  }
  
    @Override
    public void onNewIntent(Intent intent) {
        setIntent(intent);
    }
    
    public void processIntent(Intent intent) {
        Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
      IsoDep tag = IsoDep.get(tagFromIntent);
      // Only proces tag when we're actually expecting a card.
      if (tag != null && activityState == STATE_CONNECTED) {
        setState(STATE_READY);
        postMessage(new ReaderMessage(ReaderMessage.TYPE_EVENT, ReaderMessage.NAME_EVENT_CARDFOUND, null));
        lastTag = tag;
      }      
    }

    class CardPollerTask extends TimerTask {
      /**
       * Dirty Hack. Since android doesn't produce events when an NFC card
       * is lost, we send a command to the card, and see if it still responds.
       * It is important that this command does not affect the card's state.
       * 
       * FIXME: The command we sent is IRMA dependent, which is dangerous when
       * the proxy is used with other cards/protocols.
       */
      public void run() {
      // Only in the ready state do we need to actively check for card
      // presence.
        if(activityState == STATE_READY) {
          Log.i("CardPollerTask", "Checking card presence");
        ReaderMessage rm = new ReaderMessage(
            ReaderMessage.TYPE_COMMAND,
            ReaderMessage.NAME_COMMAND_IDLE, "idle");

        new ProcessReaderMessage().execute(new ReaderInput(lastTag, rm));
      }
      }
    }
  
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    IntentResult scanResult = IntentIntegrator
        .parseActivityResult(requestCode, resultCode, data);

    // Process the results from the QR-scanning activity
    if (scanResult != null) {
      String contents = scanResult.getContents();
      if (contents != null) {
        gotoConnectingState(contents);
      }
    }
  }
  
  private void gotoConnectingState(String url) {
    Log.i(TAG, "Start channel listening: " + url);
    currentReaderURL = url;
    Message msg = new Message();
    msg.what = MESSAGE_STARTGET;
    setState(STATE_CONNECTING_TO_SERVER);
    handler.sendMessage(msg);
  }
  
  public void askForPIN() {
    setState(STATE_WAITING_FOR_PIN);
    DialogFragment newFragment = EnterPINDialogFragment.getInstance(tries);
      newFragment.show(getFragmentManager(), "pinentry");
  }
  
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.activity_main, menu);
    return true;
  }
  
  public void startQRScanner(String message) {
    IntentIntegrator integrator = new IntentIntegrator(this);
    integrator.setPrompt(message);
      integrator.initiateScan();
  }
  
  
  private class ReaderInput {
    public IsoDep tag;
    public ReaderMessage message;
    public String pincode = null;
    public ReaderInput(IsoDep tag, ReaderMessage message) {
      this.tag = tag;
      this.message = message;
    }
    
    public ReaderInput(IsoDep tag, ReaderMessage message, String pincode) {
      this.tag = tag;
      this.message = message;
      this.pincode = pincode;
    }
  }
    
  private class ProcessReaderMessage extends AsyncTask<ReaderInput, Void, ReaderMessage> {
    

    @Override
    protected ReaderMessage doInBackground(ReaderInput... params) {
      ReaderInput input = params[0];
      IsoDep tag = input.tag;
      ReaderMessage rm = input.message;

      // It seems sometimes tag is null afterall
      if(tag == null) {
        Log.e("ReaderMessage", "tag is null, this should not happen!");
        return new ReaderMessage(ReaderMessage.TYPE_EVENT, ReaderMessage.NAME_EVENT_CARDLOST, null);
      }

      // Make sure time-out is long enough (10 seconds)
      tag.setTimeout(10000);
      
      // TODO: The current version of the cardproxy shouldn't depend on idemix terminal, but for now
      // it is convenient.
      IdemixService is = new IdemixService(new IsoDepCardService(tag));
      try {
        if (!is.isOpen()) {
          // TODO: this is dangerous, this call to IdemixService already does a "select applet"
          is.open();
        }
        if (rm.name.equals(ReaderMessage.NAME_COMMAND_AUTHPIN)) {
          if (input.pincode != null) {
            // TODO: this should be done properly, maybe without using IdemixService?
            tries = is.sendCredentialPin(input.pincode.getBytes());

            return new ReaderMessage("response", rm.name, rm.id, new PinResultArguments(tries));
          }
        } else if (rm.name.equals(ReaderMessage.NAME_COMMAND_TRANSMIT)) {
          TransmitCommandSetArguments arg = (TransmitCommandSetArguments)rm.arguments;
          ProtocolResponses responses = new ProtocolResponses();
          for (ProtocolCommand c: arg.commands) {
            ResponseAPDU apdu_response = is.transmit(c.getAPDU());
            responses.put(c.getKey(), 
                new ProtocolResponse(c.getKey(), apdu_response));
            if(apdu_response.getSW() != 0x9000) {
              break;
            }
          }
          return new ReaderMessage(ReaderMessage.TYPE_RESPONSE, rm.name, rm.id, new ResponseArguments(responses));
        } else if (rm.name.equals(ReaderMessage.NAME_COMMAND_IDLE)) {
          // FIXME: IRMA specific implementation,
          // This command is not allowed in normal mode,
          // so it will result in an exception.
          Log.i("READER", "Processing idle command");
          is.getCredentials();
        }
        
      } catch (CardServiceException e) {
        // FIXME: IRMA specific handling of failed command, this is too generic.
        if(e.getMessage().contains("Command failed:") && e.getSW() == 0x6982) {
          return null;
        }
        e.printStackTrace();
        // TODO: maybe also include the information about the exception in the event?
        return new ReaderMessage(ReaderMessage.TYPE_EVENT, ReaderMessage.NAME_EVENT_CARDLOST, null);
      } catch (IllegalStateException e) {
        // This sometimes props up when applications comes out of suspend for now we just ignore this.
        Log.i("READER", "IllegalStateException ignored");
        return null;
      }
      return null;
    }
    
    @Override
    protected void onPostExecute(ReaderMessage result) {
      if(result == null)
        return;

      // Update state
      if( result.type.equals(ReaderMessage.TYPE_EVENT) &&
        result.name.equals(ReaderMessage.NAME_EVENT_CARDLOST)) {
        // Connection to the card is lost
        setState(STATE_CONNECTED);
      } else {
        if(activityState == STATE_COMMUNICATING) {
          setState(STATE_READY);
        }
      }

      if (result.name.equals(ReaderMessage.NAME_COMMAND_AUTHPIN)) {
        // Handle pin separately, abort if pin incorrect and more tries
        // left
        PinResultArguments args = (PinResultArguments) result.arguments;
        if (!args.success) {
          if (args.tries > 0) {
            // Still some tries left, asking again
            setState(STATE_WAITING_FOR_PIN);
            askForPIN();
            return; // do not send a response yet.
          } else {
            // FIXME: No more tries left
            // Need to go to error state
          }
        }
      }

      // Post result to browser
      postMessage(result);
    }
  }  

  @Override
  public void onPINEntry(String dialogPincode) {
    // TODO: in the final version, the following debug code should go :)
    Log.i(TAG, "PIN entered: " + dialogPincode);
    setState(STATE_COMMUNICATING);
    new ProcessReaderMessage().execute(new ReaderInput(lastTag, lastReaderMessage, dialogPincode));
  }

  @Override
  public void onPINCancel() {
    Log.i(TAG, "PIN entry canceled!");
    postMessage(
        new ReaderMessage(ReaderMessage.TYPE_RESPONSE, 
            ReaderMessage.NAME_COMMAND_AUTHPIN, 
            lastReaderMessage.id, 
            new ResponseArguments("cancel")));
    
    setState(STATE_READY);
  }
  
  public static class ErrorFeedbackDialogFragment extends DialogFragment {
    public static ErrorFeedbackDialogFragment newInstance(String title, String message) {
      ErrorFeedbackDialogFragment f = new ErrorFeedbackDialogFragment();
      Bundle args = new Bundle();
      args.putString("message", message);
      args.putString("title", title);
      f.setArguments(args);
      return f;
    }
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
      AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
      builder.setMessage(getArguments().getString("message"))
      .setTitle(getArguments().getString("title"))
      .setPositiveButton("OK", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
          dialog.dismiss();
        }
      });
      return builder.create();
    }
  }

  @Override
  public void onBackPressed() {
    // When we are not in IDLE state, return there
    if(activityState != STATE_IDLE) {
      if(cdt != null)
        cdt.cancel();

      setState(STATE_IDLE);
      clearFeedback();
    } else {
      // We are in Idle, do what we always do
      super.onBackPressed();
    }
  }
}




Java Source Code List

org.irmacard.androidcardproxy.ConfirmationDialogFragment.java
org.irmacard.androidcardproxy.MainActivity.java
org.irmacard.androidcardproxy.ProtocolCommandDeserializer.java
org.irmacard.androidcardproxy.ProtocolResponseSerializer.java
org.irmacard.androidcardproxy.messages.EventArguments.java
org.irmacard.androidcardproxy.messages.PinResultArguments.java
org.irmacard.androidcardproxy.messages.ReaderMessageArguments.java
org.irmacard.androidcardproxy.messages.ReaderMessageDeserializer.java
org.irmacard.androidcardproxy.messages.ReaderMessage.java
org.irmacard.androidcardproxy.messages.ResponseArguments.java
org.irmacard.androidcardproxy.messages.SelectAppletArguments.java
org.irmacard.androidcardproxy.messages.TransmitCommandSetArguments.java