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