summaryrefslogtreecommitdiff
path: root/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java')
-rw-r--r--java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java340
1 files changed, 340 insertions, 0 deletions
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<Voicemail> 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<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
+ List<Voicemail> 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<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
+ List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
+
+ if (localVoicemails == null || serverVoicemails == null) {
+ // Null value means the query failed.
+ return false;
+ }
+
+ Map<String, Voicemail> 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<String, Voicemail> buildMap(List<Voicemail> messages) {
+ Map<String, Voicemail> map = new ArrayMap<String, Voicemail>();
+ 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);
+ }
+ }
+}