1 /* 2 * Copyright (C) 2018 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.wifi.calling; 18 19 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; 20 21 import android.app.PendingIntent; 22 import android.content.ComponentName; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.net.Uri; 27 import android.os.PersistableBundle; 28 import android.provider.Settings; 29 import android.support.v4.graphics.drawable.IconCompat; 30 import android.telephony.CarrierConfigManager; 31 import android.telephony.SubscriptionManager; 32 import android.telephony.TelephonyManager; 33 import android.text.TextUtils; 34 import android.util.Log; 35 36 import androidx.slice.Slice; 37 import androidx.slice.builders.ListBuilder; 38 import androidx.slice.builders.SliceAction; 39 40 import com.android.ims.ImsManager; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.settings.R; 43 import com.android.settings.slices.SettingsSliceProvider; 44 import com.android.settings.slices.SliceBroadcastReceiver; 45 import com.android.settings.slices.SliceBuilderUtils; 46 47 import java.util.concurrent.Callable; 48 import java.util.concurrent.ExecutionException; 49 import java.util.concurrent.ExecutorService; 50 import java.util.concurrent.Executors; 51 import java.util.concurrent.FutureTask; 52 import java.util.concurrent.TimeUnit; 53 import java.util.concurrent.TimeoutException; 54 55 56 /** 57 * Helper class to control slices for wifi calling settings. 58 */ 59 public class WifiCallingSliceHelper { 60 61 private static final String TAG = "WifiCallingSliceHelper"; 62 63 /** 64 * Settings slice path to wifi calling setting. 65 */ 66 public static final String PATH_WIFI_CALLING = "wifi_calling"; 67 68 /** 69 * Action passed for changes to wifi calling slice (toggle). 70 */ 71 public static final String ACTION_WIFI_CALLING_CHANGED = 72 "com.android.settings.wifi.calling.action.WIFI_CALLING_CHANGED"; 73 74 /** 75 * Action for Wifi calling Settings activity which 76 * allows setting configuration for Wifi calling 77 * related settings 78 */ 79 public static final String ACTION_WIFI_CALLING_SETTINGS_ACTIVITY = 80 "android.settings.WIFI_CALLING_SETTINGS"; 81 82 /** 83 * Full {@link Uri} for the Wifi Calling Slice. 84 */ 85 public static final Uri WIFI_CALLING_URI = new Uri.Builder() 86 .scheme(ContentResolver.SCHEME_CONTENT) 87 .authority(SettingsSliceProvider.SLICE_AUTHORITY) 88 .appendPath(PATH_WIFI_CALLING) 89 .build(); 90 91 /** 92 * Timeout for querying wifi calling setting from ims manager. 93 */ 94 private static final int TIMEOUT_MILLIS = 2000; 95 96 protected SubscriptionManager mSubscriptionManager; 97 private final Context mContext; 98 99 @VisibleForTesting 100 public WifiCallingSliceHelper(Context context) { 101 mContext = context; 102 } 103 104 /** 105 * Returns Slice object for wifi calling settings. 106 * 107 * If wifi calling is being turned on and if wifi calling activation is needed for the current 108 * carrier, this method will return Slice with instructions to go to Settings App. 109 * 110 * If wifi calling is not supported for the current carrier, this method will return slice with 111 * not supported message. 112 * 113 * If wifi calling setting can be changed, this method will return the slice to toggle wifi 114 * calling option with ACTION_WIFI_CALLING_CHANGED as endItem. 115 */ 116 public Slice createWifiCallingSlice(Uri sliceUri) { 117 final int subId = getDefaultVoiceSubId(); 118 final String carrierName = getSimCarrierName(); 119 120 if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 121 Log.d(TAG, "Invalid subscription Id"); 122 return getNonActionableWifiCallingSlice( 123 mContext.getString(R.string.wifi_calling_settings_title), 124 mContext.getString(R.string.wifi_calling_not_supported, carrierName), 125 sliceUri, getSettingsIntent(mContext)); 126 } 127 128 final ImsManager imsManager = getImsManager(subId); 129 130 if (!imsManager.isWfcEnabledByPlatform() 131 || !imsManager.isWfcProvisionedOnDevice()) { 132 Log.d(TAG, "Wifi calling is either not provisioned or not enabled by Platform"); 133 return getNonActionableWifiCallingSlice( 134 mContext.getString(R.string.wifi_calling_settings_title), 135 mContext.getString(R.string.wifi_calling_not_supported, carrierName), 136 sliceUri, getSettingsIntent(mContext)); 137 } 138 139 try { 140 final boolean isWifiCallingEnabled = isWifiCallingEnabled(imsManager); 141 final Intent activationAppIntent = 142 getWifiCallingCarrierActivityIntent(subId); 143 144 // Send this actionable wifi calling slice to toggle the setting 145 // only when there is no need for wifi calling activation with the server 146 if (activationAppIntent != null && !isWifiCallingEnabled) { 147 Log.d(TAG, "Needs Activation"); 148 // Activation needed for the next action of the user 149 // Give instructions to go to settings app 150 return getNonActionableWifiCallingSlice( 151 mContext.getString(R.string.wifi_calling_settings_title), 152 mContext.getString( 153 R.string.wifi_calling_settings_activation_instructions), 154 sliceUri, getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY)); 155 } 156 return getWifiCallingSlice(sliceUri, mContext, isWifiCallingEnabled); 157 } catch (InterruptedException | TimeoutException | ExecutionException e) { 158 Log.e(TAG, "Unable to read the current WiFi calling status", e); 159 return getNonActionableWifiCallingSlice( 160 mContext.getString(R.string.wifi_calling_settings_title), 161 mContext.getString(R.string.wifi_calling_turn_on), 162 sliceUri, getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY)); 163 } 164 } 165 166 private boolean isWifiCallingEnabled(ImsManager imsManager) 167 throws InterruptedException, ExecutionException, TimeoutException { 168 final FutureTask<Boolean> isWifiOnTask = new FutureTask<>(new Callable<Boolean>() { 169 @Override 170 public Boolean call() { 171 return imsManager.isWfcEnabledByUser(); 172 } 173 }); 174 final ExecutorService executor = Executors.newSingleThreadExecutor(); 175 executor.execute(isWifiOnTask); 176 177 Boolean isWifiEnabledByUser = false; 178 isWifiEnabledByUser = isWifiOnTask.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 179 180 return isWifiEnabledByUser && imsManager.isNonTtyOrTtyOnVolteEnabled(); 181 } 182 183 /** 184 * Builds a toggle slice where the intent takes you to the wifi calling page and the toggle 185 * enables/disables wifi calling. 186 */ 187 private Slice getWifiCallingSlice(Uri sliceUri, Context mContext, 188 boolean isWifiCallingEnabled) { 189 190 final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.wifi_signal); 191 final String title = mContext.getString(R.string.wifi_calling_settings_title); 192 return new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) 193 .setColor(R.color.material_blue_500) 194 .addRow(b -> b 195 .setTitle(title) 196 .addEndItem( 197 new SliceAction( 198 getBroadcastIntent(ACTION_WIFI_CALLING_CHANGED), 199 null /* actionTitle */, isWifiCallingEnabled)) 200 .setPrimaryAction(new SliceAction( 201 getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY), 202 icon, 203 title))) 204 .build(); 205 } 206 207 protected ImsManager getImsManager(int subId) { 208 return ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(subId)); 209 } 210 211 private Integer getWfcMode(ImsManager imsManager) 212 throws InterruptedException, ExecutionException, TimeoutException { 213 FutureTask<Integer> wfcModeTask = new FutureTask<>(new Callable<Integer>() { 214 @Override 215 public Integer call() { 216 return imsManager.getWfcMode(false); 217 } 218 }); 219 ExecutorService executor = Executors.newSingleThreadExecutor(); 220 executor.execute(wfcModeTask); 221 return wfcModeTask.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 222 } 223 224 /** 225 * Handles wifi calling setting change from wifi calling slice and posts notification. Should be 226 * called when intent action is ACTION_WIFI_CALLING_CHANGED. Executed in @WorkerThread 227 * 228 * @param intent action performed 229 */ 230 public void handleWifiCallingChanged(Intent intent) { 231 final int subId = getDefaultVoiceSubId(); 232 233 if (subId > SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 234 final ImsManager imsManager = getImsManager(subId); 235 if (imsManager.isWfcEnabledByPlatform() 236 || imsManager.isWfcProvisionedOnDevice()) { 237 final boolean currentValue = imsManager.isWfcEnabledByUser() 238 && imsManager.isNonTtyOrTtyOnVolteEnabled(); 239 final boolean newValue = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, 240 currentValue); 241 final Intent activationAppIntent = 242 getWifiCallingCarrierActivityIntent(subId); 243 if (!newValue || activationAppIntent == null) { 244 // If either the action is to turn off wifi calling setting 245 // or there is no activation involved - Update the setting 246 if (newValue != currentValue) { 247 imsManager.setWfcSetting(newValue); 248 } 249 } 250 } 251 } 252 // notify change in slice in any case to get re-queried. This would result in displaying 253 // appropriate message with the updated setting. 254 final Uri uri = SliceBuilderUtils.getUri(PATH_WIFI_CALLING, false /*isPlatformSlice*/); 255 mContext.getContentResolver().notifyChange(uri, null); 256 } 257 258 /** 259 * Returns Slice with the title and subtitle provided as arguments with wifi signal Icon. 260 * 261 * @param title Title of the slice 262 * @param subtitle Subtitle of the slice 263 * @param sliceUri slice uri 264 * @return Slice with title and subtitle 265 */ 266 // TODO(b/79548264) asses different scenarios and return null instead of non-actionable slice 267 private Slice getNonActionableWifiCallingSlice(String title, String subtitle, Uri sliceUri, 268 PendingIntent primaryActionIntent) { 269 final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.wifi_signal); 270 return new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) 271 .setColor(R.color.material_blue_500) 272 .addRow(b -> b 273 .setTitle(title) 274 .setSubtitle(subtitle) 275 .setPrimaryAction(new SliceAction( 276 primaryActionIntent, icon, 277 title))) 278 .build(); 279 } 280 281 /** 282 * Returns {@code true} when the key is enabled for the carrier, and {@code false} otherwise. 283 */ 284 private boolean isCarrierConfigManagerKeyEnabled(Context mContext, String key, 285 int subId, boolean defaultValue) { 286 final CarrierConfigManager configManager = getCarrierConfigManager(mContext); 287 boolean ret = false; 288 if (configManager != null) { 289 final PersistableBundle bundle = configManager.getConfigForSubId(subId); 290 if (bundle != null) { 291 ret = bundle.getBoolean(key, defaultValue); 292 } 293 } 294 return ret; 295 } 296 297 protected CarrierConfigManager getCarrierConfigManager(Context mContext) { 298 return mContext.getSystemService(CarrierConfigManager.class); 299 } 300 301 /** 302 * Returns the current default voice subId obtained from SubscriptionManager 303 */ 304 protected int getDefaultVoiceSubId() { 305 if (mSubscriptionManager == null) { 306 mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); 307 } 308 return SubscriptionManager.getDefaultVoiceSubscriptionId(); 309 } 310 311 /** 312 * Returns Intent of the activation app required to activate wifi calling or null if there is no 313 * need for activation. 314 */ 315 protected Intent getWifiCallingCarrierActivityIntent(int subId) { 316 final CarrierConfigManager configManager = getCarrierConfigManager(mContext); 317 if (configManager == null) { 318 return null; 319 } 320 321 final PersistableBundle bundle = configManager.getConfigForSubId(subId); 322 if (bundle == null) { 323 return null; 324 } 325 326 final String carrierApp = bundle.getString( 327 CarrierConfigManager.KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING); 328 if (TextUtils.isEmpty(carrierApp)) { 329 return null; 330 } 331 332 final ComponentName componentName = ComponentName.unflattenFromString(carrierApp); 333 if (componentName == null) { 334 return null; 335 } 336 337 final Intent intent = new Intent(); 338 intent.setComponent(componentName); 339 return intent; 340 } 341 342 /** 343 * @return {@link PendingIntent} to the Settings home page. 344 */ 345 public static PendingIntent getSettingsIntent(Context context) { 346 final Intent intent = new Intent(Settings.ACTION_SETTINGS); 347 return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */); 348 } 349 350 private PendingIntent getBroadcastIntent(String action) { 351 final Intent intent = new Intent(action); 352 intent.setClass(mContext, SliceBroadcastReceiver.class); 353 return PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent, 354 PendingIntent.FLAG_CANCEL_CURRENT); 355 } 356 357 /** 358 * Returns PendingIntent to start activity specified by action 359 */ 360 private PendingIntent getActivityIntent(String action) { 361 final Intent intent = new Intent(action); 362 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 363 return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); 364 } 365 366 /** 367 * Returns carrier id name of the current Subscription 368 */ 369 private String getSimCarrierName() { 370 final TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); 371 final CharSequence carrierName = telephonyManager.getSimCarrierIdName(); 372 if (carrierName == null) { 373 return mContext.getString(R.string.carrier); 374 } 375 return carrierName.toString(); 376 } 377 378 } 379