341 lines
14 KiB
Java
341 lines
14 KiB
Java
|
/*
|
||
|
* 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.logging.DialerImpression;
|
||
|
import com.android.voicemail.VoicemailComponent;
|
||
|
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.mail.store.ImapFolder.Quota;
|
||
|
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.LoggerUtils;
|
||
|
import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
|
||
|
import java.util.ArrayList;
|
||
|
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";
|
||
|
|
||
|
/** 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 context;
|
||
|
private final VoicemailsQueryHelper queryHelper;
|
||
|
|
||
|
public OmtpVvmSyncService(Context context) {
|
||
|
this.context = context;
|
||
|
queryHelper = new VoicemailsQueryHelper(this.context);
|
||
|
}
|
||
|
|
||
|
public void sync(
|
||
|
BaseTask task,
|
||
|
PhoneAccountHandle phoneAccount,
|
||
|
Voicemail voicemail,
|
||
|
VoicemailStatus.Editor status) {
|
||
|
Assert.isTrue(phoneAccount != null);
|
||
|
VvmLog.v(TAG, "Sync requested for account: " + phoneAccount);
|
||
|
setupAndSendRequest(task, phoneAccount, voicemail, status);
|
||
|
}
|
||
|
|
||
|
private void setupAndSendRequest(
|
||
|
BaseTask task,
|
||
|
PhoneAccountHandle phoneAccount,
|
||
|
Voicemail voicemail,
|
||
|
VoicemailStatus.Editor status) {
|
||
|
if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
|
||
|
VvmLog.e(TAG, "Sync requested for disabled account");
|
||
|
return;
|
||
|
}
|
||
|
if (!VvmAccountManager.isAccountActivated(context, phoneAccount)) {
|
||
|
ActivationTask.start(context, phoneAccount, null);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount);
|
||
|
LoggerUtils.logImpressionOnMainThread(context, DialerImpression.Type.VVM_SYNC_STARTED);
|
||
|
// 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, which is currently displayed to the user as no error.
|
||
|
config.handleEvent(
|
||
|
VoicemailStatus.edit(context, 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, 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,
|
||
|
VoicemailStatus.Editor status) {
|
||
|
try (ImapHelper imapHelper = new ImapHelper(context, phoneAccount, network, status)) {
|
||
|
boolean success;
|
||
|
if (voicemail == null) {
|
||
|
success = syncAll(imapHelper, phoneAccount);
|
||
|
} else {
|
||
|
success = downloadOneVoicemail(imapHelper, voicemail, phoneAccount);
|
||
|
}
|
||
|
if (success) {
|
||
|
// TODO: a bug failure should interrupt all subsequent task via exceptions
|
||
|
imapHelper.updateQuota();
|
||
|
autoDeleteAndArchiveVM(imapHelper, phoneAccount);
|
||
|
imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
|
||
|
LoggerUtils.logImpressionOnMainThread(context, DialerImpression.Type.VVM_SYNC_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 (!isArchiveAllowedAndEnabled(context, phoneAccountHandle)) {
|
||
|
VvmLog.i(TAG, "autoDeleteAndArchiveVM is turned off");
|
||
|
LoggerUtils.logImpressionOnMainThread(
|
||
|
context, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF);
|
||
|
return;
|
||
|
}
|
||
|
Quota quotaOnServer = imapHelper.getQuota();
|
||
|
if (quotaOnServer == null) {
|
||
|
LoggerUtils.logImpressionOnMainThread(
|
||
|
context, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_FAILED_DUE_TO_FAILED_QUOTA_CHECK);
|
||
|
VvmLog.e(TAG, "autoDeleteAndArchiveVM failed - Can't retrieve Imap quota.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ((float) quotaOnServer.occupied / (float) quotaOnServer.total
|
||
|
> AUTO_DELETE_ARCHIVE_VM_THRESHOLD) {
|
||
|
deleteAndArchiveVM(imapHelper, quotaOnServer);
|
||
|
imapHelper.updateQuota();
|
||
|
LoggerUtils.logImpressionOnMainThread(
|
||
|
context, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER);
|
||
|
} else {
|
||
|
VvmLog.i(TAG, "no need to archive and auto delete VM, quota below threshold");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static boolean isArchiveAllowedAndEnabled(
|
||
|
Context context, PhoneAccountHandle phoneAccountHandle) {
|
||
|
|
||
|
if (!VoicemailComponent.get(context)
|
||
|
.getVoicemailClient()
|
||
|
.isVoicemailArchiveAvailable(context)) {
|
||
|
VvmLog.i("isArchiveAllowedAndEnabled", "voicemail archive is not available");
|
||
|
return false;
|
||
|
}
|
||
|
if (!VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle)) {
|
||
|
VvmLog.i("isArchiveAllowedAndEnabled", "voicemail archive is turned off");
|
||
|
return false;
|
||
|
}
|
||
|
if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle)) {
|
||
|
VvmLog.i("isArchiveAllowedAndEnabled", "voicemail is turned off");
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private void deleteAndArchiveVM(ImapHelper imapHelper, Quota quotaOnServer) {
|
||
|
// 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 =
|
||
|
quotaOnServer.occupied - (int) (AUTO_DELETE_ARCHIVE_VM_THRESHOLD * quotaOnServer.total);
|
||
|
List<Voicemail> oldestVoicemails = queryHelper.oldestVoicemailsOnServer(numVoicemails);
|
||
|
VvmLog.w(TAG, "number of voicemails to delete " + numVoicemails);
|
||
|
if (!oldestVoicemails.isEmpty()) {
|
||
|
queryHelper.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(ImapHelper imapHelper, PhoneAccountHandle account) {
|
||
|
|
||
|
List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
|
||
|
List<Voicemail> localVoicemails = queryHelper.getAllVoicemails(account);
|
||
|
List<Voicemail> deletedVoicemails = queryHelper.getDeletedVoicemails(account);
|
||
|
boolean succeeded = true;
|
||
|
|
||
|
if (localVoicemails == null || serverVoicemails == null) {
|
||
|
// Null value means the query failed.
|
||
|
VvmLog.e(TAG, "syncAll: query failed");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (deletedVoicemails.size() > 0) {
|
||
|
if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
|
||
|
// Delete only the voicemails that was deleted on the server, in case more are deleted
|
||
|
// since the IMAP query was completed.
|
||
|
queryHelper.deleteFromDatabase(deletedVoicemails);
|
||
|
} else {
|
||
|
succeeded = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
|
||
|
|
||
|
List<Voicemail> localReadVoicemails = new ArrayList<>();
|
||
|
|
||
|
// 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) {
|
||
|
queryHelper.deleteNonArchivedFromDatabase(localVoicemail);
|
||
|
} else {
|
||
|
if (remoteVoicemail.isRead() && !localVoicemail.isRead()) {
|
||
|
queryHelper.markReadInDatabase(localVoicemail);
|
||
|
} else if (localVoicemail.isRead() && !remoteVoicemail.isRead()) {
|
||
|
localReadVoicemails.add(localVoicemail);
|
||
|
}
|
||
|
|
||
|
if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())
|
||
|
&& TextUtils.isEmpty(localVoicemail.getTranscription())) {
|
||
|
LoggerUtils.logImpressionOnMainThread(
|
||
|
context, DialerImpression.Type.VVM_TRANSCRIPTION_DOWNLOADED);
|
||
|
queryHelper.updateWithTranscription(localVoicemail, remoteVoicemail.getTranscription());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (localReadVoicemails.size() > 0) {
|
||
|
VvmLog.i(TAG, "Marking voicemails as read");
|
||
|
if (imapHelper.markMessagesAsRead(localReadVoicemails)) {
|
||
|
VvmLog.i(TAG, "Marking voicemails as clean");
|
||
|
queryHelper.markCleanInDatabase(localReadVoicemails);
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// The leftover messages are messages that exist on the server but not locally.
|
||
|
boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
|
||
|
for (Voicemail remoteVoicemail : remoteMap.values()) {
|
||
|
if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())) {
|
||
|
LoggerUtils.logImpressionOnMainThread(
|
||
|
context, DialerImpression.Type.VVM_TRANSCRIPTION_DOWNLOADED);
|
||
|
}
|
||
|
Uri uri = VoicemailDatabaseUtil.insert(context, remoteVoicemail);
|
||
|
if (prefetchEnabled) {
|
||
|
VoicemailFetchedCallback fetchedCallback =
|
||
|
new VoicemailFetchedCallback(context, uri, account);
|
||
|
imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return succeeded;
|
||
|
}
|
||
|
|
||
|
private boolean downloadOneVoicemail(
|
||
|
ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account) {
|
||
|
if (shouldPerformPrefetch(account, imapHelper)) {
|
||
|
VoicemailFetchedCallback callback =
|
||
|
new VoicemailFetchedCallback(context, voicemail.getUri(), account);
|
||
|
imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
|
||
|
}
|
||
|
|
||
|
return imapHelper.fetchTranscription(
|
||
|
new TranscriptionFetchedCallback(context, voicemail), voicemail.getSourceData());
|
||
|
}
|
||
|
|
||
|
private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
|
||
|
OmtpVvmCarrierConfigHelper carrierConfigHelper =
|
||
|
new OmtpVvmCarrierConfigHelper(context, 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 context;
|
||
|
private Voicemail voicemail;
|
||
|
|
||
|
public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
|
||
|
this.context = context;
|
||
|
this.voicemail = voicemail;
|
||
|
}
|
||
|
|
||
|
public void setVoicemailTranscription(String transcription) {
|
||
|
VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(context);
|
||
|
queryHelper.updateWithTranscription(voicemail, transcription);
|
||
|
}
|
||
|
}
|
||
|
}
|