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