1 /** 2 * Copyright (C) 2022 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 17 package com.android.settings.network.telephony; 18 19 import static androidx.lifecycle.Lifecycle.Event.ON_START; 20 import static androidx.lifecycle.Lifecycle.Event.ON_STOP; 21 22 import android.Manifest; 23 import android.app.Activity; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.ActivityInfo; 28 import android.content.pm.ComponentInfo; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.content.pm.ServiceInfo; 32 import android.service.euicc.EuiccService; 33 import android.telephony.SubscriptionInfo; 34 import android.telephony.SubscriptionManager; 35 import android.telephony.euicc.EuiccManager; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import androidx.annotation.VisibleForTesting; 40 import androidx.lifecycle.LifecycleObserver; 41 import androidx.lifecycle.LifecycleOwner; 42 import androidx.lifecycle.OnLifecycleEvent; 43 import androidx.preference.Preference; 44 import androidx.preference.PreferenceScreen; 45 46 import com.android.internal.telephony.flags.Flags; 47 import com.android.internal.telephony.util.TelephonyUtils; 48 import com.android.settings.network.MobileNetworkRepository; 49 import com.android.settingslib.core.lifecycle.Lifecycle; 50 import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity; 51 52 import org.jetbrains.annotations.NotNull; 53 54 import java.util.ArrayList; 55 import java.util.List; 56 57 public class ConvertToEsimPreferenceController extends TelephonyBasePreferenceController implements 58 LifecycleObserver, MobileNetworkRepository.MobileNetworkCallback { 59 private static final String TAG = "ConvertToEsimPreference"; 60 private Preference mPreference; 61 private LifecycleOwner mLifecycleOwner; 62 private MobileNetworkRepository mMobileNetworkRepository; 63 private List<SubscriptionInfoEntity> mSubscriptionInfoEntityList = new ArrayList<>(); 64 private SubscriptionInfoEntity mSubscriptionInfoEntity; 65 private static int sQueryFlag = 66 PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DIRECT_BOOT_AUTO 67 | PackageManager.GET_RESOLVED_FILTER; 68 ConvertToEsimPreferenceController(Context context, String key, Lifecycle lifecycle, LifecycleOwner lifecycleOwner, int subId)69 public ConvertToEsimPreferenceController(Context context, String key, Lifecycle lifecycle, 70 LifecycleOwner lifecycleOwner, int subId) { 71 this(context, key); 72 mSubId = subId; 73 mLifecycleOwner = lifecycleOwner; 74 if (lifecycle != null) { 75 lifecycle.addObserver(this); 76 } 77 } 78 ConvertToEsimPreferenceController(Context context, String key)79 public ConvertToEsimPreferenceController(Context context, String key) { 80 super(context, key); 81 mMobileNetworkRepository = MobileNetworkRepository.getInstance(context); 82 } 83 init(int subId, SubscriptionInfoEntity subInfoEntity)84 public void init(int subId, SubscriptionInfoEntity subInfoEntity) { 85 mSubId = subId; 86 mSubscriptionInfoEntity = subInfoEntity; 87 } 88 89 @OnLifecycleEvent(ON_START) onStart()90 public void onStart() { 91 mMobileNetworkRepository.addRegister(mLifecycleOwner, this, mSubId); 92 mMobileNetworkRepository.updateEntity(); 93 } 94 95 @OnLifecycleEvent(ON_STOP) onStop()96 public void onStop() { 97 mMobileNetworkRepository.removeRegister(this); 98 } 99 100 @Override displayPreference(PreferenceScreen screen)101 public void displayPreference(PreferenceScreen screen) { 102 super.displayPreference(screen); 103 mPreference = screen.findPreference(getPreferenceKey()); 104 } 105 106 @Override getAvailabilityStatus(int subId)107 public int getAvailabilityStatus(int subId) { 108 // TODO(b/315073761) : Add a new API to set whether the profile has been 109 // converted/transferred. Remove any confusion to the user according to the set value. 110 111 /* 112 * If pSIM is set to preferred SIM and there is an active eSIM, convert the pSIM to eSIM 113 * and then disable the pSIM. 114 * This causes a dialog to switch the preferred SIM to downloaded new eSIM. 115 * This may cause confusion for the user about the seamless conversion. 116 * To avoid showing users dialogs that can cause confusion, 117 * add conditions to allow conversion in the absence of active eSIM. 118 */ 119 if (!Flags.supportPsimToEsimConversion()) { 120 Log.d(TAG, "supportPsimToEsimConversion flag is not enabled"); 121 return CONDITIONALLY_UNAVAILABLE; 122 } 123 124 SubscriptionManager subscriptionManager = mContext.getSystemService( 125 SubscriptionManager.class); 126 SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId); 127 if (subInfo == null) { 128 return CONDITIONALLY_UNAVAILABLE; 129 } 130 EuiccManager euiccManager = (EuiccManager) 131 mContext.getSystemService(Context.EUICC_SERVICE); 132 try { 133 if (!euiccManager.isPsimConversionSupported(subInfo.getCarrierId())) { 134 Log.i(TAG, "subId is not matched with pSIM conversion" 135 + " supported carriers:" + subInfo.getCarrierId()); 136 return CONDITIONALLY_UNAVAILABLE; 137 } 138 if (findConversionSupportComponent()) { 139 return mSubscriptionInfoEntity != null 140 && mSubscriptionInfoEntity.isActiveSubscriptionId 141 && !mSubscriptionInfoEntity.isEmbedded && isActiveSubscription(subId) 142 ? AVAILABLE 143 : CONDITIONALLY_UNAVAILABLE; 144 } 145 } catch (RuntimeException e) { 146 Log.e(TAG, "Fail to check pSIM conversion supported carrier: " + e.getMessage()); 147 } 148 return CONDITIONALLY_UNAVAILABLE; 149 } 150 151 @VisibleForTesting update()152 void update() { 153 if (mPreference == null) { 154 return; 155 } 156 mPreference.setVisible(getAvailabilityStatus(mSubId) == AVAILABLE); 157 } 158 159 @Override handlePreferenceTreeClick(Preference preference)160 public boolean handlePreferenceTreeClick(Preference preference) { 161 if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { 162 return false; 163 } 164 // Send intent to launch LPA 165 Intent intent = new Intent(EuiccManager.ACTION_CONVERT_TO_EMBEDDED_SUBSCRIPTION); 166 intent.putExtra("subId", mSubId); 167 mContext.startActivity(intent); 168 ((Activity) mContext).finish(); 169 return true; 170 } 171 172 @VisibleForTesting setSubscriptionInfoEntity(SubscriptionInfoEntity subscriptionInfoEntity)173 public void setSubscriptionInfoEntity(SubscriptionInfoEntity subscriptionInfoEntity) { 174 mSubscriptionInfoEntity = subscriptionInfoEntity; 175 } 176 177 @Override onActiveSubInfoChanged(List<SubscriptionInfoEntity> subInfoEntityList)178 public void onActiveSubInfoChanged(List<SubscriptionInfoEntity> subInfoEntityList) { 179 mSubscriptionInfoEntityList = subInfoEntityList; 180 mSubscriptionInfoEntityList.forEach(entity -> { 181 if (Integer.parseInt(entity.subId) == mSubId) { 182 mSubscriptionInfoEntity = entity; 183 update(); 184 } 185 }); 186 } 187 isActiveSubscription(int subId)188 private boolean isActiveSubscription(int subId) { 189 SubscriptionManager subscriptionManager = mContext.getSystemService( 190 SubscriptionManager.class); 191 SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId); 192 if (subInfo == null) { 193 return false; 194 } 195 return true; 196 } 197 findConversionSupportComponent()198 private boolean findConversionSupportComponent() { 199 Intent intent = new Intent(EuiccService.ACTION_CONVERT_TO_EMBEDDED_SUBSCRIPTION); 200 PackageManager packageManager = mContext.getPackageManager(); 201 List<ResolveInfo> resolveInfoList = packageManager 202 .queryIntentActivities(intent, sQueryFlag); 203 if (resolveInfoList == null || resolveInfoList.isEmpty()) { 204 return false; 205 } 206 for (ResolveInfo resolveInfo : resolveInfoList) { 207 if (!isValidEuiccComponent(packageManager, resolveInfo)) { 208 continue; 209 } else { 210 return true; 211 } 212 } 213 return true; 214 } 215 isValidEuiccComponent( PackageManager packageManager, @NotNull ResolveInfo resolveInfo)216 private boolean isValidEuiccComponent( 217 PackageManager packageManager, @NotNull ResolveInfo resolveInfo) { 218 ComponentInfo componentInfo = TelephonyUtils.getComponentInfo(resolveInfo); 219 String packageName = new ComponentName(componentInfo.packageName, componentInfo.name) 220 .getPackageName(); 221 222 // Verify that the app is privileged (via granting of a privileged permission). 223 if (packageManager.checkPermission( 224 Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS, packageName) 225 != PackageManager.PERMISSION_GRANTED) { 226 return false; 227 } 228 229 // Verify that only the system can access the component. 230 final String permission; 231 if (componentInfo instanceof ServiceInfo) { 232 permission = ((ServiceInfo) componentInfo).permission; 233 } else if (componentInfo instanceof ActivityInfo) { 234 permission = ((ActivityInfo) componentInfo).permission; 235 } else { 236 return false; 237 } 238 if (!TextUtils.equals(permission, Manifest.permission.BIND_EUICC_SERVICE)) { 239 return false; 240 } 241 242 // Verify that the component declares a priority. 243 if (resolveInfo.filter == null || resolveInfo.filter.getPriority() == 0) { 244 return false; 245 } 246 return true; 247 } 248 } 249