1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.phone.euicc; 17 18 import android.annotation.NonNull; 19 import android.annotation.Nullable; 20 import android.app.Activity; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.os.Bundle; 28 import android.os.UserHandle; 29 import android.permission.LegacyPermissionManager; 30 import android.service.euicc.EuiccService; 31 import android.telephony.euicc.EuiccManager; 32 import android.util.Log; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.telephony.euicc.EuiccConnector; 36 import com.android.internal.telephony.util.TelephonyUtils; 37 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Set; 41 import java.util.concurrent.CountDownLatch; 42 import java.util.concurrent.LinkedBlockingQueue; 43 import java.util.concurrent.ThreadFactory; 44 import java.util.concurrent.ThreadPoolExecutor; 45 import java.util.concurrent.TimeUnit; 46 import java.util.concurrent.atomic.AtomicInteger; 47 48 /** Trampoline activity to forward eUICC intents from apps to the active UI implementation. */ 49 public class EuiccUiDispatcherActivity extends Activity { 50 private static final String TAG = "EuiccUiDispatcher"; 51 private static final long CHANGE_PERMISSION_TIMEOUT_MS = 15 * 1000; // 15 seconds 52 53 /** Flags to use when querying PackageManager for Euicc component implementations. */ 54 private static final int EUICC_QUERY_FLAGS = 55 PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DEBUG_TRIAGED_MISSING 56 | PackageManager.GET_RESOLVED_FILTER; 57 58 private LegacyPermissionManager mPermissionManager; 59 private boolean mGrantPermissionDone = false; 60 private ThreadPoolExecutor mExecutor; 61 62 @Override onCreate(Bundle savedInstanceState)63 public void onCreate(Bundle savedInstanceState) { 64 super.onCreate(savedInstanceState); 65 mPermissionManager = (LegacyPermissionManager) getSystemService( 66 Context.LEGACY_PERMISSION_SERVICE); 67 mExecutor = new ThreadPoolExecutor( 68 1 /* corePoolSize */, 69 1 /* maxPoolSize */, 70 15, TimeUnit.SECONDS, /* keepAliveTime */ 71 new LinkedBlockingQueue<>(), /* workQueue */ 72 new ThreadFactory() { 73 private final AtomicInteger mCount = new AtomicInteger(1); 74 75 @Override 76 public Thread newThread(Runnable r) { 77 return new Thread(r, "EuiccService #" + mCount.getAndIncrement()); 78 } 79 } 80 ); 81 try { 82 Intent euiccUiIntent = resolveEuiccUiIntent(); 83 if (euiccUiIntent == null) { 84 setResult(RESULT_CANCELED); 85 onDispatchFailure(); 86 return; 87 } 88 89 euiccUiIntent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 90 startActivity(euiccUiIntent); 91 } finally { 92 // Since we're using Theme.NO_DISPLAY, we must always finish() at the end of onCreate(). 93 finish(); 94 } 95 } 96 97 @VisibleForTesting 98 @Nullable resolveEuiccUiIntent()99 Intent resolveEuiccUiIntent() { 100 EuiccManager euiccManager = (EuiccManager) getSystemService(Context.EUICC_SERVICE); 101 if (!euiccManager.isEnabled()) { 102 Log.w(TAG, "eUICC not enabled"); 103 return null; 104 } 105 106 Intent euiccUiIntent = getEuiccUiIntent(); 107 if (euiccUiIntent == null) { 108 Log.w(TAG, "Unable to handle intent"); 109 return null; 110 } 111 112 revokePermissionFromLuiApps(euiccUiIntent); 113 114 ActivityInfo activityInfo = findBestActivity(euiccUiIntent); 115 if (activityInfo == null) { 116 Log.w(TAG, "Could not resolve activity for intent: " + euiccUiIntent); 117 return null; 118 } 119 120 grantDefaultPermissionsToLuiApp(activityInfo); 121 122 euiccUiIntent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name)); 123 return euiccUiIntent; 124 } 125 126 /** Called when dispatch fails. May be overridden to perform some operation here. */ onDispatchFailure()127 protected void onDispatchFailure() { 128 } 129 130 /** 131 * Return an Intent to start the Euicc app's UI for the given intent, or null if given intent 132 * cannot be handled. 133 */ 134 @Nullable getEuiccUiIntent()135 protected Intent getEuiccUiIntent() { 136 String action = getIntent().getAction(); 137 138 Intent intent = new Intent(); 139 intent.putExtras(getIntent()); 140 switch (action) { 141 case EuiccManager.ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS: 142 intent.setAction(EuiccService.ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS); 143 break; 144 case EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION: 145 intent.setAction(EuiccService.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION); 146 break; 147 default: 148 Log.w(TAG, "Unsupported action: " + action); 149 return null; 150 } 151 152 return intent; 153 } 154 155 @Override onDestroy()156 protected void onDestroy() { 157 mExecutor.shutdownNow(); 158 super.onDestroy(); 159 } 160 161 @VisibleForTesting 162 @Nullable findBestActivity(Intent euiccUiIntent)163 ActivityInfo findBestActivity(Intent euiccUiIntent) { 164 return EuiccConnector.findBestActivity(getPackageManager(), euiccUiIntent); 165 } 166 167 /** Grants default permissions to the active LUI app. */ 168 @VisibleForTesting grantDefaultPermissionsToLuiApp(ActivityInfo activityInfo)169 protected void grantDefaultPermissionsToLuiApp(ActivityInfo activityInfo) { 170 CountDownLatch latch = new CountDownLatch(1); 171 try { 172 mPermissionManager.grantDefaultPermissionsToLuiApp( 173 activityInfo.packageName, UserHandle.of(UserHandle.myUserId()), mExecutor, 174 isSuccess -> { 175 if (isSuccess) { 176 latch.countDown(); 177 } else { 178 Log.e(TAG, "Failed to revoke LUI app permissions."); 179 } 180 }); 181 TelephonyUtils.waitUntilReady(latch, CHANGE_PERMISSION_TIMEOUT_MS); 182 } catch (RuntimeException e) { 183 Log.e(TAG, "Failed to grant permissions to active LUI app.", e); 184 } 185 } 186 187 /** Cleans up all the packages that shouldn't have permission. */ 188 @VisibleForTesting revokePermissionFromLuiApps(Intent intent)189 protected void revokePermissionFromLuiApps(Intent intent) { 190 CountDownLatch latch = new CountDownLatch(1); 191 try { 192 Set<String> luiApps = getAllLuiAppPackageNames(intent); 193 String[] luiAppsArray = luiApps.toArray(new String[luiApps.size()]); 194 mPermissionManager.revokeDefaultPermissionsFromLuiApps(luiAppsArray, 195 UserHandle.of(UserHandle.myUserId()), mExecutor, isSuccess -> { 196 if (isSuccess) { 197 latch.countDown(); 198 } else { 199 Log.e(TAG, "Failed to revoke LUI app permissions."); 200 } 201 }); 202 TelephonyUtils.waitUntilReady(latch, CHANGE_PERMISSION_TIMEOUT_MS); 203 } catch (RuntimeException e) { 204 Log.e(TAG, "Failed to revoke LUI app permissions."); 205 throw e; 206 } 207 } 208 209 @NonNull getAllLuiAppPackageNames(Intent intent)210 private Set<String> getAllLuiAppPackageNames(Intent intent) { 211 List<ResolveInfo> luiPackages = 212 getPackageManager().queryIntentServices(intent, EUICC_QUERY_FLAGS); 213 HashSet<String> packageNames = new HashSet<>(); 214 for (ResolveInfo info : luiPackages) { 215 if (info.serviceInfo == null) continue; 216 packageNames.add(info.serviceInfo.packageName); 217 } 218 return packageNames; 219 } 220 } 221