Eric Erfanian | ccca315 | 2017-02-22 16:32:36 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2016 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.voicemailomtp; |
| 18 | |
| 19 | import android.annotation.TargetApi; |
| 20 | import android.content.Context; |
| 21 | import android.content.Intent; |
| 22 | import android.database.ContentObserver; |
| 23 | import android.os.Build.VERSION_CODES; |
| 24 | import android.os.Bundle; |
| 25 | import android.provider.Settings; |
| 26 | import android.provider.Settings.Global; |
| 27 | import android.support.annotation.Nullable; |
| 28 | import android.support.annotation.WorkerThread; |
| 29 | import android.telecom.PhoneAccountHandle; |
| 30 | import android.telephony.ServiceState; |
| 31 | import android.telephony.TelephonyManager; |
| 32 | import com.android.voicemailomtp.protocol.VisualVoicemailProtocol; |
| 33 | import com.android.voicemailomtp.scheduling.BaseTask; |
| 34 | import com.android.voicemailomtp.scheduling.RetryPolicy; |
| 35 | import com.android.voicemailomtp.sms.StatusMessage; |
| 36 | import com.android.voicemailomtp.sms.StatusSmsFetcher; |
| 37 | import com.android.voicemailomtp.sync.OmtpVvmSourceManager; |
| 38 | import com.android.voicemailomtp.sync.OmtpVvmSyncService; |
| 39 | import com.android.voicemailomtp.sync.SyncTask; |
| 40 | import java.io.IOException; |
| 41 | import java.util.HashSet; |
| 42 | import java.util.Set; |
| 43 | import java.util.concurrent.CancellationException; |
| 44 | import java.util.concurrent.ExecutionException; |
| 45 | import java.util.concurrent.TimeoutException; |
| 46 | |
| 47 | /** |
| 48 | * Task to activate the visual voicemail service. A request to activate VVM will be sent to the |
| 49 | * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If |
| 50 | * the user is not provisioned provisioning will be attempted. Activation happens when the phone |
| 51 | * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier |
| 52 | * spontaneously sent a STATUS SMS. |
| 53 | */ |
| 54 | @TargetApi(VERSION_CODES.CUR_DEVELOPMENT) |
| 55 | public class ActivationTask extends BaseTask { |
| 56 | |
| 57 | private static final String TAG = "VvmActivationTask"; |
| 58 | |
| 59 | private static final int RETRY_TIMES = 4; |
| 60 | private static final int RETRY_INTERVAL_MILLIS = 5_000; |
| 61 | |
| 62 | private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle"; |
| 63 | |
| 64 | @Nullable |
| 65 | private static DeviceProvisionedObserver sDeviceProvisionedObserver; |
| 66 | |
| 67 | private final RetryPolicy mRetryPolicy; |
| 68 | |
| 69 | private Bundle mMessageData; |
| 70 | |
| 71 | public ActivationTask() { |
| 72 | super(TASK_ACTIVATION); |
| 73 | mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS); |
| 74 | addPolicy(mRetryPolicy); |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Has the user gone through the setup wizard yet. |
| 79 | */ |
| 80 | private static boolean isDeviceProvisioned(Context context) { |
| 81 | return Settings.Global.getInt( |
| 82 | context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) == 1; |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * @param messageData The optional bundle from {@link android.provider.VoicemailContract# |
| 87 | * EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task will |
| 88 | * request a status SMS itself. |
| 89 | */ |
| 90 | public static void start(Context context, PhoneAccountHandle phoneAccountHandle, |
| 91 | @Nullable Bundle messageData) { |
| 92 | if (!isDeviceProvisioned(context)) { |
| 93 | VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing"); |
| 94 | // Activation might need information such as system language to be set, so wait until |
| 95 | // the setup wizard is finished. The data bundle from the SMS will be re-requested upon |
| 96 | // activation. |
| 97 | queueActivationAfterProvisioned(context, phoneAccountHandle); |
| 98 | return; |
| 99 | } |
| 100 | |
| 101 | Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle); |
| 102 | if (messageData != null) { |
| 103 | intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData); |
| 104 | } |
| 105 | context.startService(intent); |
| 106 | } |
| 107 | |
| 108 | public void onCreate(Context context, Intent intent, int flags, int startId) { |
| 109 | super.onCreate(context, intent, flags, startId); |
| 110 | mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE); |
| 111 | } |
| 112 | |
| 113 | @Override |
| 114 | public Intent createRestartIntent() { |
| 115 | Intent intent = super.createRestartIntent(); |
| 116 | // mMessageData is discarded, request a fresh STATUS SMS for retries. |
| 117 | return intent; |
| 118 | } |
| 119 | |
| 120 | @Override |
| 121 | @WorkerThread |
| 122 | public void onExecuteInBackgroundThread() { |
| 123 | Assert.isNotMainThread(); |
| 124 | |
| 125 | PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle(); |
| 126 | if (phoneAccountHandle == null) { |
| 127 | // This should never happen |
| 128 | VvmLog.e(TAG, "null PhoneAccountHandle"); |
| 129 | return; |
| 130 | } |
| 131 | |
| 132 | OmtpVvmCarrierConfigHelper helper = |
| 133 | new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle); |
| 134 | if (!helper.isValid()) { |
| 135 | VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle); |
| 136 | VoicemailStatus.disable(getContext(), phoneAccountHandle); |
| 137 | return; |
| 138 | } |
| 139 | |
| 140 | // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm |
| 141 | // content provider URI which we will use. On some occasions, setting that URI will |
| 142 | // fail, so we will perform a few attempts to ensure that the vvm content provider has |
| 143 | // a good chance of being started up. |
| 144 | if (!VoicemailStatus.edit(getContext(), phoneAccountHandle) |
| 145 | .setType(helper.getVvmType()) |
| 146 | .apply()) { |
| 147 | VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType()); |
| 148 | fail(); |
| 149 | } |
| 150 | VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType()); |
| 151 | |
| 152 | if (!OmtpVvmSourceManager.getInstance(getContext()) |
| 153 | .isVvmSourceRegistered(phoneAccountHandle)) { |
| 154 | // This account has not been activated before during the lifetime of this boot. |
| 155 | VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(), |
| 156 | phoneAccountHandle); |
| 157 | if (preferences.getString(OmtpConstants.SERVER_ADDRESS, null) == null) { |
| 158 | // Only show the "activating" message if activation has not been completed before. |
| 159 | // Subsequent activations are more of a status check and usually does not |
| 160 | // concern the user. |
| 161 | helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), |
| 162 | OmtpEvents.CONFIG_ACTIVATING); |
| 163 | } else { |
| 164 | // The account has been activated on this device before. Pretend it is already |
| 165 | // activated. If there are any activation error it will overwrite this status. |
| 166 | helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), |
| 167 | OmtpEvents.CONFIG_ACTIVATING_SUBSEQUENT); |
| 168 | } |
| 169 | |
| 170 | } |
| 171 | if (!hasSignal(getContext(), phoneAccountHandle)) { |
| 172 | VvmLog.i(TAG, "Service lost during activation, aborting"); |
| 173 | // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING |
| 174 | // event. |
| 175 | helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), |
| 176 | OmtpEvents.NOTIFICATION_SERVICE_LOST); |
| 177 | // Don't retry, a new activation will be started after the signal returned. |
| 178 | return; |
| 179 | } |
| 180 | |
| 181 | helper.activateSmsFilter(); |
| 182 | VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor(); |
| 183 | |
| 184 | VisualVoicemailProtocol protocol = helper.getProtocol(); |
| 185 | |
| 186 | Bundle data; |
| 187 | if (mMessageData != null) { |
| 188 | // The content of STATUS SMS is provided to launch this task, no need to request it |
| 189 | // again. |
| 190 | data = mMessageData; |
| 191 | } else { |
| 192 | try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(), |
| 193 | phoneAccountHandle)) { |
| 194 | protocol.startActivation(helper, fetcher.getSentIntent()); |
| 195 | // Both the fetcher and OmtpMessageReceiver will be triggered, but |
| 196 | // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be |
| 197 | // rejected because the task is still running. |
| 198 | data = fetcher.get(); |
| 199 | } catch (TimeoutException e) { |
| 200 | // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS |
| 201 | // handleEvent() will do the logging. |
| 202 | helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT); |
| 203 | fail(); |
| 204 | return; |
| 205 | } catch (CancellationException e) { |
| 206 | VvmLog.e(TAG, "Unable to send status request SMS"); |
| 207 | fail(); |
| 208 | return; |
| 209 | } catch (InterruptedException | ExecutionException | IOException e) { |
| 210 | VvmLog.e(TAG, "can't get future STATUS SMS", e); |
| 211 | fail(); |
| 212 | return; |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | StatusMessage message = new StatusMessage(data); |
| 217 | VvmLog.d(TAG, "STATUS SMS received: st=" + message.getProvisioningStatus() |
| 218 | + ", rc=" + message.getReturnCode()); |
| 219 | |
| 220 | if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) { |
| 221 | VvmLog.d(TAG, "subscriber ready, no activation required"); |
| 222 | updateSource(getContext(), phoneAccountHandle, status, message); |
| 223 | } else { |
| 224 | if (helper.supportsProvisioning()) { |
| 225 | VvmLog.i(TAG, "Subscriber not ready, start provisioning"); |
| 226 | helper.startProvisioning(this, phoneAccountHandle, status, message, data); |
| 227 | |
| 228 | } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) { |
| 229 | VvmLog.i(TAG, "Subscriber new but provisioning is not supported"); |
| 230 | // Ignore the non-ready state and attempt to use the provided info as is. |
| 231 | // This is probably caused by not completing the new user tutorial. |
| 232 | updateSource(getContext(), phoneAccountHandle, status, message); |
| 233 | } else { |
| 234 | VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported"); |
| 235 | helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE); |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | public static void updateSource(Context context, PhoneAccountHandle phone, |
| 241 | VoicemailStatus.Editor status, StatusMessage message) { |
| 242 | OmtpVvmSourceManager vvmSourceManager = |
| 243 | OmtpVvmSourceManager.getInstance(context); |
| 244 | |
| 245 | if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) { |
| 246 | OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone); |
| 247 | helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS); |
| 248 | |
| 249 | // Save the IMAP credentials in preferences so they are persistent and can be retrieved. |
| 250 | VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phone); |
| 251 | message.putStatus(prefs.edit()).apply(); |
| 252 | |
| 253 | // Add the source to indicate that it is active. |
| 254 | vvmSourceManager.addSource(phone); |
| 255 | |
| 256 | SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC); |
| 257 | } else { |
| 258 | VvmLog.e(TAG, "Visual voicemail not available for subscriber."); |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) { |
| 263 | TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class) |
| 264 | .createForPhoneAccountHandle(phoneAccountHandle); |
| 265 | return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE; |
| 266 | } |
| 267 | |
| 268 | private static void queueActivationAfterProvisioned(Context context, |
| 269 | PhoneAccountHandle phoneAccountHandle) { |
| 270 | if (sDeviceProvisionedObserver == null) { |
| 271 | sDeviceProvisionedObserver = new DeviceProvisionedObserver(context); |
| 272 | context.getContentResolver() |
| 273 | .registerContentObserver(Settings.Global.getUriFor(Global.DEVICE_PROVISIONED), |
| 274 | false, sDeviceProvisionedObserver); |
| 275 | } |
| 276 | sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle); |
| 277 | } |
| 278 | |
| 279 | private static class DeviceProvisionedObserver extends ContentObserver { |
| 280 | |
| 281 | private final Context mContext; |
| 282 | private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>(); |
| 283 | |
| 284 | private DeviceProvisionedObserver(Context context) { |
| 285 | super(null); |
| 286 | mContext = context; |
| 287 | } |
| 288 | |
| 289 | public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) { |
| 290 | mPhoneAccountHandles.add(phoneAccountHandle); |
| 291 | } |
| 292 | |
| 293 | @Override |
| 294 | public void onChange(boolean selfChange) { |
| 295 | if (isDeviceProvisioned(mContext)) { |
| 296 | VvmLog.i(TAG, "device provisioned, resuming activation"); |
| 297 | for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) { |
| 298 | start(mContext, phoneAccountHandle, null); |
| 299 | } |
| 300 | mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver); |
| 301 | sDeviceProvisionedObserver = null; |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | } |