/* * Copyright (C) 2018 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.settings.wifi.calling; import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; import android.app.PendingIntent; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.PersistableBundle; import android.provider.Settings; import android.support.v4.graphics.drawable.IconCompat; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import androidx.slice.Slice; import androidx.slice.builders.ListBuilder; import androidx.slice.builders.SliceAction; import com.android.ims.ImsManager; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.slices.SettingsSliceProvider; import com.android.settings.slices.SliceBroadcastReceiver; import com.android.settings.slices.SliceBuilderUtils; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * Helper class to control slices for wifi calling settings. */ public class WifiCallingSliceHelper { private static final String TAG = "WifiCallingSliceHelper"; /** * Settings slice path to wifi calling setting. */ public static final String PATH_WIFI_CALLING = "wifi_calling"; /** * Action passed for changes to wifi calling slice (toggle). */ public static final String ACTION_WIFI_CALLING_CHANGED = "com.android.settings.wifi.calling.action.WIFI_CALLING_CHANGED"; /** * Action for Wifi calling Settings activity which * allows setting configuration for Wifi calling * related settings */ public static final String ACTION_WIFI_CALLING_SETTINGS_ACTIVITY = "android.settings.WIFI_CALLING_SETTINGS"; /** * Full {@link Uri} for the Wifi Calling Slice. */ public static final Uri WIFI_CALLING_URI = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(PATH_WIFI_CALLING) .build(); /** * Timeout for querying wifi calling setting from ims manager. */ private static final int TIMEOUT_MILLIS = 2000; protected SubscriptionManager mSubscriptionManager; private final Context mContext; @VisibleForTesting public WifiCallingSliceHelper(Context context) { mContext = context; } /** * Returns Slice object for wifi calling settings. * * If wifi calling is being turned on and if wifi calling activation is needed for the current * carrier, this method will return Slice with instructions to go to Settings App. * * If wifi calling is not supported for the current carrier, this method will return slice with * not supported message. * * If wifi calling setting can be changed, this method will return the slice to toggle wifi * calling option with ACTION_WIFI_CALLING_CHANGED as endItem. */ public Slice createWifiCallingSlice(Uri sliceUri) { final int subId = getDefaultVoiceSubId(); final String carrierName = getSimCarrierName(); if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) { Log.d(TAG, "Invalid subscription Id"); return getNonActionableWifiCallingSlice( mContext.getString(R.string.wifi_calling_settings_title), mContext.getString(R.string.wifi_calling_not_supported, carrierName), sliceUri, getSettingsIntent(mContext)); } final ImsManager imsManager = getImsManager(subId); if (!imsManager.isWfcEnabledByPlatform() || !imsManager.isWfcProvisionedOnDevice()) { Log.d(TAG, "Wifi calling is either not provisioned or not enabled by Platform"); return getNonActionableWifiCallingSlice( mContext.getString(R.string.wifi_calling_settings_title), mContext.getString(R.string.wifi_calling_not_supported, carrierName), sliceUri, getSettingsIntent(mContext)); } try { final boolean isWifiCallingEnabled = isWifiCallingEnabled(imsManager); final Intent activationAppIntent = getWifiCallingCarrierActivityIntent(subId); // Send this actionable wifi calling slice to toggle the setting // only when there is no need for wifi calling activation with the server if (activationAppIntent != null && !isWifiCallingEnabled) { Log.d(TAG, "Needs Activation"); // Activation needed for the next action of the user // Give instructions to go to settings app return getNonActionableWifiCallingSlice( mContext.getString(R.string.wifi_calling_settings_title), mContext.getString( R.string.wifi_calling_settings_activation_instructions), sliceUri, getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY)); } return getWifiCallingSlice(sliceUri, mContext, isWifiCallingEnabled); } catch (InterruptedException | TimeoutException | ExecutionException e) { Log.e(TAG, "Unable to read the current WiFi calling status", e); return getNonActionableWifiCallingSlice( mContext.getString(R.string.wifi_calling_settings_title), mContext.getString(R.string.wifi_calling_turn_on), sliceUri, getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY)); } } private boolean isWifiCallingEnabled(ImsManager imsManager) throws InterruptedException, ExecutionException, TimeoutException { final FutureTask isWifiOnTask = new FutureTask<>(new Callable() { @Override public Boolean call() { return imsManager.isWfcEnabledByUser(); } }); final ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(isWifiOnTask); Boolean isWifiEnabledByUser = false; isWifiEnabledByUser = isWifiOnTask.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); return isWifiEnabledByUser && imsManager.isNonTtyOrTtyOnVolteEnabled(); } /** * Builds a toggle slice where the intent takes you to the wifi calling page and the toggle * enables/disables wifi calling. */ private Slice getWifiCallingSlice(Uri sliceUri, Context mContext, boolean isWifiCallingEnabled) { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.wifi_signal); final String title = mContext.getString(R.string.wifi_calling_settings_title); return new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) .setColor(R.color.material_blue_500) .addRow(b -> b .setTitle(title) .addEndItem( new SliceAction( getBroadcastIntent(ACTION_WIFI_CALLING_CHANGED), null /* actionTitle */, isWifiCallingEnabled)) .setPrimaryAction(new SliceAction( getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY), icon, title))) .build(); } protected ImsManager getImsManager(int subId) { return ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(subId)); } private Integer getWfcMode(ImsManager imsManager) throws InterruptedException, ExecutionException, TimeoutException { FutureTask wfcModeTask = new FutureTask<>(new Callable() { @Override public Integer call() { return imsManager.getWfcMode(false); } }); ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(wfcModeTask); return wfcModeTask.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } /** * Handles wifi calling setting change from wifi calling slice and posts notification. Should be * called when intent action is ACTION_WIFI_CALLING_CHANGED. Executed in @WorkerThread * * @param intent action performed */ public void handleWifiCallingChanged(Intent intent) { final int subId = getDefaultVoiceSubId(); if (subId > SubscriptionManager.INVALID_SUBSCRIPTION_ID) { final ImsManager imsManager = getImsManager(subId); if (imsManager.isWfcEnabledByPlatform() || imsManager.isWfcProvisionedOnDevice()) { final boolean currentValue = imsManager.isWfcEnabledByUser() && imsManager.isNonTtyOrTtyOnVolteEnabled(); final boolean newValue = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, currentValue); final Intent activationAppIntent = getWifiCallingCarrierActivityIntent(subId); if (!newValue || activationAppIntent == null) { // If either the action is to turn off wifi calling setting // or there is no activation involved - Update the setting if (newValue != currentValue) { imsManager.setWfcSetting(newValue); } } } } // notify change in slice in any case to get re-queried. This would result in displaying // appropriate message with the updated setting. final Uri uri = SliceBuilderUtils.getUri(PATH_WIFI_CALLING, false /*isPlatformSlice*/); mContext.getContentResolver().notifyChange(uri, null); } /** * Returns Slice with the title and subtitle provided as arguments with wifi signal Icon. * * @param title Title of the slice * @param subtitle Subtitle of the slice * @param sliceUri slice uri * @return Slice with title and subtitle */ // TODO(b/79548264) asses different scenarios and return null instead of non-actionable slice private Slice getNonActionableWifiCallingSlice(String title, String subtitle, Uri sliceUri, PendingIntent primaryActionIntent) { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.wifi_signal); return new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) .setColor(R.color.material_blue_500) .addRow(b -> b .setTitle(title) .setSubtitle(subtitle) .setPrimaryAction(new SliceAction( primaryActionIntent, icon, title))) .build(); } /** * Returns {@code true} when the key is enabled for the carrier, and {@code false} otherwise. */ private boolean isCarrierConfigManagerKeyEnabled(Context mContext, String key, int subId, boolean defaultValue) { final CarrierConfigManager configManager = getCarrierConfigManager(mContext); boolean ret = false; if (configManager != null) { final PersistableBundle bundle = configManager.getConfigForSubId(subId); if (bundle != null) { ret = bundle.getBoolean(key, defaultValue); } } return ret; } protected CarrierConfigManager getCarrierConfigManager(Context mContext) { return mContext.getSystemService(CarrierConfigManager.class); } /** * Returns the current default voice subId obtained from SubscriptionManager */ protected int getDefaultVoiceSubId() { if (mSubscriptionManager == null) { mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); } return SubscriptionManager.getDefaultVoiceSubscriptionId(); } /** * Returns Intent of the activation app required to activate wifi calling or null if there is no * need for activation. */ protected Intent getWifiCallingCarrierActivityIntent(int subId) { final CarrierConfigManager configManager = getCarrierConfigManager(mContext); if (configManager == null) { return null; } final PersistableBundle bundle = configManager.getConfigForSubId(subId); if (bundle == null) { return null; } final String carrierApp = bundle.getString( CarrierConfigManager.KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING); if (TextUtils.isEmpty(carrierApp)) { return null; } final ComponentName componentName = ComponentName.unflattenFromString(carrierApp); if (componentName == null) { return null; } final Intent intent = new Intent(); intent.setComponent(componentName); return intent; } /** * @return {@link PendingIntent} to the Settings home page. */ public static PendingIntent getSettingsIntent(Context context) { final Intent intent = new Intent(Settings.ACTION_SETTINGS); return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */); } private PendingIntent getBroadcastIntent(String action) { final Intent intent = new Intent(action); intent.setClass(mContext, SliceBroadcastReceiver.class); return PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_CANCEL_CURRENT); } /** * Returns PendingIntent to start activity specified by action */ private PendingIntent getActivityIntent(String action) { final Intent intent = new Intent(action); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); } /** * Returns carrier id name of the current Subscription */ private String getSimCarrierName() { final TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); final CharSequence carrierName = telephonyManager.getSimCarrierIdName(); if (carrierName == null) { return mContext.getString(R.string.carrier); } return carrierName.toString(); } }