package im.angry.openeuicc.bridge; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Set; import java.util.LinkedHashSet; import java.util.Arrays; import java.util.ArrayList; import java.util.Base64; import java.util.stream.Stream; import java.util.stream.Collectors; import java.time.Instant; import java.nio.charset.*; import java.net.URI; import java.net.URL; import java.net.URLDecoder; import java.net.HttpURLConnection; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function2; import kotlin.coroutines.Continuation; import kotlin.coroutines.EmptyCoroutineContext; import kotlinx.coroutines.BuildersKt; import kotlinx.coroutines.flow.FlowKt; import kotlinx.coroutines.sync.Mutex; import kotlinx.coroutines.sync.MutexKt; import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import im.angry.openeuicc.OpenEuiccApplication; import im.angry.openeuicc.di.AppContainer; import im.angry.openeuicc.di.UnprivilegedAppContainer; import im.angry.openeuicc.core.EuiccChannel; import im.angry.openeuicc.core.EuiccChannelManager; import im.angry.openeuicc.core.DefaultEuiccChannelManager; import im.angry.openeuicc.util.UiccCardInfoCompat; import im.angry.openeuicc.util.UiccPortInfoCompat; import im.angry.openeuicc.util.LPAUtilsKt; import im.angry.openeuicc.util.ActivationCode; import im.angry.openeuicc.util.PreferenceUtilsKt; import im.angry.openeuicc.util.PreferenceFlowWrapper; import net.typeblog.lpac_jni.LocalProfileInfo; import net.typeblog.lpac_jni.ProfileDownloadCallback; public class LpaProvider extends ContentProvider { private AppContainer appContainer; private Mutex mutex; private Gson gson; @Override public boolean onCreate() { appContainer = ((OpenEuiccApplication) getContext().getApplicationContext()).getAppContainer(); mutex = MutexKt.Mutex(false); gson = new GsonBuilder() .serializeNulls() .disableHtmlEscaping() .create(); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { MatrixCursor rows; final String endpoint = uri.getLastPathSegment(); final Map args = getArgsFromUri(uri); boolean[] json = new boolean[1]; if (endpoint == null) { rows = error("no_endpoint"); } else { try { rows = (MatrixCursor) BuildersKt.runBlocking ( EmptyCoroutineContext.INSTANCE, (_, continuation) -> MutexKt.withLock ( mutex, null, new Function0() { @Override public MatrixCursor invoke() { MatrixCursor rows; try { rows = switch (endpoint) { // out: string ping=pong case "ping" -> handlePing(args); // out (many): string name, bool enabled case "preferences" -> handleGetPreferences(args); // in: string name, bool enabled // out: bool success case "setPreference" -> handleSetPreference(args); // out (many, can be empty): int slotId, int portId case "cards" -> handleGetCards(args); // in: int slotId, int portId // out (many, can be empty): string iccid, bool enabled, string name, string nickname case "profiles" -> handleGetProfiles(args); // in: int slotId, int portId // out (single, can be empty): string iccid, bool enabled, string name, string nickname case "activeProfile" -> handleGetActiveProfile(args); // in: int slotId, int portId, (either {string activationCode} or {string address, string? matchingId}), string? confirmationCode, string? imei, string? callbackUrl // out (single, can be empty): string iccid, bool enabled, string name, string nickname case "downloadProfile" -> handleDownloadProfile(args); // in: int slotId, int portId, string iccid // out: bool success case "deleteProfile" -> handleDeleteProfile(args); // in: int slotId, int portId, string iccid, bool refresh=true // out: bool success case "enableProfile" -> handleEnableProfile(args); // in: int slotId, int portId, string iccid, bool refresh=true // out: bool success case "disableProfile" -> handleDisableProfile(args); // in: int slotId, int portId, bool refresh=true // out: bool success case "disableActiveProfile" -> handleDisableActiveProfile(args); // in: int slotId, int portId, string iccid, string nickname // out: bool success case "setProfileNickname" -> handleSetProfileNickname(args); default -> error("unknown_endpoint"); }; } catch (Exception ex) { rows = error(ex.getMessage()); } return rows; } }, continuation ) ); } catch (Exception ex) { rows = error(ex.getMessage()); } } rows = projectColumns(rows, projection, new String[] { "error" }); if (tryGetArgAsBoolean(args, "json", json) && json[0]) rows = row("rows", rowsToJson(rows)); return rows; } // region Mandatory Overrides @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public String getType(Uri uri) { return null; } // endregion // region Handlers private MatrixCursor handlePing(Map args) { return row("ping", "pong"); } private MatrixCursor handleGetPreferences(Map args) throws Exception { var preferences = List.of ( "verboseLogging", "safeguardActiveProfile", "filterProfileList", "ignoreTlsCertificate", "notificationsDownload", "notificationsDelete", "notificationsSwitch" ); if (!(appContainer instanceof UnprivilegedAppContainer)) preferences.add(1, "forceUseTelephonyManager"); var columns = new String[] { "name", "enabled" }; var values = new Object[preferences.size()][2]; for (int valIndex = 0; valIndex < preferences.size(); valIndex++) { String name = preferences.get(valIndex); values[valIndex][0] = name; values[valIndex][1] = getPreference(name); } return rows(columns, values); } private MatrixCursor handleSetPreference(Map args) throws Exception { String[] name = new String[1]; boolean[] enabled = new boolean[1]; if (!tryGetArgAsString(args, "name", name)) return missingArgError("name"); if (!tryGetArgAsBoolean(args, "enabled", enabled)) return missingArgError("enabled"); setPreference(name[0], enabled[0]); return success(); } private MatrixCursor handleGetCards(Map args) throws Exception { var euiccChannelManager = (DefaultEuiccChannelManager) appContainer.getEuiccChannelManager(); var getUiccCardsMethod = DefaultEuiccChannelManager.class.getDeclaredMethod("getUiccCards"); getUiccCardsMethod.setAccessible(true); @SuppressWarnings("unchecked") var cards = (List) getUiccCardsMethod.invoke(euiccChannelManager); var rows = new MatrixCursor(new String[] { "slotId", "portId" }); for (UiccCardInfoCompat card : cards) { for (UiccPortInfoCompat port : card.getPorts()) { int slotId = card.getPhysicalSlotIndex(); int portId = port.getPortIndex(); var euiccChannel = findEuiccChannel(euiccChannelManager, slotId, portId); if (euiccChannel != null) { rows.addRow(new Object[] { slotId, portId }); } } } return rows; } private MatrixCursor handleGetProfiles(Map args) throws Exception { var profiles = getProfiles(args); return profiles(profiles); } private MatrixCursor handleGetActiveProfile(Map args) throws Exception { var profiles = getProfiles(args); var enabledProfile = LPAUtilsKt.getEnabled(profiles); if (enabledProfile == null) return empty(); return profile(enabledProfile); } private MatrixCursor handleDownloadProfile(Map args) throws Exception { String[] address = new String[1]; String[] matchingId = { args.get("matchingId") }; String[] confirmationCode = { args.get("confirmationCode") }; String imei = args.get("imei"); String[] activationCodeArg = new String[1]; if (tryGetArgAsString(args, "activationCode", activationCodeArg)) { var activationCode = ActivationCode.Companion.fromString(activationCodeArg[0]); address[0] = activationCode.getAddress(); matchingId[0] = activationCode.getMatchingId(); if (activationCode.getConfirmationCodeRequired()) if (!tryGetArgAsString(args, "confirmationCode", confirmationCode)) return missingArgError("confirmationCode"); } else if (!tryGetArgAsString(args, "address", address)) return missingArgError("activationCode_or_address"); var profilesBefore = getProfiles(args); var iccidsBefore = profilesBefore.stream() .map(LocalProfileInfo::getIccid) .collect(Collectors.toSet()); withEuiccChannel ( args, (channel, _) -> { channel.getLpa().downloadProfile ( address[0], matchingId[0], imei, confirmationCode[0], new ProfileDownloadCallback() { @Override public void onStateUpdate(ProfileDownloadCallback.DownloadState state) { new Thread(() -> { try { String[] callbackUrl = new String[1]; if (tryGetArgAsString(args, "callbackUrl", callbackUrl)) { var url = new URI(callbackUrl[0]).toURL(); var data = new LinkedHashMap() {{ put("timestamp", Instant.now().getEpochSecond()); put("state", state.name()); put("progress", state.getProgress()); put("address", address[0]); put("matchingId", matchingId[0]); put("confirmationCode", confirmationCode[0]); put("imei", imei); }}; httpPostAsJson(url, data); } } catch (Exception ex) { // ignored } }).start(); } } ); return null; } ); var profilesAfter = getProfiles(args); var downloadedProfile = profilesAfter.stream() .filter(p -> !iccidsBefore.contains(p.getIccid())) .findFirst() .orElse(null); if (downloadedProfile == null) return empty(); // boolean[] enable = new boolean[1]; // boolean[] refresh = new boolean[1]; // if (tryGetArgAsBoolean(args, "enable", enable) && enable[0]) // { // if (!tryGetArgAsBoolean(args, "refresh", refresh)) // refresh[0] = true; // String iccid = downloadedProfile.getIccid(); // try // { // withEuiccChannel // ( // args, // (channel, _) -> channel.getLpa().enableProfile(iccid, refresh[0]) // ); // profilesAfter = getProfiles(args); // downloadedProfile = profilesAfter.stream() // .filter(p -> p.getIccid().equals(iccid)) // .findFirst() // .orElseThrow(); // should never happen // } // catch (Exception ex) // { // // ignored // } // } return profile(downloadedProfile); } private MatrixCursor handleDeleteProfile(Map args) throws Exception { String[] iccid = new String[1]; if (!tryGetArgAsString(args, "iccid", iccid)) return missingArgError("iccid"); safeguardActiveProfile(args, iccid[0]); boolean success = withEuiccChannel ( args, (channel, _) -> channel.getLpa().deleteProfile(iccid[0]) ); return success(success); } private MatrixCursor handleEnableProfile(Map args) throws Exception { String[] iccid = new String[1]; boolean[] refresh = new boolean[1]; if (!tryGetArgAsString(args, "iccid", iccid)) return missingArgError("iccid"); if (!tryGetArgAsBoolean(args, "refresh", refresh)) refresh[0] = true; boolean success = withEuiccChannel ( args, (channel, _) -> channel.getLpa().enableProfile(iccid[0], refresh[0]) ); return success(success); } private MatrixCursor handleDisableProfile(Map args) throws Exception { String[] iccid = new String[1]; boolean[] refresh = new boolean[1]; if (!tryGetArgAsString(args, "iccid", iccid)) return missingArgError("iccid"); if (!tryGetArgAsBoolean(args, "refresh", refresh)) refresh[0] = true; safeguardActiveProfile(args, iccid[0]); boolean success = withEuiccChannel ( args, (channel, _) -> channel.getLpa().disableProfile(iccid[0], refresh[0]) ); return success(success); } private MatrixCursor handleDisableActiveProfile(Map args) throws Exception { boolean[] refresh = new boolean[1]; if (!tryGetArgAsBoolean(args, "refresh", refresh)) refresh[0] = true; safeguardActiveProfile(args, null); String iccid = withEuiccChannel ( args, (channel, _) -> LPAUtilsKt.disableActiveProfileKeepIccId(channel.getLpa(), refresh[0]) ); return success(); // if (iccid == null) // return empty(); // var profiles = getProfiles(args); // var profile = profiles.stream() // .filter(p -> iccid.equals(p.getIccid())) // .findFirst() // .get(); // if (profile == null) // return empty(); // return profile(profile); } private MatrixCursor handleSetProfileNickname(Map args) throws Exception { String[] iccid = new String[1]; String[] nickname = new String[1]; if (!tryGetArgAsString(args, "iccid", iccid)) return missingArgError("iccid"); if (!tryGetArgAsString(args, "nickname", nickname)) return missingArgError("nickname"); withEuiccChannel ( args, (channel, _) -> { channel.getLpa().setNickname(iccid[0], nickname[0]); return null; } ); return success(); } // endregion // region LPA Helpers private static EuiccChannel findEuiccChannel(DefaultEuiccChannelManager euiccChannelManager, int slotId, int portId) throws Exception { var findEuiccChannelByPortMethod = DefaultEuiccChannelManager.class.getDeclaredMethod("findEuiccChannelByPort", int.class, int.class, Continuation.class); findEuiccChannelByPortMethod.setAccessible(true); return (EuiccChannel) BuildersKt.runBlocking ( EmptyCoroutineContext.INSTANCE, (_, continuation) -> { try { return findEuiccChannelByPortMethod.invoke(euiccChannelManager, slotId, portId, continuation); } catch (Exception ex) { return null; } } ); } @SuppressWarnings("unchecked") private T withEuiccChannel(Map args, Function2, ?> operation) throws Exception { var slotId = new int[1]; var portId = new int[1]; requireSlotAndPort(args, slotId, portId); var euiccChannelManager = appContainer.getEuiccChannelManager(); return (T) BuildersKt.runBlocking ( EmptyCoroutineContext.INSTANCE, (_, continuation) -> euiccChannelManager.withEuiccChannel(slotId[0], portId[0], operation, continuation) ); } private List getProfiles(Map args) throws Exception { @SuppressWarnings("unchecked") var profiles = (List) withEuiccChannel ( args, (channel, _) -> channel.getLpa().getProfiles() ); boolean filterProfileList = getPreference("filterProfileList"); if (filterProfileList) return LPAUtilsKt.getOperational(profiles); return profiles; } // endregion // region Preference Helpers private List invertedPreferences = List.of ( "safeguardActiveProfile", "filterProfileList" ); private PreferenceFlowWrapper getPreferenceFlow(String name) throws Exception { var preferenceRepository = PreferenceUtilsKt.getPreferenceRepository(getContext()); return switch (name) { case "verboseLogging" -> preferenceRepository.getVerboseLoggingFlow(); case "forceUseTelephonyManager" -> preferenceRepository.getForceUseTMAPIFlow(); case "safeguardActiveProfile" -> preferenceRepository.getDisableSafeguardFlow(); case "filterProfileList" -> preferenceRepository.getUnfilteredProfileListFlow(); case "ignoreTlsCertificate" -> preferenceRepository.getIgnoreTLSCertificateFlow(); case "notificationsDownload" -> preferenceRepository.getNotificationDownloadFlow(); case "notificationsDelete" -> preferenceRepository.getNotificationDeleteFlow(); case "notificationsSwitch" -> preferenceRepository.getNotificationSwitchFlow(); default -> throw new Exception("unknown_preference_name"); }; } private boolean getPreference(String name) throws Exception { var preferenceFlow = getPreferenceFlow(name); boolean enabled = BuildersKt.runBlocking ( EmptyCoroutineContext.INSTANCE, (_, continuation) -> FlowKt.first(preferenceFlow, continuation) ); if (invertedPreferences.contains(name)) enabled = !enabled; return enabled; } private void setPreference(String name, boolean enabled) throws Exception { var preferenceFlow = getPreferenceFlow(name); if (invertedPreferences.contains(name)) enabled = !enabled; final boolean enabledFinal = enabled; BuildersKt.runBlocking ( EmptyCoroutineContext.INSTANCE, (_, continuation) -> preferenceFlow.updatePreference(enabledFinal, continuation) ); } private void safeguardActiveProfile(Map args, String iccid) throws Exception { int[] slotId = new int[1]; int[] portId = new int[1]; requireSlotAndPort(args, slotId, portId); if (slotId[0] == EuiccChannelManager.USB_CHANNEL_ID) return; boolean safeguardEnabled = getPreference("safeguardActiveProfile"); if (!safeguardEnabled) return; boolean isTargetActive = iccid == null; if (!isTargetActive) { var profiles = getProfiles(args); var activeProfile = LPAUtilsKt.getEnabled(profiles); if (activeProfile == null) return; isTargetActive = iccid.equals(activeProfile.getIccid()); } if (isTargetActive) throw new Exception("safeguard_active_profile"); } // endregion // region Arg Helpers private static Map getArgsFromUri(Uri uri) { var args = new LinkedHashMap(); for (String name : uri.getQueryParameterNames()) { args.put(name, URLDecoder.decode(uri.getQueryParameter(name), StandardCharsets.UTF_8)); } return args; } private static boolean tryGetArgAsString(Map args, String key, String[] out) { String arg = args.get(key); if (arg == null || arg.isEmpty()) return false; out[0] = arg; return true; } private static boolean tryGetArgAsInt(Map args, String key, int[] out) { String arg = args.get(key); if (arg == null || arg.isEmpty()) return false; try { out[0] = Integer.parseInt(arg); return true; } catch (NumberFormatException ex) { return false; } } private static boolean tryGetArgAsBoolean(Map args, String key, boolean[] out) { String arg = args.get(key); if (arg == null) return false; out[0] = arg.isEmpty() || arg.equals("1") || arg.toLowerCase().startsWith("y") || arg.equalsIgnoreCase("on") || arg.equalsIgnoreCase("true"); return true; } private static void requireSlotAndPort(Map args, int[] slotIdOut, int[] portIdOut) throws Exception { final String slotIdArg = "slotId"; final String portIdArg = "portId"; if (!tryGetArgAsInt(args, slotIdArg, slotIdOut)) throw new Exception("missing_arg_" + slotIdArg); if (!tryGetArgAsInt(args, portIdArg, portIdOut)) throw new Exception("missing_arg_" + portIdArg); } // endregion // region Row Helpers private static MatrixCursor rows(String[] columns, Object[][] values) { var rows = new MatrixCursor(columns); for (Object[] rowValues : values) { rows.addRow(rowValues); } return rows; } private static MatrixCursor row(String column, String value) { return rows(new String[] { column }, new Object[][] { new Object[] { value } }); } private static MatrixCursor empty() { return new MatrixCursor(new String[0]); } private static MatrixCursor success() { return success(true); } private static MatrixCursor success(boolean success) { return row("success", Boolean.toString(success)); } private static MatrixCursor error(String message) { return row("error", message); } private static MatrixCursor missingArgError(String argName) { return error("missing_arg_" + argName); } private static MatrixCursor profile(LocalProfileInfo profile) { return profiles(Collections.singletonList(profile)); } private static MatrixCursor profiles(List profiles) { String[] columns = { "iccid", "enabled", "nickname" }; Object[][] rows = profiles.stream() .map(p -> new Object[] { p.getIccid(), LPAUtilsKt.isEnabled(p), p.getNickName() }) .toArray(Object[][]::new); return rows(columns, rows); } private static MatrixCursor projectColumns(MatrixCursor rows, String[] projection) { return projectColumns(rows, projection, null); } private static MatrixCursor projectColumns(MatrixCursor rows, String[] projection, String[] preserve) { var rowCols = new LinkedHashSet(Arrays.asList(rows.getColumnNames())); var outCols = new LinkedHashSet(); if (projection != null && projection.length > 0) outCols.addAll(Arrays.asList(projection)); else outCols.addAll(rowCols); if (preserve != null && preserve.length > 0) { Stream.of(preserve) .filter(rowCols::contains) .forEach(outCols::add); } if (outCols.isEmpty()) return rows; var outColsArray = outCols.toArray(new String[0]); var outRows = new MatrixCursor(outColsArray); rows.moveToPosition(-1); while (rows.moveToNext()) { var row = new Object[outColsArray.length]; for (int rowIndex = 0; rowIndex < outColsArray.length; rowIndex++) { String colName = outColsArray[rowIndex]; int colIndex = rows.getColumnIndex(colName); if (colIndex < 0) { row[rowIndex] = null; continue; } switch (rows.getType(colIndex)) { case Cursor.FIELD_TYPE_NULL: row[rowIndex] = null; break; case Cursor.FIELD_TYPE_INTEGER: row[rowIndex] = rows.getLong(colIndex); break; case Cursor.FIELD_TYPE_FLOAT: row[rowIndex] = rows.getDouble(colIndex); break; case Cursor.FIELD_TYPE_BLOB: row[rowIndex] = rows.getBlob(colIndex); break; case Cursor.FIELD_TYPE_STRING: default: row[rowIndex] = rows.getString(colIndex); break; } } outRows.addRow(row); } return outRows; } private String rowsToJson(MatrixCursor rows) { String[] rowCols = rows.getColumnNames(); var outRows = new ArrayList>(); rows.moveToPosition(-1); while (rows.moveToNext()) { var row = new LinkedHashMap(); for (String colName : rowCols) { int colIndex = rows.getColumnIndex(colName); switch (rows.getType(colIndex)) { case Cursor.FIELD_TYPE_NULL: row.put(colName, null); break; case Cursor.FIELD_TYPE_INTEGER: row.put(colName, rows.getLong(colIndex)); break; case Cursor.FIELD_TYPE_FLOAT: row.put(colName, rows.getDouble(colIndex)); break; case Cursor.FIELD_TYPE_BLOB: byte[] blob = rows.getBlob(colIndex); String blobBase64 = Base64.getEncoder().encodeToString(blob); row.put(colName, blobBase64); break; case Cursor.FIELD_TYPE_STRING: default: row.put(colName, rows.getString(colIndex)); break; } } var booleanCols = List.of ( "success", "enabled" ); for (String colName : booleanCols) { Object colValue = row.get(colName); if (colValue instanceof String) { String colValueString = (String) colValue; if (colValueString.equalsIgnoreCase(Boolean.toString(false)) || colValueString.equalsIgnoreCase(Boolean.toString(true))) { row.put(colName, Boolean.parseBoolean(colValueString)); } } } outRows.add(row); } return gson.toJson(outRows); } // endregion // region HTTP Helpers private void httpPostAsJson(URL url, Map data) throws Exception { String json = gson.toJson(data); var httpConnection = (HttpURLConnection) url.openConnection(); httpConnection.setRequestMethod("POST"); httpConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); httpConnection.setDoOutput(true); try (var outputStream = httpConnection.getOutputStream()) { outputStream.write(json.getBytes(StandardCharsets.UTF_8)); } httpConnection.getInputStream().close(); } // endregion }