/* (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(); } }