From d5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9 Mon Sep 17 00:00:00 2001 From: Eric Erfanian Date: Wed, 15 Mar 2017 14:41:07 -0700 Subject: Update Dialer source from latest green build. * Refactor voicemail component * Add new enriched calling components Test: treehugger, manual aosp testing Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942 --- .../voicemail/impl/sync/OmtpVvmSyncReceiver.java | 54 ++++ .../voicemail/impl/sync/OmtpVvmSyncService.java | 340 +++++++++++++++++++++ .../android/voicemail/impl/sync/SyncOneTask.java | 78 +++++ java/com/android/voicemail/impl/sync/SyncTask.java | 75 +++++ .../android/voicemail/impl/sync/UploadTask.java | 69 +++++ .../impl/sync/VoicemailProviderChangeReceiver.java | 40 +++ .../impl/sync/VoicemailStatusQueryHelper.java | 113 +++++++ .../voicemail/impl/sync/VoicemailsQueryHelper.java | 295 ++++++++++++++++++ .../voicemail/impl/sync/VvmAccountManager.java | 79 +++++ .../voicemail/impl/sync/VvmNetworkRequest.java | 120 ++++++++ .../impl/sync/VvmNetworkRequestCallback.java | 183 +++++++++++ 11 files changed, 1446 insertions(+) create mode 100644 java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java create mode 100644 java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java create mode 100644 java/com/android/voicemail/impl/sync/SyncOneTask.java create mode 100644 java/com/android/voicemail/impl/sync/SyncTask.java create mode 100644 java/com/android/voicemail/impl/sync/UploadTask.java create mode 100644 java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java create mode 100644 java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java create mode 100644 java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java create mode 100644 java/com/android/voicemail/impl/sync/VvmAccountManager.java create mode 100644 java/com/android/voicemail/impl/sync/VvmNetworkRequest.java create mode 100644 java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java (limited to 'java/com/android/voicemail/impl/sync') diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java new file mode 100644 index 000000000..5a2fe146e --- /dev/null +++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java @@ -0,0 +1,54 @@ +/* + * 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.voicemail.impl.sync; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.provider.VoicemailContract; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import com.android.voicemail.impl.ActivationTask; +import com.android.voicemail.impl.VvmLog; +import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil; +import java.util.List; + +public class OmtpVvmSyncReceiver extends BroadcastReceiver { + + private static final String TAG = "OmtpVvmSyncReceiver"; + + @Override + public void onReceive(final Context context, Intent intent) { + if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) { + VvmLog.v(TAG, "Sync intent received"); + + List accounts = + context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts(); + for (PhoneAccountHandle phoneAccount : accounts) { + if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) { + continue; + } + if (!VvmAccountManager.isAccountActivated(context, phoneAccount)) { + VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating"); + ActivationTask.start(context, phoneAccount, null); + } else { + SyncTask.start(context, phoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC); + } + } + } + } +} diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java new file mode 100644 index 000000000..c255019fc --- /dev/null +++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2015 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.voicemail.impl.sync; + +import android.annotation.TargetApi; +import android.content.Context; +import android.net.Network; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.support.v4.os.BuildCompat; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.ArrayMap; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.voicemail.impl.ActivationTask; +import com.android.voicemail.impl.Assert; +import com.android.voicemail.impl.OmtpEvents; +import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper; +import com.android.voicemail.impl.Voicemail; +import com.android.voicemail.impl.VoicemailStatus; +import com.android.voicemail.impl.VvmLog; +import com.android.voicemail.impl.fetch.VoicemailFetchedCallback; +import com.android.voicemail.impl.imap.ImapHelper; +import com.android.voicemail.impl.imap.ImapHelper.InitializingException; +import com.android.voicemail.impl.scheduling.BaseTask; +import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil; +import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper; +import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException; +import com.android.voicemail.impl.utils.VoicemailDatabaseUtil; +import java.util.List; +import java.util.Map; + +/** Sync OMTP visual voicemail. */ +@TargetApi(VERSION_CODES.O) +public class OmtpVvmSyncService { + + private static final String TAG = OmtpVvmSyncService.class.getSimpleName(); + + /** Signifies a sync with both uploading to the server and downloading from the server. */ + public static final String SYNC_FULL_SYNC = "full_sync"; + /** Only upload to the server. */ + public static final String SYNC_UPLOAD_ONLY = "upload_only"; + /** Only download from the server. */ + public static final String SYNC_DOWNLOAD_ONLY = "download_only"; + /** Only download single voicemail transcription. */ + public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = "download_one_transcription"; + /** Threshold for whether we should archive and delete voicemails from the remote VM server. */ + private static final float AUTO_DELETE_ARCHIVE_VM_THRESHOLD = 0.75f; + + private final Context mContext; + + private VoicemailsQueryHelper mQueryHelper; + + public OmtpVvmSyncService(Context context) { + mContext = context; + mQueryHelper = new VoicemailsQueryHelper(mContext); + } + + public void sync( + BaseTask task, + String action, + PhoneAccountHandle phoneAccount, + Voicemail voicemail, + VoicemailStatus.Editor status) { + Assert.isTrue(phoneAccount != null); + VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount); + setupAndSendRequest(task, phoneAccount, voicemail, action, status); + } + + private void setupAndSendRequest( + BaseTask task, + PhoneAccountHandle phoneAccount, + Voicemail voicemail, + String action, + VoicemailStatus.Editor status) { + if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) { + VvmLog.v(TAG, "Sync requested for disabled account"); + return; + } + if (!VvmAccountManager.isAccountActivated(mContext, phoneAccount)) { + ActivationTask.start(mContext, phoneAccount, null); + return; + } + + OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount); + // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data + // channel errors, which should happen when the task starts, not when it ends. It is the + // "Sync in progress..." status. + config.handleEvent( + VoicemailStatus.edit(mContext, phoneAccount), OmtpEvents.DATA_IMAP_OPERATION_STARTED); + try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) { + if (network == null) { + VvmLog.e(TAG, "unable to acquire network"); + task.fail(); + return; + } + doSync(task, network.get(), phoneAccount, voicemail, action, status); + } catch (RequestFailedException e) { + config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); + task.fail(); + } + } + + private void doSync( + BaseTask task, + Network network, + PhoneAccountHandle phoneAccount, + Voicemail voicemail, + String action, + VoicemailStatus.Editor status) { + try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) { + boolean success; + if (voicemail == null) { + success = syncAll(action, imapHelper, phoneAccount); + } else { + success = syncOne(imapHelper, voicemail, phoneAccount); + } + if (success) { + // TODO: b/30569269 failure should interrupt all subsequent task via exceptions + imapHelper.updateQuota(); + autoDeleteAndArchiveVM(imapHelper, phoneAccount); + imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED); + } else { + task.fail(); + } + } catch (InitializingException e) { + VvmLog.w(TAG, "Can't retrieve Imap credentials.", e); + return; + } + } + + /** + * If the VM quota exceeds {@value AUTO_DELETE_ARCHIVE_VM_THRESHOLD}, we should archive the VMs + * and delete them from the server to ensure new VMs can be received. + */ + private void autoDeleteAndArchiveVM( + ImapHelper imapHelper, PhoneAccountHandle phoneAccountHandle) { + + if (ConfigProviderBindings.get(mContext) + .getBoolean(VisualVoicemailSettingsUtil.ALLOW_VOICEMAIL_ARCHIVE, true) + && isArchiveEnabled(mContext, phoneAccountHandle)) { + if ((float) imapHelper.getOccuupiedQuota() / (float) imapHelper.getTotalQuota() + > AUTO_DELETE_ARCHIVE_VM_THRESHOLD) { + deleteAndArchiveVM(imapHelper); + imapHelper.updateQuota(); + Logger.get(mContext) + .logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER); + } else { + VvmLog.i(TAG, "no need to archive and auto delete VM, quota below threshold"); + } + } else { + VvmLog.i(TAG, "autoDeleteAndArchiveVM is turned off"); + Logger.get(mContext).logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF); + } + } + + private static boolean isArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) { + return VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle) + && VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle); + } + + private void deleteAndArchiveVM(ImapHelper imapHelper) { + // Archive column should only be used for 0 and above + Assert.isTrue(BuildCompat.isAtLeastO()); + // The number of voicemails that exceed our threshold and should be deleted from the server + int numVoicemails = + imapHelper.getOccuupiedQuota() + - ((int) AUTO_DELETE_ARCHIVE_VM_THRESHOLD * imapHelper.getTotalQuota()); + List oldestVoicemails = mQueryHelper.oldestVoicemailsOnServer(numVoicemails); + if (!oldestVoicemails.isEmpty()) { + mQueryHelper.markArchivedInDatabase(oldestVoicemails); + imapHelper.markMessagesAsDeleted(oldestVoicemails); + VvmLog.i( + TAG, + String.format( + "successfully archived and deleted %d voicemails", oldestVoicemails.size())); + } else { + VvmLog.w(TAG, "remote voicemail server is empty"); + } + } + + private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) { + boolean uploadSuccess = true; + boolean downloadSuccess = true; + + if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) { + uploadSuccess = upload(imapHelper); + } + if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) { + downloadSuccess = download(imapHelper, account); + } + + VvmLog.v( + TAG, + "upload succeeded: [" + + String.valueOf(uploadSuccess) + + "] download succeeded: [" + + String.valueOf(downloadSuccess) + + "]"); + + return uploadSuccess && downloadSuccess; + } + + private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account) { + if (shouldPerformPrefetch(account, imapHelper)) { + VoicemailFetchedCallback callback = + new VoicemailFetchedCallback(mContext, voicemail.getUri(), account); + imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData()); + } + + return imapHelper.fetchTranscription( + new TranscriptionFetchedCallback(mContext, voicemail), voicemail.getSourceData()); + } + + private boolean upload(ImapHelper imapHelper) { + List readVoicemails = mQueryHelper.getReadVoicemails(); + List deletedVoicemails = mQueryHelper.getDeletedVoicemails(); + + boolean success = true; + + if (deletedVoicemails.size() > 0) { + if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) { + // We want to delete selectively instead of all the voicemails for this provider + // in case the state changed since the IMAP query was completed. + mQueryHelper.deleteFromDatabase(deletedVoicemails); + } else { + success = false; + } + } + + if (readVoicemails.size() > 0) { + if (imapHelper.markMessagesAsRead(readVoicemails)) { + mQueryHelper.markCleanInDatabase(readVoicemails); + } else { + success = false; + } + } + + return success; + } + + private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) { + List serverVoicemails = imapHelper.fetchAllVoicemails(); + List localVoicemails = mQueryHelper.getAllVoicemails(); + + if (localVoicemails == null || serverVoicemails == null) { + // Null value means the query failed. + return false; + } + + Map remoteMap = buildMap(serverVoicemails); + + // Go through all the local voicemails and check if they are on the server. + // They may be read or deleted on the server but not locally. Perform the + // appropriate local operation if the status differs from the server. Remove + // the messages that exist both locally and on the server to know which server + // messages to insert locally. + // Voicemails that were removed automatically from the server, are marked as + // archived and are stored locally. We do not delete them, as they were removed from the server + // by design (to make space). + for (int i = 0; i < localVoicemails.size(); i++) { + Voicemail localVoicemail = localVoicemails.get(i); + Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData()); + + // Do not delete voicemails that are archived marked as archived. + if (remoteVoicemail == null) { + mQueryHelper.deleteNonArchivedFromDatabase(localVoicemail); + } else { + if (remoteVoicemail.isRead() != localVoicemail.isRead()) { + mQueryHelper.markReadInDatabase(localVoicemail); + } + + if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) + && TextUtils.isEmpty(localVoicemail.getTranscription())) { + mQueryHelper.updateWithTranscription(localVoicemail, remoteVoicemail.getTranscription()); + } + } + } + + // The leftover messages are messages that exist on the server but not locally. + boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper); + for (Voicemail remoteVoicemail : remoteMap.values()) { + Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail); + if (prefetchEnabled) { + VoicemailFetchedCallback fetchedCallback = + new VoicemailFetchedCallback(mContext, uri, account); + imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData()); + } + } + + return true; + } + + private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) { + OmtpVvmCarrierConfigHelper carrierConfigHelper = + new OmtpVvmCarrierConfigHelper(mContext, account); + return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming(); + } + + /** Builds a map from provider data to message for the given collection of voicemails. */ + private Map buildMap(List messages) { + Map map = new ArrayMap(); + for (Voicemail message : messages) { + map.put(message.getSourceData(), message); + } + return map; + } + + /** Callback for {@link ImapHelper#fetchTranscription(TranscriptionFetchedCallback, String)} */ + public static class TranscriptionFetchedCallback { + + private Context mContext; + private Voicemail mVoicemail; + + public TranscriptionFetchedCallback(Context context, Voicemail voicemail) { + mContext = context; + mVoicemail = voicemail; + } + + public void setVoicemailTranscription(String transcription) { + VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext); + queryHelper.updateWithTranscription(mVoicemail, transcription); + } + } +} diff --git a/java/com/android/voicemail/impl/sync/SyncOneTask.java b/java/com/android/voicemail/impl/sync/SyncOneTask.java new file mode 100644 index 000000000..f9701506d --- /dev/null +++ b/java/com/android/voicemail/impl/sync/SyncOneTask.java @@ -0,0 +1,78 @@ +/* + * 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.voicemail.impl.sync; + +import android.content.Context; +import android.content.Intent; +import android.telecom.PhoneAccountHandle; +import com.android.voicemail.impl.Voicemail; +import com.android.voicemail.impl.VoicemailStatus; +import com.android.voicemail.impl.scheduling.BaseTask; +import com.android.voicemail.impl.scheduling.RetryPolicy; + +/** + * Task to download a single voicemail from the server. This task is initiated by a SMS notifying + * the new voicemail arrival, and ignores the duplicated tasks constraint. + */ +public class SyncOneTask extends BaseTask { + + private static final int RETRY_TIMES = 2; + private static final int RETRY_INTERVAL_MILLIS = 5_000; + + private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle"; + private static final String EXTRA_SYNC_TYPE = "extra_sync_type"; + private static final String EXTRA_VOICEMAIL = "extra_voicemail"; + + private PhoneAccountHandle mPhone; + private String mSyncType; + private Voicemail mVoicemail; + + public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) { + Intent intent = BaseTask.createIntent(context, SyncOneTask.class, phone); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone); + intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION); + intent.putExtra(EXTRA_VOICEMAIL, voicemail); + context.startService(intent); + } + + public SyncOneTask() { + super(TASK_ALLOW_DUPLICATES); + addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS)); + } + + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); + mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE); + mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL); + } + + @Override + public void onExecuteInBackgroundThread() { + OmtpVvmSyncService service = new OmtpVvmSyncService(getContext()); + service.sync(this, mSyncType, mPhone, mVoicemail, VoicemailStatus.edit(getContext(), mPhone)); + } + + @Override + public Intent createRestartIntent() { + Intent intent = super.createRestartIntent(); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone); + intent.putExtra(EXTRA_SYNC_TYPE, mSyncType); + intent.putExtra(EXTRA_VOICEMAIL, mVoicemail); + return intent; + } +} diff --git a/java/com/android/voicemail/impl/sync/SyncTask.java b/java/com/android/voicemail/impl/sync/SyncTask.java new file mode 100644 index 000000000..71c98412b --- /dev/null +++ b/java/com/android/voicemail/impl/sync/SyncTask.java @@ -0,0 +1,75 @@ +/* + * 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.voicemail.impl.sync; + +import android.content.Context; +import android.content.Intent; +import android.telecom.PhoneAccountHandle; +import com.android.voicemail.impl.scheduling.BaseTask; +import com.android.voicemail.impl.scheduling.MinimalIntervalPolicy; +import com.android.voicemail.impl.scheduling.RetryPolicy; + +/** System initiated sync request. */ +public class SyncTask extends BaseTask { + + // Try sync for a total of 5 times, should take around 5 minutes before finally giving up. + private static final int RETRY_TIMES = 4; + private static final int RETRY_INTERVAL_MILLIS = 5_000; + private static final int MINIMAL_INTERVAL_MILLIS = 60_000; + + private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle"; + private static final String EXTRA_SYNC_TYPE = "extra_sync_type"; + + private final RetryPolicy mRetryPolicy; + + private PhoneAccountHandle mPhone; + private String mSyncType; + + public static void start(Context context, PhoneAccountHandle phone, String syncType) { + Intent intent = BaseTask.createIntent(context, SyncTask.class, phone); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone); + intent.putExtra(EXTRA_SYNC_TYPE, syncType); + context.startService(intent); + } + + public SyncTask() { + super(TASK_SYNC); + mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS); + addPolicy(mRetryPolicy); + addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS)); + } + + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); + mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE); + } + + @Override + public void onExecuteInBackgroundThread() { + OmtpVvmSyncService service = new OmtpVvmSyncService(getContext()); + service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor()); + } + + @Override + public Intent createRestartIntent() { + Intent intent = super.createRestartIntent(); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone); + intent.putExtra(EXTRA_SYNC_TYPE, mSyncType); + return intent; + } +} diff --git a/java/com/android/voicemail/impl/sync/UploadTask.java b/java/com/android/voicemail/impl/sync/UploadTask.java new file mode 100644 index 000000000..7d1a79756 --- /dev/null +++ b/java/com/android/voicemail/impl/sync/UploadTask.java @@ -0,0 +1,69 @@ +/* + * 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.voicemail.impl.sync; + +import android.content.Context; +import android.content.Intent; +import android.telecom.PhoneAccountHandle; +import com.android.voicemail.impl.VoicemailStatus; +import com.android.voicemail.impl.VvmLog; +import com.android.voicemail.impl.scheduling.BaseTask; +import com.android.voicemail.impl.scheduling.PostponePolicy; + +/** + * Upload task triggered by database changes. Will wait until the database has been stable for + * {@link #POSTPONE_MILLIS} to execute. + */ +public class UploadTask extends BaseTask { + + private static final String TAG = "VvmUploadTask"; + + private static final int POSTPONE_MILLIS = 5_000; + + public UploadTask() { + super(TASK_UPLOAD); + addPolicy(new PostponePolicy(POSTPONE_MILLIS)); + } + + public static void start(Context context, PhoneAccountHandle phoneAccountHandle) { + Intent intent = BaseTask.createIntent(context, UploadTask.class, phoneAccountHandle); + context.startService(intent); + } + + @Override + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + } + + @Override + public void onExecuteInBackgroundThread() { + OmtpVvmSyncService service = new OmtpVvmSyncService(getContext()); + + PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle(); + if (phoneAccountHandle == null) { + // This should never happen + VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle()); + return; + } + service.sync( + this, + OmtpVvmSyncService.SYNC_UPLOAD_ONLY, + phoneAccountHandle, + null, + VoicemailStatus.edit(getContext(), phoneAccountHandle)); + } +} diff --git a/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java new file mode 100644 index 000000000..eaca3c44b --- /dev/null +++ b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 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.voicemail.impl.sync; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.provider.VoicemailContract; +import android.telecom.PhoneAccountHandle; +import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil; + +/** Receives changes to the voicemail provider so they can be sent to the voicemail server. */ +public class VoicemailProviderChangeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false); + if (!isSelfChanged) { + for (PhoneAccountHandle phoneAccount : VvmAccountManager.getActiveAccounts(context)) { + if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) { + continue; + } + UploadTask.start(context, phoneAccount); + } + } + } +} diff --git a/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java new file mode 100644 index 000000000..4ef19daf6 --- /dev/null +++ b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2015 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.voicemail.impl.sync; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Status; +import android.telecom.PhoneAccountHandle; + +/** Construct queries to interact with the voicemail status table. */ +public class VoicemailStatusQueryHelper { + + static final String[] PROJECTION = + new String[] { + Status._ID, // 0 + Status.CONFIGURATION_STATE, // 1 + Status.NOTIFICATION_CHANNEL_STATE, // 2 + Status.SOURCE_PACKAGE // 3 + }; + + public static final int _ID = 0; + public static final int CONFIGURATION_STATE = 1; + public static final int NOTIFICATION_CHANNEL_STATE = 2; + public static final int SOURCE_PACKAGE = 3; + + private Context mContext; + private ContentResolver mContentResolver; + private Uri mSourceUri; + + public VoicemailStatusQueryHelper(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName()); + } + + /** + * Check if the configuration state for the voicemail source is "ok", meaning that the source is + * set up. + * + * @param phoneAccount The phone account for the voicemail source to check. + * @return {@code true} if the voicemail source is configured, {@code} false otherwise, including + * if the voicemail source is not registered in the table. + */ + public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) { + return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK); + } + + /** + * Check if the notifications channel of a voicemail source is active. That is, when a new + * voicemail is available, if the server able to notify the device. + * + * @return {@code true} if notifications channel is active, {@code false} otherwise. + */ + public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) { + return isFieldEqualTo( + phoneAccount, NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK); + } + + /** + * Check if a field for an entry in the status table is equal to a specific value. + * + * @param phoneAccount The phone account of the voicemail source to query for. + * @param columnIndex The column index of the field in the returned query. + * @param value The value to compare against. + * @return {@code true} if the stored value is equal to the provided value. {@code false} + * otherwise. + */ + private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) { + Cursor cursor = null; + if (phoneAccount != null) { + String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString(); + String phoneAccountId = phoneAccount.getId(); + if (phoneAccountComponentName == null || phoneAccountId == null) { + return false; + } + try { + String whereClause = + Status.PHONE_ACCOUNT_COMPONENT_NAME + + "=? AND " + + Status.PHONE_ACCOUNT_ID + + "=? AND " + + Status.SOURCE_PACKAGE + + "=?"; + String[] whereArgs = {phoneAccountComponentName, phoneAccountId, mContext.getPackageName()}; + cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(columnIndex) == value; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + return false; + } +} diff --git a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java new file mode 100644 index 000000000..d129406ff --- /dev/null +++ b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2015 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.voicemail.impl.sync; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Voicemails; +import android.telecom.PhoneAccountHandle; +import com.android.dialer.common.Assert; +import com.android.voicemail.impl.Voicemail; +import java.util.ArrayList; +import java.util.List; + +/** Construct queries to interact with the voicemails table. */ +public class VoicemailsQueryHelper { + static final String[] PROJECTION = + new String[] { + Voicemails._ID, // 0 + Voicemails.SOURCE_DATA, // 1 + Voicemails.IS_READ, // 2 + Voicemails.DELETED, // 3 + Voicemails.TRANSCRIPTION // 4 + }; + + public static final int _ID = 0; + public static final int SOURCE_DATA = 1; + public static final int IS_READ = 2; + public static final int DELETED = 3; + public static final int TRANSCRIPTION = 4; + + static final String READ_SELECTION = + Voicemails.DIRTY + "=1 AND " + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1"; + static final String DELETED_SELECTION = Voicemails.DELETED + "=1"; + static final String ARCHIVED_SELECTION = Voicemails.ARCHIVED + "=0"; + + private Context mContext; + private ContentResolver mContentResolver; + private Uri mSourceUri; + + public VoicemailsQueryHelper(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName()); + } + + /** + * Get all the local read voicemails that have not been synced to the server. + * + * @return A list of read voicemails. + */ + public List getReadVoicemails() { + return getLocalVoicemails(READ_SELECTION); + } + + /** + * Get all the locally deleted voicemails that have not been synced to the server. + * + * @return A list of deleted voicemails. + */ + public List getDeletedVoicemails() { + return getLocalVoicemails(DELETED_SELECTION); + } + + /** + * Get all voicemails locally stored. + * + * @return A list of all locally stored voicemails. + */ + public List getAllVoicemails() { + return getLocalVoicemails(null); + } + + /** + * Utility method to make queries to the voicemail database. + * + * @param selection A filter declaring which rows to return. {@code null} returns all rows. + * @return A list of voicemails according to the selection statement. + */ + private List getLocalVoicemails(String selection) { + Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null); + if (cursor == null) { + return null; + } + try { + List voicemails = new ArrayList(); + while (cursor.moveToNext()) { + final long id = cursor.getLong(_ID); + final String sourceData = cursor.getString(SOURCE_DATA); + final boolean isRead = cursor.getInt(IS_READ) == 1; + final String transcription = cursor.getString(TRANSCRIPTION); + Voicemail voicemail = + Voicemail.createForUpdate(id, sourceData) + .setIsRead(isRead) + .setTranscription(transcription) + .build(); + voicemails.add(voicemail); + } + return voicemails; + } finally { + cursor.close(); + } + } + + /** + * Deletes a list of voicemails from the voicemail content provider. + * + * @param voicemails The list of voicemails to delete + * @return The number of voicemails deleted + */ + public int deleteFromDatabase(List voicemails) { + int count = voicemails.size(); + if (count == 0) { + return 0; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + if (i > 0) { + sb.append(","); + } + sb.append(voicemails.get(i).getId()); + } + + String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString()); + return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null); + } + + /** Utility method to delete a single voicemail that is not archived. */ + public void deleteNonArchivedFromDatabase(Voicemail voicemail) { + mContentResolver.delete( + Voicemails.CONTENT_URI, + Voicemails._ID + "=? AND " + Voicemails.ARCHIVED + "= 0", + new String[] {Long.toString(voicemail.getId())}); + } + + public int markReadInDatabase(List voicemails) { + int count = voicemails.size(); + for (int i = 0; i < count; i++) { + markReadInDatabase(voicemails.get(i)); + } + return count; + } + + /** Utility method to mark single message as read. */ + public void markReadInDatabase(Voicemail voicemail) { + Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId()); + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.IS_READ, "1"); + mContentResolver.update(uri, contentValues, null, null); + } + + /** + * Sends an update command to the voicemail content provider for a list of voicemails. From the + * view of the provider, since the updater is the owner of the entry, a blank "update" means that + * the voicemail source is indicating that the server has up-to-date information on the voicemail. + * This flips the "dirty" bit to "0". + * + * @param voicemails The list of voicemails to update + * @return The number of voicemails updated + */ + public int markCleanInDatabase(List voicemails) { + int count = voicemails.size(); + for (int i = 0; i < count; i++) { + markCleanInDatabase(voicemails.get(i)); + } + return count; + } + + /** Utility method to mark single message as clean. */ + public void markCleanInDatabase(Voicemail voicemail) { + Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId()); + ContentValues contentValues = new ContentValues(); + mContentResolver.update(uri, contentValues, null, null); + } + + /** Utility method to add a transcription to the voicemail. */ + public void updateWithTranscription(Voicemail voicemail, String transcription) { + Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId()); + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.TRANSCRIPTION, transcription); + mContentResolver.update(uri, contentValues, null, null); + } + + /** + * Voicemail is unique if the tuple of (phone account component name, phone account id, source + * data) is unique. If the phone account is missing, we also consider this unique since it's + * simply an "unknown" account. + * + * @param voicemail The voicemail to check if it is unique. + * @return {@code true} if the voicemail is unique, {@code false} otherwise. + */ + public boolean isVoicemailUnique(Voicemail voicemail) { + Cursor cursor = null; + PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount(); + if (phoneAccount != null) { + String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString(); + String phoneAccountId = phoneAccount.getId(); + String sourceData = voicemail.getSourceData(); + if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) { + return true; + } + try { + String whereClause = + Voicemails.PHONE_ACCOUNT_COMPONENT_NAME + + "=? AND " + + Voicemails.PHONE_ACCOUNT_ID + + "=? AND " + + Voicemails.SOURCE_DATA + + "=?"; + String[] whereArgs = {phoneAccountComponentName, phoneAccountId, sourceData}; + cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null); + if (cursor.getCount() == 0) { + return true; + } else { + return false; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + return true; + } + + /** + * Marks voicemails in the local database as archived. This indicates that the voicemails from the + * server were removed automatically to make space for new voicemails, and are stored locally on + * the users devices, without a corresponding server copy. + */ + public void markArchivedInDatabase(List voicemails) { + for (Voicemail voicemail : voicemails) { + markArchiveInDatabase(voicemail); + } + } + + /** Utility method to mark single voicemail as archived. */ + public void markArchiveInDatabase(Voicemail voicemail) { + Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId()); + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.ARCHIVED, "1"); + mContentResolver.update(uri, contentValues, null, null); + } + + /** Find the oldest voicemails that are on the device, and also on the server. */ + @TargetApi(VERSION_CODES.M) // used for try with resources + public List oldestVoicemailsOnServer(int numVoicemails) { + if (numVoicemails <= 0) { + Assert.fail("Query for remote voicemails cannot be <= 0"); + } + + String sortAndLimit = "date ASC limit " + numVoicemails; + + try (Cursor cursor = + mContentResolver.query(mSourceUri, null, ARCHIVED_SELECTION, null, sortAndLimit)) { + + Assert.isNotNull(cursor); + + List voicemails = new ArrayList<>(); + while (cursor.moveToNext()) { + final String sourceData = cursor.getString(SOURCE_DATA); + Voicemail voicemail = Voicemail.createForUpdate(cursor.getLong(_ID), sourceData).build(); + voicemails.add(voicemail); + } + + if (voicemails.size() != numVoicemails) { + Assert.fail( + String.format( + "voicemail count (%d) doesn't matched expected (%d)", + voicemails.size(), numVoicemails)); + } + return voicemails; + } + } +} diff --git a/java/com/android/voicemail/impl/sync/VvmAccountManager.java b/java/com/android/voicemail/impl/sync/VvmAccountManager.java new file mode 100644 index 000000000..05f649450 --- /dev/null +++ b/java/com/android/voicemail/impl/sync/VvmAccountManager.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 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.voicemail.impl.sync; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import com.android.dialer.common.Assert; +import com.android.voicemail.impl.OmtpConstants; +import com.android.voicemail.impl.VisualVoicemailPreferences; +import com.android.voicemail.impl.VoicemailStatus; +import com.android.voicemail.impl.sms.StatusMessage; +import java.util.ArrayList; +import java.util.List; + +/** + * Tracks the activation state of a visual voicemail phone account. An account is considered + * activated if it has valid connection information from the {@link StatusMessage} stored on the + * device. Once activation/provisioning is completed, {@link #addAccount(Context, + * PhoneAccountHandle, StatusMessage)} should be called to store the connection information. When an + * account is removed or if the connection information is deemed invalid, {@link + * #removeAccount(Context, PhoneAccountHandle)} should be called to clear the connection information + * and allow reactivation. + */ +public class VvmAccountManager { + public static final String TAG = "VvmAccountManager"; + + private static final String IS_ACCOUNT_ACTIVATED = "is_account_activated"; + + public static void addAccount( + Context context, PhoneAccountHandle phoneAccountHandle, StatusMessage statusMessage) { + VisualVoicemailPreferences preferences = + new VisualVoicemailPreferences(context, phoneAccountHandle); + statusMessage.putStatus(preferences.edit()).putBoolean(IS_ACCOUNT_ACTIVATED, true).apply(); + } + + public static void removeAccount(Context context, PhoneAccountHandle phoneAccount) { + VoicemailStatus.disable(context, phoneAccount); + VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount); + preferences + .edit() + .putBoolean(IS_ACCOUNT_ACTIVATED, false) + .putString(OmtpConstants.IMAP_USER_NAME, null) + .putString(OmtpConstants.IMAP_PASSWORD, null) + .apply(); + } + + public static boolean isAccountActivated(Context context, PhoneAccountHandle phoneAccount) { + Assert.isNotNull(phoneAccount); + VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount); + return preferences.getBoolean(IS_ACCOUNT_ACTIVATED, false); + } + + @NonNull + public static List getActiveAccounts(Context context) { + List results = new ArrayList<>(); + for (PhoneAccountHandle phoneAccountHandle : + context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) { + if (isAccountActivated(context, phoneAccountHandle)) { + results.add(phoneAccountHandle); + } + } + return results; + } +} diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java new file mode 100644 index 000000000..189dc8f2b --- /dev/null +++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java @@ -0,0 +1,120 @@ +/* + * 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.voicemail.impl.sync; + +import android.annotation.TargetApi; +import android.net.Network; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.telecom.PhoneAccountHandle; +import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper; +import com.android.voicemail.impl.VoicemailStatus; +import com.android.voicemail.impl.VvmLog; +import java.io.Closeable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper, + * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed. + */ +@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/ +@TargetApi(VERSION_CODES.O) +public class VvmNetworkRequest { + + private static final String TAG = "VvmNetworkRequest"; + + /** + * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be + * closed once not needed anymore. + */ + public static class NetworkWrapper implements Closeable { + + private final Network mNetwork; + private final VvmNetworkRequestCallback mCallback; + + private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) { + mNetwork = network; + mCallback = callback; + } + + public Network get() { + return mNetwork; + } + + @Override + public void close() { + mCallback.releaseNetwork(); + } + } + + public static class RequestFailedException extends Exception { + + private RequestFailedException(Throwable cause) { + super(cause); + } + } + + @NonNull + public static NetworkWrapper getNetwork( + OmtpVvmCarrierConfigHelper config, PhoneAccountHandle handle, VoicemailStatus.Editor status) + throws RequestFailedException { + FutureNetworkRequestCallback callback = + new FutureNetworkRequestCallback(config, handle, status); + callback.requestNetwork(); + try { + return callback.getFuture().get(); + } catch (InterruptedException | ExecutionException e) { + callback.releaseNetwork(); + VvmLog.e(TAG, "can't get future network", e); + throw new RequestFailedException(e); + } + } + + private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback { + + /** + * {@link CompletableFuture#get()} will block until {@link CompletableFuture# complete(Object) } + * has been called on the other thread. + */ + private final CompletableFuture mFuture = new CompletableFuture<>(); + + public FutureNetworkRequestCallback( + OmtpVvmCarrierConfigHelper config, + PhoneAccountHandle phoneAccount, + VoicemailStatus.Editor status) { + super(config, phoneAccount, status); + } + + public Future getFuture() { + return mFuture; + } + + @Override + public void onAvailable(Network network) { + super.onAvailable(network); + mFuture.complete(new NetworkWrapper(network, this)); + } + + @Override + public void onFailed(String reason) { + super.onFailed(reason); + mFuture.complete(null); + } + } +} diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java new file mode 100644 index 000000000..067eff803 --- /dev/null +++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2015 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.voicemail.impl.sync; + +import android.annotation.TargetApi; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.CallSuper; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import com.android.dialer.common.Assert; +import com.android.voicemail.impl.OmtpEvents; +import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper; +import com.android.voicemail.impl.VoicemailStatus; +import com.android.voicemail.impl.VvmLog; + +/** + * Base class for network request call backs for visual voicemail syncing with the Imap server. This + * handles retries and network requests. + */ +@TargetApi(VERSION_CODES.O) +public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback { + + private static final String TAG = "VvmNetworkRequest"; + + // Timeout used to call ConnectivityManager.requestNetwork + private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000; + + public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout"; + public static final String NETWORK_REQUEST_FAILED_LOST = "lost"; + + protected Context mContext; + protected PhoneAccountHandle mPhoneAccount; + protected NetworkRequest mNetworkRequest; + private ConnectivityManager mConnectivityManager; + private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper; + private final VoicemailStatus.Editor mStatus; + private boolean mRequestSent = false; + private boolean mResultReceived = false; + + public VvmNetworkRequestCallback( + Context context, PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) { + mContext = context; + mPhoneAccount = phoneAccount; + mStatus = status; + mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount); + mNetworkRequest = createNetworkRequest(); + } + + public VvmNetworkRequestCallback( + OmtpVvmCarrierConfigHelper config, + PhoneAccountHandle phoneAccount, + VoicemailStatus.Editor status) { + mContext = config.getContext(); + mPhoneAccount = phoneAccount; + mStatus = status; + mCarrierConfigHelper = config; + mNetworkRequest = createNetworkRequest(); + } + + public VoicemailStatus.Editor getVoicemailStatusEditor() { + return mStatus; + } + + /** + * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier + * requires it. Otherwise use whatever available. + */ + private NetworkRequest createNetworkRequest() { + + NetworkRequest.Builder builder = + new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + + TelephonyManager telephonyManager = + mContext + .getSystemService(TelephonyManager.class) + .createForPhoneAccountHandle(mPhoneAccount); + // At this point mPhoneAccount should always be valid and telephonyManager will never be null + Assert.isNotNull(telephonyManager); + if (mCarrierConfigHelper.isCellularDataRequired()) { + VvmLog.d(TAG, "Transport type: CELLULAR"); + builder + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .setNetworkSpecifier(telephonyManager.getNetworkSpecifier()); + } else { + VvmLog.d(TAG, "Transport type: ANY"); + } + return builder.build(); + } + + public NetworkRequest getNetworkRequest() { + return mNetworkRequest; + } + + @Override + @CallSuper + public void onLost(Network network) { + VvmLog.d(TAG, "onLost"); + mResultReceived = true; + onFailed(NETWORK_REQUEST_FAILED_LOST); + } + + @Override + @CallSuper + public void onAvailable(Network network) { + super.onAvailable(network); + mResultReceived = true; + } + + @CallSuper + public void onUnavailable() { + // TODO: b/32637799 this is hidden, do we really need this? + mResultReceived = true; + onFailed(NETWORK_REQUEST_FAILED_TIMEOUT); + } + + public void requestNetwork() { + if (mRequestSent == true) { + VvmLog.e(TAG, "requestNetwork() called twice"); + return; + } + mRequestSent = true; + getConnectivityManager().requestNetwork(getNetworkRequest(), this); + /** + * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method. Implement our + * own timeout mechanism instead. + */ + Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed( + new Runnable() { + @Override + public void run() { + if (mResultReceived == false) { + onFailed(NETWORK_REQUEST_FAILED_TIMEOUT); + } + } + }, + NETWORK_REQUEST_TIMEOUT_MILLIS); + } + + public void releaseNetwork() { + VvmLog.d(TAG, "releaseNetwork"); + getConnectivityManager().unregisterNetworkCallback(this); + } + + public ConnectivityManager getConnectivityManager() { + if (mConnectivityManager == null) { + mConnectivityManager = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + } + return mConnectivityManager; + } + + @CallSuper + public void onFailed(String reason) { + VvmLog.d(TAG, "onFailed: " + reason); + if (mCarrierConfigHelper.isCellularDataRequired()) { + mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); + } else { + mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION); + } + releaseNetwork(); + } +} -- cgit v1.2.3