| /* |
| * Copyright (C) 2016 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.phone.vvm.omtp; |
| |
| import android.annotation.Nullable; |
| import android.annotation.WorkerThread; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.ContentObserver; |
| import android.os.Bundle; |
| import android.provider.Settings; |
| import android.provider.Settings.Global; |
| import android.telecom.PhoneAccountHandle; |
| import android.telephony.ServiceState; |
| import android.telephony.TelephonyManager; |
| import com.android.phone.Assert; |
| import com.android.phone.PhoneGlobals; |
| import com.android.phone.VoicemailStatus; |
| import com.android.phone.vvm.omtp.protocol.VisualVoicemailProtocol; |
| import com.android.phone.vvm.omtp.scheduling.BaseTask; |
| import com.android.phone.vvm.omtp.scheduling.RetryPolicy; |
| import com.android.phone.vvm.omtp.sms.StatusMessage; |
| import com.android.phone.vvm.omtp.sms.StatusSmsFetcher; |
| import com.android.phone.vvm.omtp.sync.OmtpVvmSourceManager; |
| import com.android.phone.vvm.omtp.sync.OmtpVvmSyncService; |
| import com.android.phone.vvm.omtp.sync.SyncTask; |
| import com.android.phone.vvm.omtp.utils.PhoneAccountHandleConverter; |
| import java.io.IOException; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.concurrent.CancellationException; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * Task to activate the visual voicemail service. A request to activate VVM will be sent to the |
| * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If |
| * the user is not provisioned provisioning will be attempted. Activation happens when the phone |
| * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier |
| * spontaneously sent a STATUS SMS. |
| */ |
| public class ActivationTask extends BaseTask { |
| |
| private static final String TAG = "VvmActivationTask"; |
| |
| private static final int RETRY_TIMES = 4; |
| private static final int RETRY_INTERVAL_MILLIS = 5_000; |
| |
| private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle"; |
| |
| @Nullable |
| private static DeviceProvisionedObserver sDeviceProvisionedObserver; |
| |
| private final RetryPolicy mRetryPolicy; |
| |
| private Bundle mMessageData; |
| |
| public ActivationTask() { |
| super(TASK_ACTIVATION); |
| mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS); |
| addPolicy(mRetryPolicy); |
| } |
| |
| /** |
| * Has the user gone through the setup wizard yet. |
| */ |
| private static boolean isDeviceProvisioned(Context context) { |
| return Settings.Global.getInt( |
| context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) == 1; |
| } |
| |
| /** |
| * @param messageData The optional bundle from {@link android.provider.VoicemailContract# |
| * EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task will |
| * request a status SMS itself. |
| */ |
| public static void start(Context context, int subId, @Nullable Bundle messageData) { |
| if (!isDeviceProvisioned(context)) { |
| VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing"); |
| // Activation might need information such as system language to be set, so wait until |
| // the setup wizard is finished. The data bundle from the SMS will be re-requested upon |
| // activation. |
| queueActivationAfterProvisioned(context, subId); |
| return; |
| } |
| |
| Intent intent = BaseTask.createIntent(context, ActivationTask.class, subId); |
| if (messageData != null) { |
| intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData); |
| } |
| context.startService(intent); |
| } |
| |
| public void onCreate(Context context, Intent intent, int flags, int startId) { |
| super.onCreate(context, intent, flags, startId); |
| mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE); |
| } |
| |
| @Override |
| public Intent createRestartIntent() { |
| Intent intent = super.createRestartIntent(); |
| // mMessageData is discarded, request a fresh STATUS SMS for retries. |
| return intent; |
| } |
| |
| @Override |
| @WorkerThread |
| public void onExecuteInBackgroundThread() { |
| Assert.isNotMainThread(); |
| int subId = getSubId(); |
| |
| PhoneAccountHandle phoneAccountHandle = PhoneAccountHandleConverter.fromSubId(subId); |
| if (phoneAccountHandle == null) { |
| // This should never happen |
| VvmLog.e(TAG, "null phone account for subId " + subId); |
| return; |
| } |
| |
| OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(getContext(), subId); |
| if (!helper.isValid()) { |
| VvmLog.i(TAG, "VVM not supported on subId " + subId); |
| OmtpVvmSourceManager.getInstance(getContext()).removeSource(phoneAccountHandle); |
| return; |
| } |
| |
| // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm |
| // content provider URI which we will use. On some occasions, setting that URI will |
| // fail, so we will perform a few attempts to ensure that the vvm content provider has |
| // a good chance of being started up. |
| if (!VoicemailStatus.edit(getContext(), phoneAccountHandle) |
| .setType(helper.getVvmType()) |
| .apply()) { |
| VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType()); |
| fail(); |
| } |
| VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType()); |
| |
| if (!OmtpVvmSourceManager.getInstance(getContext()) |
| .isVvmSourceRegistered(phoneAccountHandle)) { |
| // This account has not been activated before during the lifetime of this boot. |
| VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(), |
| phoneAccountHandle); |
| if (preferences.getString(OmtpConstants.SERVER_ADDRESS, null) == null) { |
| // Only show the "activating" message if activation has not been completed before. |
| // Subsequent activations are more of a status check and usually does not |
| // concern the user. |
| helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), |
| OmtpEvents.CONFIG_ACTIVATING); |
| } else { |
| // The account has been activated on this device before. Pretend it is already |
| // activated. If there are any activation error it will overwrite this status. |
| helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), |
| OmtpEvents.CONFIG_ACTIVATING_SUBSEQUENT); |
| } |
| |
| } |
| if (!hasSignal(getContext(), subId)) { |
| VvmLog.i(TAG, "Service lost during activation, aborting"); |
| // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING |
| // event. |
| helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), |
| OmtpEvents.NOTIFICATION_SERVICE_LOST); |
| // Don't retry, a new activation will be started after the signal returned. |
| return; |
| } |
| |
| helper.activateSmsFilter(); |
| VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor(); |
| |
| VisualVoicemailProtocol protocol = helper.getProtocol(); |
| |
| Bundle data; |
| if (mMessageData != null) { |
| // The content of STATUS SMS is provided to launch this task, no need to request it |
| // again. |
| data = mMessageData; |
| } else { |
| try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(), subId)) { |
| protocol.startActivation(helper, fetcher.getSentIntent()); |
| // Both the fetcher and OmtpMessageReceiver will be triggered, but |
| // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be |
| // rejected because the task is still running. |
| data = fetcher.get(); |
| } catch (TimeoutException e) { |
| // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS |
| // handleEvent() will do the logging. |
| helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT); |
| fail(); |
| return; |
| } catch (CancellationException e) { |
| VvmLog.e(TAG, "Unable to send status request SMS"); |
| fail(); |
| return; |
| } catch (InterruptedException | ExecutionException | IOException e) { |
| VvmLog.e(TAG, "can't get future STATUS SMS", e); |
| fail(); |
| return; |
| } |
| } |
| |
| StatusMessage message = new StatusMessage(data); |
| VvmLog.d(TAG, "STATUS SMS received: st=" + message.getProvisioningStatus() |
| + ", rc=" + message.getReturnCode()); |
| |
| if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) { |
| VvmLog.d(TAG, "subscriber ready, no activation required"); |
| updateSource(getContext(), phoneAccountHandle, getSubId(), status, message); |
| } else { |
| if (helper.supportsProvisioning()) { |
| VvmLog.i(TAG, "Subscriber not ready, start provisioning"); |
| helper.startProvisioning(this, phoneAccountHandle, status, message, data); |
| |
| } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) { |
| VvmLog.i(TAG, "Subscriber new but provisioning is not supported"); |
| // Ignore the non-ready state and attempt to use the provided info as is. |
| // This is probably caused by not completing the new user tutorial. |
| updateSource(getContext(), phoneAccountHandle, getSubId(), status, message); |
| } else { |
| VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported"); |
| helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE); |
| PhoneGlobals.getInstance().setShouldCheckVisualVoicemailConfigurationForMwi(subId, false); |
| } |
| } |
| } |
| |
| public static void updateSource(Context context, PhoneAccountHandle phone, int subId, |
| VoicemailStatus.Editor status, StatusMessage message) { |
| OmtpVvmSourceManager vvmSourceManager = |
| OmtpVvmSourceManager.getInstance(context); |
| |
| if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) { |
| OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, subId); |
| helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS); |
| |
| // Save the IMAP credentials in preferences so they are persistent and can be retrieved. |
| VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phone); |
| message.putStatus(prefs.edit()).apply(); |
| |
| // Add the source to indicate that it is active. |
| vvmSourceManager.addSource(phone); |
| |
| SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC); |
| // Remove the message waiting indicator, which is a sticky notification for traditional |
| // voicemails. |
| PhoneGlobals.getInstance() |
| .setShouldCheckVisualVoicemailConfigurationForMwi(subId, true); |
| PhoneGlobals.getInstance().clearMwiIndicator(subId); |
| } else { |
| VvmLog.e(TAG, "Visual voicemail not available for subscriber."); |
| } |
| } |
| |
| private static boolean hasSignal(Context context, int subId) { |
| return context.getSystemService(TelephonyManager.class) |
| .getServiceStateForSubscriber(subId).getState() == ServiceState.STATE_IN_SERVICE; |
| } |
| |
| private static void queueActivationAfterProvisioned(Context context, int subId) { |
| if (sDeviceProvisionedObserver == null) { |
| sDeviceProvisionedObserver = new DeviceProvisionedObserver(context); |
| context.getContentResolver() |
| .registerContentObserver(Settings.Global.getUriFor(Global.DEVICE_PROVISIONED), |
| false, sDeviceProvisionedObserver); |
| } |
| sDeviceProvisionedObserver.addSubId(subId); |
| } |
| |
| private static class DeviceProvisionedObserver extends ContentObserver { |
| |
| private final Context mContext; |
| private final Set<Integer> mSubIds = new HashSet<>(); |
| |
| private DeviceProvisionedObserver(Context context) { |
| super(null); |
| mContext = context; |
| } |
| |
| public void addSubId(int subId) { |
| mSubIds.add(subId); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| if (isDeviceProvisioned(mContext)) { |
| VvmLog.i(TAG, "device provisioned, resuming activation"); |
| for (int subId : mSubIds) { |
| start(mContext, subId, null); |
| } |
| mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver); |
| sDeviceProvisionedObserver = null; |
| } |
| } |
| } |
| } |