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