/* (C) 2024 by sysmocom s.f.m.c. GmbH
* All Rights Reserved
*
* Author: Philipp Maier
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License
* along with this program. If not, see .
*
*/
package org.osmocom.androidApduProxy;
import androidx.appcompat.app.AppCompatActivity;
import android.app.AlertDialog;
import android.content.SharedPreferences;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.RadioButton;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.List;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private Omapi omapi;
SharedPreferences pref;
//Message types that can be passed between the UI thread and OmapiCallbackHandlers
public static final int IND_ATR_TRAFFIC = 1;
public static final int IND_APDU_TRAFFIC = 2;
public static final int IND_CHANNEL_OPEN = 3;
public static final int IND_CHANNEL_CLOSE = 4;
public static final int IND_ERROR = 5;
public static final int IND_SHUTDOWN = 6;
public static final int IND_SLOT_SCAN_RESULT = 7;
//Helper method to display a message box on the screen
public void msgBox(String prompt, String title){
Log.i("MAIN","message box: " + title + ": " + prompt + "\n");
AlertDialog.Builder alertDlgBuilder = new AlertDialog.Builder(this);
alertDlgBuilder.setMessage(prompt);
alertDlgBuilder.setTitle(title);
AlertDialog alertDlg = alertDlgBuilder.create();
alertDlg.show();
}
//Reset the indicator lights on the UI interface
private void resetIndicatorLights()
{
final RadioButton indAtrTraffic=findViewById(R.id.indAtrTraffic);
final RadioButton indApduTraffic=findViewById(R.id.indApduTraffic);
final RadioButton indChannel=findViewById(R.id.indChannel);
indAtrTraffic.setChecked(false);
indApduTraffic.setChecked(false);
indChannel.setChecked(false);
}
//Lock UI elements depending on the connected/disconnected state
private void setUiConnected(boolean connected)
{
final Spinner spnSlot = findViewById(R.id.spnSlot);
final TextView txtRemoteHost = findViewById(R.id.txtRemoteHost);
final TextView txtRemotePort = findViewById(R.id.txtRemotePort);
final Button btnConnect = findViewById(R.id.btnConnect);
final Button btnDisconnect = findViewById(R.id.btnDisconnect);
spnSlot.setEnabled(!connected);
txtRemoteHost.setEnabled(!connected);
txtRemotePort.setEnabled(!connected);
btnConnect.setEnabled(!connected);
btnDisconnect.setEnabled(connected);
}
Handler uiHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message inputMessage) {
final RadioButton indAtrTraffic=findViewById(R.id.indAtrTraffic);
final RadioButton indApduTraffic=findViewById(R.id.indApduTraffic);
final RadioButton indChannel=findViewById(R.id.indChannel);
final Spinner spnSlot = findViewById(R.id.spnSlot);
switch(inputMessage.what) {
case IND_ATR_TRAFFIC:
indAtrTraffic.setChecked(!indAtrTraffic.isChecked());
break;
case IND_APDU_TRAFFIC:
indApduTraffic.setChecked(!indApduTraffic.isChecked());
break;
case IND_CHANNEL_OPEN:
indChannel.setChecked(true);
break;
case IND_CHANNEL_CLOSE:
indChannel.setChecked(false);
indApduTraffic.setChecked(false);
break;
case IND_ERROR:
msgBox(inputMessage.obj.toString(),"Error");
break;
case IND_SHUTDOWN:
resetIndicatorLights();
setUiConnected(false);
break;
case IND_SLOT_SCAN_RESULT:
ArrayAdapter dataAdapter = new ArrayAdapter(getBaseContext(),
android.R.layout.simple_spinner_item, (List)inputMessage.obj);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spnSlot.setAdapter(dataAdapter);
omapi.shutdown();
break;
}
}
};
private String getInfoString() {
PackageManager packageManager = this.getPackageManager();
PackageInfo packageInfo = null;
String version;
try {
packageInfo = packageManager.getPackageInfo(getBaseContext().getPackageName(), 0);
version = packageInfo.versionName;
} catch (Exception e) {
version = "(unknown)";
}
return getString(R.string.app_name) + " version " + version + "\n" +
"(C) 2024 by sysmocom - s.f.m.c. GmbH\n" +
"running on Android " + Build.VERSION.RELEASE + " (API level " + Build.VERSION.SDK_INT + ")";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final TextView txtRemoteHost=findViewById(R.id.txtRemoteHost);
final TextView txtRemotePort=findViewById(R.id.txtRemotePort);
final TextView lblInfo=findViewById(R.id.lblInfo);
PackageManager packageManager = this.getPackageManager();
Log.i("MAIN", "Welcome to " + getInfoString() + "\n");
lblInfo.setText(getInfoString());
//Check if this device even supports APDU access to an UICC/eUICC card, if not we display
//an error message and continue normally, maybe we are lucky and it works anyway.
//(This check only works on Android 11 / API-level 30 and higher,
//see also https://developer.android.com/develop/connectivity/telecom/dialer-app/telephony-ids)
if (Build.VERSION.SDK_INT >= 30 &&
!packageManager.hasSystemFeature(PackageManager.FEATURE_SE_OMAPI_UICC)) {
msgBox("Feature FEATURE_SE_OMAPI_UICC is not available on this device, APDU access to UICC/eUICC not possible!",
"Error");
}
//Scan for usable card slots
omapi = new Omapi(this, new OmapiCallbackHandlerScan(uiHandler));
//Populate remote host settings
pref = getSharedPreferences("pref", Context.MODE_PRIVATE);
txtRemoteHost.setText(pref.getString("REMOTE_HOST", "192.168.1.156"));
pref = getSharedPreferences("pref", Context.MODE_PRIVATE);
txtRemotePort.setText(pref.getString("REMOTE_PORT", "8000"));
setUiConnected(false);
}
public void onClickBtnConnect(View view)
{
Log.i("MAIN","user has pressed connect button");
final Spinner spnSlot = findViewById(R.id.spnSlot);
final TextView txtRemoteHost = findViewById(R.id.txtRemoteHost);
final TextView txtRemotePort = findViewById(R.id.txtRemotePort);
setUiConnected(true);
resetIndicatorLights();
//Start APDU proxy
if (spnSlot.getSelectedItem() == null) {
msgBox("no UICC/eUICC card selected!", "Error");
return;
}
String omapiReader = spnSlot.getSelectedItem().toString();
String remoteHost = txtRemoteHost.getText().toString();
Integer remotePort = Integer.valueOf((txtRemotePort.getText().toString()));
omapi = new Omapi(this, new OmapiCallbackHandlerVpcd(uiHandler, omapiReader, remoteHost, remotePort));
}
public void onClickBtnDisconnect(View view)
{
Log.i("MAIN","user has pressed disconnect button");
omapi.shutdown();
setUiConnected(false);
resetIndicatorLights();
}
public void onClickBtnExit(View view)
{
Log.i("MAIN","user has pressed exit button");
omapi.shutdown();
this.finishAffinity();
}
@Override
protected void onDestroy() {
super.onDestroy();
final TextView txtRemoteHost = findViewById(R.id.txtRemoteHost);
final TextView txtRemotePort = findViewById(R.id.txtRemotePort);
//Save remote host settings
SharedPreferences.Editor editor = pref.edit();
editor.putString("REMOTE_HOST", txtRemoteHost.getText().toString());
editor.putString("REMOTE_PORT", txtRemotePort.getText().toString());
editor.commit();
}
}