/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.phone.euicc; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.UserHandle; import android.permission.LegacyPermissionManager; import android.service.euicc.EuiccService; import android.telephony.euicc.EuiccManager; import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.euicc.EuiccConnector; import com.android.internal.telephony.util.TelephonyUtils; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** Trampoline activity to forward eUICC intents from apps to the active UI implementation. */ public class EuiccUiDispatcherActivity extends Activity { private static final String TAG = "EuiccUiDispatcher"; private static final long CHANGE_PERMISSION_TIMEOUT_MS = 15 * 1000; // 15 seconds /** Flags to use when querying PackageManager for Euicc component implementations. */ private static final int EUICC_QUERY_FLAGS = PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DEBUG_TRIAGED_MISSING | PackageManager.GET_RESOLVED_FILTER; private LegacyPermissionManager mPermissionManager; private boolean mGrantPermissionDone = false; private ThreadPoolExecutor mExecutor; // Used for CTS EuiccManager action verification private static ComponentName mTestEuiccUiComponentName; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPermissionManager = (LegacyPermissionManager) getSystemService( Context.LEGACY_PERMISSION_SERVICE); mExecutor = new ThreadPoolExecutor( 1 /* corePoolSize */, 1 /* maxPoolSize */, 15, TimeUnit.SECONDS, /* keepAliveTime */ new LinkedBlockingQueue<>(), /* workQueue */ new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { return new Thread(r, "EuiccService #" + mCount.getAndIncrement()); } } ); try { Intent euiccUiIntent = resolveEuiccUiIntent(); if (euiccUiIntent == null) { setResult(RESULT_CANCELED); onDispatchFailure(); return; } euiccUiIntent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); startActivity(euiccUiIntent); } finally { // Since we're using Theme.NO_DISPLAY, we must always finish() at the end of onCreate(). finish(); } } /** * This API used to set the Test EuiccUiComponent for CTS * @param packageName package which handles the intent * @param componentName ui component to be launched for testing */ public static void setTestEuiccUiComponent(String packageName, String componentName) { mTestEuiccUiComponentName = null; if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(componentName)) { mTestEuiccUiComponentName = new ComponentName(packageName, componentName); } } @VisibleForTesting @Nullable Intent resolveEuiccUiIntent() { EuiccManager euiccManager = (EuiccManager) getSystemService(Context.EUICC_SERVICE); if (euiccManager == null || !euiccManager.isEnabled()) { Log.w(TAG, "eUICC not enabled"); return null; } Intent euiccUiIntent = getEuiccUiIntent(); if (euiccUiIntent == null) { Log.w(TAG, "Unable to handle intent"); return null; } if (mTestEuiccUiComponentName != null) { Log.i(TAG, "Test mode"); euiccUiIntent.setComponent(mTestEuiccUiComponentName); mTestEuiccUiComponentName = null; return euiccUiIntent; } revokePermissionFromLuiApps(euiccUiIntent); ActivityInfo activityInfo = findBestActivity(euiccUiIntent); if (activityInfo == null) { Log.w(TAG, "Could not resolve activity for intent: " + euiccUiIntent); return null; } grantDefaultPermissionsToLuiApp(activityInfo); euiccUiIntent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name)); return euiccUiIntent; } /** Called when dispatch fails. May be overridden to perform some operation here. */ protected void onDispatchFailure() { } /** * Return an Intent to start the Euicc app's UI for the given intent, or null if given intent * cannot be handled. */ @Nullable protected Intent getEuiccUiIntent() { String action = getIntent().getAction(); if (action == null) { Log.w(TAG, "No action is specified in the intent"); return null; } Intent intent = new Intent(); intent.putExtras(getIntent()); switch (action) { case EuiccManager.ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS: intent.setAction(EuiccService.ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS); break; case EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION: intent.setAction(EuiccService.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION); break; case EuiccManager.ACTION_TRANSFER_EMBEDDED_SUBSCRIPTIONS: intent.setAction(EuiccService.ACTION_TRANSFER_EMBEDDED_SUBSCRIPTIONS); break; case EuiccManager.ACTION_CONVERT_TO_EMBEDDED_SUBSCRIPTION: intent.setAction(EuiccService.ACTION_CONVERT_TO_EMBEDDED_SUBSCRIPTION); break; default: Log.w(TAG, "Unsupported action: " + action); return null; } return intent; } @Override protected void onDestroy() { mExecutor.shutdownNow(); super.onDestroy(); } @VisibleForTesting @Nullable ActivityInfo findBestActivity(Intent euiccUiIntent) { return EuiccConnector.findBestActivity(getPackageManager(), euiccUiIntent); } /** Grants default permissions to the active LUI app. */ @VisibleForTesting protected void grantDefaultPermissionsToLuiApp(ActivityInfo activityInfo) { CountDownLatch latch = new CountDownLatch(1); try { mPermissionManager.grantDefaultPermissionsToLuiApp( activityInfo.packageName, UserHandle.of(UserHandle.myUserId()), mExecutor, isSuccess -> { if (isSuccess) { latch.countDown(); } else { Log.e(TAG, "Failed to revoke LUI app permissions."); } }); TelephonyUtils.waitUntilReady(latch, CHANGE_PERMISSION_TIMEOUT_MS); } catch (RuntimeException e) { Log.e(TAG, "Failed to grant permissions to active LUI app.", e); } } /** Cleans up all the packages that shouldn't have permission. */ @VisibleForTesting protected void revokePermissionFromLuiApps(Intent intent) { CountDownLatch latch = new CountDownLatch(1); try { Set luiApps = getAllLuiAppPackageNames(intent); String[] luiAppsArray = luiApps.toArray(new String[luiApps.size()]); mPermissionManager.revokeDefaultPermissionsFromLuiApps(luiAppsArray, UserHandle.of(UserHandle.myUserId()), mExecutor, isSuccess -> { if (isSuccess) { latch.countDown(); } else { Log.e(TAG, "Failed to revoke LUI app permissions."); } }); TelephonyUtils.waitUntilReady(latch, CHANGE_PERMISSION_TIMEOUT_MS); } catch (RuntimeException e) { Log.e(TAG, "Failed to revoke LUI app permissions."); throw e; } } @NonNull private Set getAllLuiAppPackageNames(Intent intent) { List luiPackages = getPackageManager().queryIntentServices(intent, EUICC_QUERY_FLAGS); HashSet packageNames = new HashSet<>(); for (ResolveInfo info : luiPackages) { if (info.serviceInfo == null) continue; packageNames.add(info.serviceInfo.packageName); } return packageNames; } }