| /* |
| * 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.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<String> 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<String> getAllLuiAppPackageNames(Intent intent) { |
| List<ResolveInfo> luiPackages = |
| getPackageManager().queryIntentServices(intent, EUICC_QUERY_FLAGS); |
| HashSet<String> packageNames = new HashSet<>(); |
| for (ResolveInfo info : luiPackages) { |
| if (info.serviceInfo == null) continue; |
| packageNames.add(info.serviceInfo.packageName); |
| } |
| return packageNames; |
| } |
| } |