Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
@@ -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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DataModelException;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionExecutedListener;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base class for operations that perform application business logic off the main UI thread while
|
||||
* holding a wake lock.
|
||||
* .
|
||||
* Note all derived classes need to provide real implementation of Parcelable (this is abstract)
|
||||
*/
|
||||
public abstract class Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
// Members holding the parameters common to all actions - no action state
|
||||
public final String actionKey;
|
||||
|
||||
// If derived classes keep their data in actionParameters then parcelable is trivial
|
||||
protected Bundle actionParameters;
|
||||
|
||||
// This does not get written to the parcel
|
||||
private final List<Action> mBackgroundActions = new LinkedList<Action>();
|
||||
|
||||
/**
|
||||
* Process the action locally - runs on action service thread.
|
||||
* TODO: Currently, there is no way for this method to indicate failure
|
||||
* @return result to be passed in to {@link ActionExecutedListener#onActionExecuted}. It is
|
||||
* also the result passed in to {@link ActionCompletedListener#onActionSucceeded} if
|
||||
* there is no background work.
|
||||
*/
|
||||
protected Object executeAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues up background work ie. {@link #doBackgroundWork} will be called on the
|
||||
* background worker thread.
|
||||
*/
|
||||
protected void requestBackgroundWork() {
|
||||
mBackgroundActions.add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues up background actions for background processing after the current action has
|
||||
* completed its processing ({@link #executeAction}, {@link processBackgroundCompletion}
|
||||
* or {@link #processBackgroundFailure}) on the Action thread.
|
||||
* @param backgroundAction
|
||||
*/
|
||||
protected void requestBackgroundWork(final Action backgroundAction) {
|
||||
mBackgroundActions.add(backgroundAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return flag indicating if any actions have been queued
|
||||
*/
|
||||
public boolean hasBackgroundActions() {
|
||||
return !mBackgroundActions.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send queued actions to the background worker provided
|
||||
*/
|
||||
public void sendBackgroundActions(final BackgroundWorker worker) {
|
||||
worker.queueBackgroundWork(mBackgroundActions);
|
||||
mBackgroundActions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do work in a long running background worker thread.
|
||||
* {@link #requestBackgroundWork} needs to be called for this method to
|
||||
* be called. {@link #processBackgroundFailure} will be called on the Action service thread
|
||||
* if this method throws {@link DataModelException}.
|
||||
* @return response that is to be passed to {@link #processBackgroundResponse}
|
||||
*/
|
||||
protected Bundle doBackgroundWork() throws DataModelException {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the success response from the background worker. Runs on action service thread.
|
||||
* @param response the response returned by {@link #doBackgroundWork}
|
||||
* @return result to be passed in to {@link ActionCompletedListener#onActionSucceeded}
|
||||
*/
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of failures when sending background actions. Runs on action service thread
|
||||
* @return result to be passed in to {@link ActionCompletedListener#onActionFailed}
|
||||
*/
|
||||
protected Object processBackgroundFailure() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
protected Action(final String key) {
|
||||
this.actionKey = key;
|
||||
this.actionParameters = new Bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
protected Action() {
|
||||
this.actionKey = generateUniqueActionKey(getClass().getSimpleName());
|
||||
this.actionParameters = new Bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action and monitor for processing by the ActionService via the factory helper
|
||||
*/
|
||||
protected void start(final ActionMonitor monitor) {
|
||||
ActionMonitor.registerActionMonitor(this.actionKey, monitor);
|
||||
DataModel.startActionService(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action for processing by the ActionService via the factory helper
|
||||
*/
|
||||
public void start() {
|
||||
DataModel.startActionService(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action for delayed processing by the ActionService via the factory helper
|
||||
*/
|
||||
public void schedule(final int requestCode, final long delayMs) {
|
||||
DataModel.scheduleAction(this, requestCode, delayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when action queues ActionService intent
|
||||
*/
|
||||
protected final void markStart() {
|
||||
ActionMonitor.setState(this, ActionMonitor.STATE_CREATED,
|
||||
ActionMonitor.STATE_QUEUED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the beginning of local action execution
|
||||
*/
|
||||
protected final void markBeginExecute() {
|
||||
ActionMonitor.setState(this, ActionMonitor.STATE_QUEUED,
|
||||
ActionMonitor.STATE_EXECUTING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the end of local action execution - either completes the action or queues
|
||||
* background actions
|
||||
*/
|
||||
protected final void markEndExecute(final Object result) {
|
||||
final boolean hasBackgroundActions = hasBackgroundActions();
|
||||
ActionMonitor.setExecutedState(this, ActionMonitor.STATE_EXECUTING,
|
||||
hasBackgroundActions, result);
|
||||
if (!hasBackgroundActions) {
|
||||
ActionMonitor.setCompleteState(this, ActionMonitor.STATE_EXECUTING,
|
||||
result, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update action state to indicate that the background worker is starting
|
||||
*/
|
||||
protected final void markBackgroundWorkStarting() {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED,
|
||||
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update action state to indicate that the background worker has posted its response
|
||||
* (or failure) to the Action service
|
||||
*/
|
||||
protected final void markBackgroundCompletionQueued() {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
|
||||
ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update action state to indicate the background action failed but is being re-queued for retry
|
||||
*/
|
||||
protected final void markBackgroundWorkQueued() {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
|
||||
ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ActionService to process a response from the background worker
|
||||
* @param response the response returned by {@link #doBackgroundWork}
|
||||
*/
|
||||
protected final void processBackgroundWorkResponse(final Bundle response) {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED,
|
||||
ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE);
|
||||
final Object result = processBackgroundResponse(response);
|
||||
ActionMonitor.setCompleteState(this,
|
||||
ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE, result, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ActionService when a background action fails
|
||||
*/
|
||||
protected final void processBackgroundWorkFailure() {
|
||||
final Object result = processBackgroundFailure();
|
||||
ActionMonitor.setCompleteState(this, ActionMonitor.STATE_UNDEFINED,
|
||||
result, false);
|
||||
}
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
private static long sActionIdx = System.currentTimeMillis() * 1000;
|
||||
|
||||
/**
|
||||
* Helper method to generate a unique operation index
|
||||
*/
|
||||
protected static long getActionIdx() {
|
||||
long idx = 0;
|
||||
synchronized (sLock) {
|
||||
idx = ++sActionIdx;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper can be used to generate a unique key used to identify an action.
|
||||
* @param baseKey - key generated to identify the action parameters
|
||||
* @return - composite key generated by appending unique index
|
||||
*/
|
||||
protected static String generateUniqueActionKey(final String baseKey) {
|
||||
final StringBuilder key = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(baseKey)) {
|
||||
key.append(baseKey);
|
||||
}
|
||||
key.append(":");
|
||||
key.append(getActionIdx());
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Most derived classes use this base implementation (unless they include files handles)
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived classes need to implement writeToParcel (but typically should call this method
|
||||
* to parcel Action member variables before they parcel their member variables).
|
||||
*/
|
||||
public void writeActionToParcel(final Parcel parcel, final int flags) {
|
||||
parcel.writeString(this.actionKey);
|
||||
parcel.writeBundle(this.actionParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for derived classes to implement parcelable
|
||||
*/
|
||||
public Action(final Parcel in) {
|
||||
this.actionKey = in.readString();
|
||||
// Note: Need to set classloader to ensure we can un-parcel classes from this package
|
||||
this.actionParameters = in.readBundle(Action.class.getClassLoader());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.v4.util.SimpleArrayMap;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.util.Assert.RunsOnAnyThread;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.ThreadUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Base class for action monitors
|
||||
* Actions come in various flavors but
|
||||
* o) Fire and forget - no monitor
|
||||
* o) Immediate local processing only - will trigger ActionCompletedListener when done
|
||||
* o) Background worker processing only - will trigger ActionCompletedListener when done
|
||||
* o) Immediate local processing followed by background work followed by more local processing
|
||||
* - will trigger ActionExecutedListener once local processing complete and
|
||||
* ActionCompletedListener when second set of local process (dealing with background
|
||||
* worker response) is complete
|
||||
*/
|
||||
public class ActionMonitor {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Interface used to notify on completion of local execution for an action
|
||||
*/
|
||||
public interface ActionExecutedListener {
|
||||
/**
|
||||
* @param result value returned by {@link Action#executeAction}
|
||||
*/
|
||||
@RunsOnMainThread
|
||||
abstract void onActionExecuted(ActionMonitor monitor, final Action action,
|
||||
final Object data, final Object result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to notify action completion
|
||||
*/
|
||||
public interface ActionCompletedListener {
|
||||
/**
|
||||
* @param result object returned from processing the action. This is the value returned by
|
||||
* {@link Action#executeAction} if there is no background work, or
|
||||
* else the value returned by
|
||||
* {@link Action#processBackgroundResponse}
|
||||
*/
|
||||
@RunsOnMainThread
|
||||
abstract void onActionSucceeded(ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result);
|
||||
/**
|
||||
* @param result value returned by {@link Action#processBackgroundFailure}
|
||||
*/
|
||||
@RunsOnMainThread
|
||||
abstract void onActionFailed(ActionMonitor monitor, final Action action,
|
||||
final Object data, final Object result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for being notified of action state changes - used for profiling, testing only
|
||||
*/
|
||||
protected interface ActionStateChangedListener {
|
||||
/**
|
||||
* @param action the action that is changing state
|
||||
* @param state the new state of the action
|
||||
*/
|
||||
@RunsOnAnyThread
|
||||
void onActionStateChanged(Action action, int state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operations always start out as STATE_CREATED and finish as STATE_COMPLETE.
|
||||
* Some common state transition sequences in between include:
|
||||
* <ul>
|
||||
* <li>Local data change only : STATE_QUEUED - STATE_EXECUTING
|
||||
* <li>Background worker request only : STATE_BACKGROUND_ACTIONS_QUEUED
|
||||
* - STATE_EXECUTING_BACKGROUND_ACTION
|
||||
* - STATE_BACKGROUND_COMPLETION_QUEUED
|
||||
* - STATE_PROCESSING_BACKGROUND_RESPONSE
|
||||
* <li>Local plus background worker request : STATE_QUEUED - STATE_EXECUTING
|
||||
* - STATE_BACKGROUND_ACTIONS_QUEUED
|
||||
* - STATE_EXECUTING_BACKGROUND_ACTION
|
||||
* - STATE_BACKGROUND_COMPLETION_QUEUED
|
||||
* - STATE_PROCESSING_BACKGROUND_RESPONSE
|
||||
* </ul>
|
||||
*/
|
||||
protected static final int STATE_UNDEFINED = 0;
|
||||
protected static final int STATE_CREATED = 1; // Just created
|
||||
protected static final int STATE_QUEUED = 2; // Action queued for processing
|
||||
protected static final int STATE_EXECUTING = 3; // Action processing on datamodel thread
|
||||
protected static final int STATE_BACKGROUND_ACTIONS_QUEUED = 4;
|
||||
protected static final int STATE_EXECUTING_BACKGROUND_ACTION = 5;
|
||||
// The background work has completed, either returning a success response or resulting in a
|
||||
// failure
|
||||
protected static final int STATE_BACKGROUND_COMPLETION_QUEUED = 6;
|
||||
protected static final int STATE_PROCESSING_BACKGROUND_RESPONSE = 7;
|
||||
protected static final int STATE_COMPLETE = 8; // Action complete
|
||||
|
||||
/**
|
||||
* Lock used to protect access to state and listeners
|
||||
*/
|
||||
private final Object mLock = new Object();
|
||||
|
||||
/**
|
||||
* Current state of action
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected int mState;
|
||||
|
||||
/**
|
||||
* Listener which is notified on action completion
|
||||
*/
|
||||
private ActionCompletedListener mCompletedListener;
|
||||
|
||||
/**
|
||||
* Listener which is notified on action executed
|
||||
*/
|
||||
private ActionExecutedListener mExecutedListener;
|
||||
|
||||
/**
|
||||
* Listener which is notified of state changes
|
||||
*/
|
||||
private ActionStateChangedListener mStateChangedListener;
|
||||
|
||||
/**
|
||||
* Handler used to post results back to caller
|
||||
*/
|
||||
private final Handler mHandler;
|
||||
|
||||
/**
|
||||
* Data passed back to listeners (associated with the action when it is created)
|
||||
*/
|
||||
private final Object mData;
|
||||
|
||||
/**
|
||||
* The action key is used to determine equivalence of operations and their requests
|
||||
*/
|
||||
private final String mActionKey;
|
||||
|
||||
/**
|
||||
* Get action key identifying associated action
|
||||
*/
|
||||
public String getActionKey() {
|
||||
return mActionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister listeners so that they will not be called back - override this method if needed
|
||||
*/
|
||||
public void unregister() {
|
||||
clearListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister listeners so that they will not be called
|
||||
*/
|
||||
protected final void clearListeners() {
|
||||
synchronized (mLock) {
|
||||
mCompletedListener = null;
|
||||
mExecutedListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a monitor associated with a particular action instance
|
||||
*/
|
||||
protected ActionMonitor(final int initialState, final String actionKey,
|
||||
final Object data) {
|
||||
mHandler = ThreadUtil.getMainThreadHandler();
|
||||
mActionKey = actionKey;
|
||||
mState = initialState;
|
||||
mData = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return flag to indicate if action is complete
|
||||
*/
|
||||
public boolean isComplete() {
|
||||
boolean complete = false;
|
||||
synchronized (mLock) {
|
||||
complete = (mState == STATE_COMPLETE);
|
||||
}
|
||||
return complete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set listener that will be called with action completed result
|
||||
*/
|
||||
protected final void setCompletedListener(final ActionCompletedListener listener) {
|
||||
synchronized (mLock) {
|
||||
mCompletedListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set listener that will be called with local execution result
|
||||
*/
|
||||
protected final void setExecutedListener(final ActionExecutedListener listener) {
|
||||
synchronized (mLock) {
|
||||
mExecutedListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set listener that will be called with local execution result
|
||||
*/
|
||||
protected final void setStateChangedListener(final ActionStateChangedListener listener) {
|
||||
synchronized (mLock) {
|
||||
mStateChangedListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a state update transition
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param newState - new state which will be set
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected void updateState(final Action action, final int expectedOldState,
|
||||
final int newState) {
|
||||
ActionStateChangedListener listener = null;
|
||||
synchronized (mLock) {
|
||||
if (expectedOldState != STATE_UNDEFINED &&
|
||||
mState != expectedOldState) {
|
||||
throw new IllegalStateException("On updateState to " + newState + " was " + mState
|
||||
+ " expecting " + expectedOldState);
|
||||
}
|
||||
if (newState != mState) {
|
||||
mState = newState;
|
||||
listener = mStateChangedListener;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onActionStateChanged(action, newState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a state update transition
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param newState - new state which will be set
|
||||
*/
|
||||
static void setState(final Action action, final int expectedOldState,
|
||||
final int newState) {
|
||||
int oldMonitorState = expectedOldState;
|
||||
int newMonitorState = newState;
|
||||
final ActionMonitor monitor
|
||||
= ActionMonitor.lookupActionMonitor(action.actionKey);
|
||||
if (monitor != null) {
|
||||
oldMonitorState = monitor.mState;
|
||||
monitor.updateState(action, expectedOldState, newState);
|
||||
newMonitorState = monitor.mState;
|
||||
}
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
|
||||
+ "UTC State = " + oldMonitorState + " - " + newMonitorState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param result - object returned from processing the action. This is the value returned by
|
||||
* {@link Action#executeAction} if there is no background work, or
|
||||
* else the value returned by {@link Action#processBackgroundResponse}
|
||||
* or {@link Action#processBackgroundFailure}
|
||||
*/
|
||||
private final void complete(final Action action,
|
||||
final int expectedOldState, final Object result,
|
||||
final boolean succeeded) {
|
||||
ActionCompletedListener completedListener = null;
|
||||
synchronized (mLock) {
|
||||
setState(action, expectedOldState, STATE_COMPLETE);
|
||||
completedListener = mCompletedListener;
|
||||
mExecutedListener = null;
|
||||
mStateChangedListener = null;
|
||||
}
|
||||
if (completedListener != null) {
|
||||
// Marshal to UI thread
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ActionCompletedListener listener = null;
|
||||
synchronized (mLock) {
|
||||
if (mCompletedListener != null) {
|
||||
listener = mCompletedListener;
|
||||
}
|
||||
mCompletedListener = null;
|
||||
}
|
||||
if (listener != null) {
|
||||
if (succeeded) {
|
||||
listener.onActionSucceeded(ActionMonitor.this,
|
||||
action, mData, result);
|
||||
} else {
|
||||
listener.onActionFailed(ActionMonitor.this,
|
||||
action, mData, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param result - object returned from processing the action. This is the value returned by
|
||||
* {@link Action#executeAction} if there is no background work, or
|
||||
* else the value returned by {@link Action#processBackgroundResponse}
|
||||
* or {@link Action#processBackgroundFailure}
|
||||
*/
|
||||
static void setCompleteState(final Action action, final int expectedOldState,
|
||||
final Object result, final boolean succeeded) {
|
||||
int oldMonitorState = expectedOldState;
|
||||
final ActionMonitor monitor
|
||||
= ActionMonitor.lookupActionMonitor(action.actionKey);
|
||||
if (monitor != null) {
|
||||
oldMonitorState = monitor.mState;
|
||||
monitor.complete(action, expectedOldState, result, succeeded);
|
||||
unregisterActionMonitorIfComplete(action.actionKey, monitor);
|
||||
}
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
|
||||
+ "UTC State = " + oldMonitorState + " - " + STATE_COMPLETE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param hasBackgroundActions - has the completing action requested background work
|
||||
* @param result - the return value of {@link Action#executeAction}
|
||||
*/
|
||||
final void executed(final Action action,
|
||||
final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
|
||||
ActionExecutedListener executedListener = null;
|
||||
synchronized (mLock) {
|
||||
if (hasBackgroundActions) {
|
||||
setState(action, expectedOldState, STATE_BACKGROUND_ACTIONS_QUEUED);
|
||||
}
|
||||
executedListener = mExecutedListener;
|
||||
}
|
||||
if (executedListener != null) {
|
||||
// Marshal to UI thread
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ActionExecutedListener listener = null;
|
||||
synchronized (mLock) {
|
||||
if (mExecutedListener != null) {
|
||||
listener = mExecutedListener;
|
||||
mExecutedListener = null;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onActionExecuted(ActionMonitor.this,
|
||||
action, mData, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param hasBackgroundActions - has the completing action requested background work
|
||||
* @param result - the return value of {@link Action#executeAction}
|
||||
*/
|
||||
static void setExecutedState(final Action action,
|
||||
final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
|
||||
int oldMonitorState = expectedOldState;
|
||||
final ActionMonitor monitor
|
||||
= ActionMonitor.lookupActionMonitor(action.actionKey);
|
||||
if (monitor != null) {
|
||||
oldMonitorState = monitor.mState;
|
||||
monitor.executed(action, expectedOldState, hasBackgroundActions, result);
|
||||
}
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
|
||||
+ "UTC State = " + oldMonitorState + " - EXECUTED");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of action monitors indexed by actionKey
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static SimpleArrayMap<String, ActionMonitor> sActionMonitors =
|
||||
new SimpleArrayMap<String, ActionMonitor>();
|
||||
|
||||
/**
|
||||
* Insert new monitor into map
|
||||
*/
|
||||
static void registerActionMonitor(final String actionKey,
|
||||
final ActionMonitor monitor) {
|
||||
if (monitor != null
|
||||
&& (TextUtils.isEmpty(monitor.getActionKey())
|
||||
|| TextUtils.isEmpty(actionKey)
|
||||
|| !actionKey.equals(monitor.getActionKey()))) {
|
||||
throw new IllegalArgumentException("Monitor key " + monitor.getActionKey()
|
||||
+ " not compatible with action key " + actionKey);
|
||||
}
|
||||
synchronized (sActionMonitors) {
|
||||
sActionMonitors.put(actionKey, monitor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find monitor associated with particular action
|
||||
*/
|
||||
private static ActionMonitor lookupActionMonitor(final String actionKey) {
|
||||
ActionMonitor monitor = null;
|
||||
synchronized (sActionMonitors) {
|
||||
monitor = sActionMonitors.get(actionKey);
|
||||
}
|
||||
return monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove monitor from map
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void unregisterActionMonitor(final String actionKey,
|
||||
final ActionMonitor monitor) {
|
||||
if (monitor != null) {
|
||||
synchronized (sActionMonitors) {
|
||||
sActionMonitors.remove(actionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove monitor from map if the action is complete
|
||||
*/
|
||||
static void unregisterActionMonitorIfComplete(final String actionKey,
|
||||
final ActionMonitor monitor) {
|
||||
if (monitor != null && monitor.isComplete()) {
|
||||
synchronized (sActionMonitors) {
|
||||
sActionMonitors.remove(actionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* Class providing interface for the ActionService - can be stubbed for testing
|
||||
*/
|
||||
public class ActionService {
|
||||
protected static PendingIntent makeStartActionPendingIntent(final Context context,
|
||||
final Action action, final int requestCode, final boolean launchesAnActivity) {
|
||||
return ActionServiceImpl.makeStartActionPendingIntent(context, action, requestCode,
|
||||
launchesAnActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an action by posting it over the the ActionService
|
||||
*/
|
||||
public void startAction(final Action action) {
|
||||
ActionServiceImpl.startAction(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a delayed action by posting it over the the ActionService
|
||||
*/
|
||||
public void scheduleAction(final Action action, final int code,
|
||||
final long delayMs) {
|
||||
ActionServiceImpl.scheduleAction(action, code, delayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a response from the BackgroundWorker in the ActionService
|
||||
*/
|
||||
protected void handleResponseFromBackgroundWorker(
|
||||
final Action action, final Bundle response) {
|
||||
ActionServiceImpl.handleResponseFromBackgroundWorker(action, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a failure from the BackgroundWorker in the ActionService
|
||||
*/
|
||||
protected void handleFailureFromBackgroundWorker(final Action action,
|
||||
final Exception exception) {
|
||||
ActionServiceImpl.handleFailureFromBackgroundWorker(action, exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.IntentService;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.LoggingTimer;
|
||||
import com.android.messaging.util.WakeLockHelper;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* ActionService used to perform background processing for data model
|
||||
*/
|
||||
public class ActionServiceImpl extends IntentService {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final boolean VERBOSE = false;
|
||||
|
||||
public ActionServiceImpl() {
|
||||
super("ActionService");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start action by sending intent to the service
|
||||
* @param action - action to start
|
||||
*/
|
||||
protected static void startAction(final Action action) {
|
||||
final Intent intent = makeIntent(OP_START_ACTION);
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
action.markStart();
|
||||
startServiceWithIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action to run after specified delay using alarm manager to send pendingintent
|
||||
* @param action - action to start
|
||||
* @param requestCode - request code used to collapse requests
|
||||
* @param delayMs - delay in ms (from now) before action will start
|
||||
*/
|
||||
protected static void scheduleAction(final Action action, final int requestCode,
|
||||
final long delayMs) {
|
||||
final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
|
||||
PendingActionReceiver.scheduleAlarm(intent, requestCode, delayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle response returned by BackgroundWorker
|
||||
* @param request - request generating response
|
||||
* @param response - response from service
|
||||
*/
|
||||
protected static void handleResponseFromBackgroundWorker(final Action action,
|
||||
final Bundle response) {
|
||||
final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_RESPONSE);
|
||||
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
intent.putExtra(EXTRA_WORKER_RESPONSE, response);
|
||||
|
||||
startServiceWithIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle response returned by BackgroundWorker
|
||||
* @param request - request generating failure
|
||||
*/
|
||||
protected static void handleFailureFromBackgroundWorker(final Action action,
|
||||
final Exception exception) {
|
||||
final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_FAILURE);
|
||||
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
intent.putExtra(EXTRA_WORKER_EXCEPTION, exception);
|
||||
|
||||
startServiceWithIntent(intent);
|
||||
}
|
||||
|
||||
// ops
|
||||
@VisibleForTesting
|
||||
protected static final int OP_START_ACTION = 200;
|
||||
@VisibleForTesting
|
||||
protected static final int OP_RECEIVE_BACKGROUND_RESPONSE = 201;
|
||||
@VisibleForTesting
|
||||
protected static final int OP_RECEIVE_BACKGROUND_FAILURE = 202;
|
||||
|
||||
// extras
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_OP_CODE = "op";
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_ACTION_BUNDLE = "datamodel_action_bundle";
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_WORKER_EXCEPTION = "worker_exception";
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_WORKER_RESPONSE = "worker_response";
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_WORKER_UPDATE = "worker_update";
|
||||
@VisibleForTesting
|
||||
protected static final String BUNDLE_ACTION = "bundle_action";
|
||||
|
||||
private BackgroundWorker mBackgroundWorker;
|
||||
|
||||
/**
|
||||
* Allocate an intent with a specific opcode.
|
||||
*/
|
||||
private static Intent makeIntent(final int opcode) {
|
||||
final Intent intent = new Intent(Factory.get().getApplicationContext(),
|
||||
ActionServiceImpl.class);
|
||||
intent.putExtra(EXTRA_OP_CODE, opcode);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver for alarms scheduled through ActionService.
|
||||
*/
|
||||
public static class PendingActionReceiver extends BroadcastReceiver {
|
||||
static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION";
|
||||
|
||||
/**
|
||||
* Allocate an intent with a specific opcode and alarm action.
|
||||
*/
|
||||
public static Intent makeIntent(final int opcode) {
|
||||
final Intent intent = new Intent(Factory.get().getApplicationContext(),
|
||||
PendingActionReceiver.class);
|
||||
intent.setAction(ACTION);
|
||||
intent.putExtra(EXTRA_OP_CODE, opcode);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void scheduleAlarm(final Intent intent, final int requestCode,
|
||||
final long delayMs) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||
context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
final AlarmManager mgr =
|
||||
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
|
||||
if (delayMs < Long.MAX_VALUE) {
|
||||
mgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + delayMs, pendingIntent);
|
||||
} else {
|
||||
mgr.cancel(pendingIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
ActionServiceImpl.startServiceWithIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pending intent that will trigger a data model action when the intent is
|
||||
* triggered
|
||||
*/
|
||||
public static PendingIntent makeStartActionPendingIntent(final Context context,
|
||||
final Action action, final int requestCode, final boolean launchesAnActivity) {
|
||||
final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
if (launchesAnActivity) {
|
||||
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mBackgroundWorker = DataModel.get().getBackgroundWorkerForActionService();
|
||||
DataModel.get().getConnectivityUtil().registerForSignalStrength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
DataModel.get().getConnectivityUtil().unregisterForSignalStrength();
|
||||
}
|
||||
|
||||
private static final String WAKELOCK_ID = "bugle_datamodel_service_wakelock";
|
||||
@VisibleForTesting
|
||||
static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID);
|
||||
|
||||
/**
|
||||
* Queue intent to the ActionService after acquiring wake lock
|
||||
*/
|
||||
private static void startServiceWithIntent(final Intent intent) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
|
||||
// Increase refCount on wake lock - acquiring if necessary
|
||||
if (VERBOSE) {
|
||||
LogUtil.v(TAG, "acquiring wakelock for opcode " + opcode);
|
||||
}
|
||||
sWakeLock.acquire(context, intent, opcode);
|
||||
intent.setClass(context, ActionServiceImpl.class);
|
||||
|
||||
// TODO: Note that intent will be quietly discarded if it exceeds available rpc
|
||||
// memory (in total around 1MB). See this article for background
|
||||
// http://developer.android.com/reference/android/os/TransactionTooLargeException.html
|
||||
// Perhaps we should keep large structures in the action monitor?
|
||||
if (context.startService(intent) == null) {
|
||||
LogUtil.e(TAG,
|
||||
"ActionService.startServiceWithIntent: failed to start service for intent "
|
||||
+ intent);
|
||||
sWakeLock.release(intent, opcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onHandleIntent(final Intent intent) {
|
||||
if (intent == null) {
|
||||
// Shouldn't happen but sometimes does following another crash.
|
||||
LogUtil.w(TAG, "ActionService.onHandleIntent: Called with null intent");
|
||||
return;
|
||||
}
|
||||
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
|
||||
sWakeLock.ensure(intent, opcode);
|
||||
|
||||
try {
|
||||
Action action;
|
||||
final Bundle actionBundle = intent.getBundleExtra(EXTRA_ACTION_BUNDLE);
|
||||
actionBundle.setClassLoader(getClassLoader());
|
||||
switch(opcode) {
|
||||
case OP_START_ACTION: {
|
||||
action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
|
||||
executeAction(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_RECEIVE_BACKGROUND_RESPONSE: {
|
||||
action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
|
||||
final Bundle response = intent.getBundleExtra(EXTRA_WORKER_RESPONSE);
|
||||
processBackgroundResponse(action, response);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_RECEIVE_BACKGROUND_FAILURE: {
|
||||
action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
|
||||
processBackgroundFailure(action);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unrecognized opcode in ActionServiceImpl");
|
||||
}
|
||||
|
||||
action.sendBackgroundActions(mBackgroundWorker);
|
||||
} finally {
|
||||
// Decrease refCount on wake lock - releasing if necessary
|
||||
sWakeLock.release(intent, opcode);
|
||||
}
|
||||
}
|
||||
|
||||
private static final long EXECUTION_TIME_WARN_LIMIT_MS = 1000; // 1 second
|
||||
/**
|
||||
* Local execution of action on ActionService thread
|
||||
*/
|
||||
private void executeAction(final Action action) {
|
||||
action.markBeginExecute();
|
||||
|
||||
final LoggingTimer timer = createLoggingTimer(action, "#executeAction");
|
||||
timer.start();
|
||||
|
||||
final Object result = action.executeAction();
|
||||
|
||||
timer.stopAndLog();
|
||||
|
||||
action.markEndExecute(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process response on ActionService thread
|
||||
*/
|
||||
private void processBackgroundResponse(final Action action, final Bundle response) {
|
||||
final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundResponse");
|
||||
timer.start();
|
||||
|
||||
action.processBackgroundWorkResponse(response);
|
||||
|
||||
timer.stopAndLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process failure on ActionService thread
|
||||
*/
|
||||
private void processBackgroundFailure(final Action action) {
|
||||
final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundFailure");
|
||||
timer.start();
|
||||
|
||||
action.processBackgroundWorkFailure();
|
||||
|
||||
timer.stopAndLog();
|
||||
}
|
||||
|
||||
private static LoggingTimer createLoggingTimer(
|
||||
final Action action, final String methodName) {
|
||||
return new LoggingTimer(TAG, action.getClass().getSimpleName() + methodName,
|
||||
EXECUTION_TIME_WARN_LIMIT_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface between action service and its workers
|
||||
*/
|
||||
public class BackgroundWorker {
|
||||
|
||||
/**
|
||||
* Send list of requests from action service to a worker
|
||||
*/
|
||||
public void queueBackgroundWork(final List<Action> backgroundActions) {
|
||||
BackgroundWorkerService.queueBackgroundWork(backgroundActions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DataModelException;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.LoggingTimer;
|
||||
import com.android.messaging.util.WakeLockHelper;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Background worker service is an initial example of a background work queue handler
|
||||
* Used to actually "send" messages which may take some time and should not block ActionService
|
||||
* or UI
|
||||
*/
|
||||
public class BackgroundWorkerService extends IntentService {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final boolean VERBOSE = false;
|
||||
|
||||
private static final String WAKELOCK_ID = "bugle_background_worker_wakelock";
|
||||
@VisibleForTesting
|
||||
static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID);
|
||||
|
||||
private final ActionService mHost;
|
||||
|
||||
public BackgroundWorkerService() {
|
||||
super("BackgroundWorker");
|
||||
mHost = DataModel.get().getActionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a list of requests from action service to this worker
|
||||
*/
|
||||
public static void queueBackgroundWork(final List<Action> actions) {
|
||||
for (final Action action : actions) {
|
||||
startServiceWithAction(action, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ops
|
||||
@VisibleForTesting
|
||||
protected static final int OP_PROCESS_REQUEST = 400;
|
||||
|
||||
// extras
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_OP_CODE = "op";
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_ACTION = "action";
|
||||
@VisibleForTesting
|
||||
protected static final String EXTRA_ATTEMPT = "retry_attempt";
|
||||
|
||||
/**
|
||||
* Queue action intent to the BackgroundWorkerService after acquiring wake lock
|
||||
*/
|
||||
private static void startServiceWithAction(final Action action,
|
||||
final int retryCount) {
|
||||
final Intent intent = new Intent();
|
||||
intent.putExtra(EXTRA_ACTION, action);
|
||||
intent.putExtra(EXTRA_ATTEMPT, retryCount);
|
||||
startServiceWithIntent(OP_PROCESS_REQUEST, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue intent to the BackgroundWorkerService after acquiring wake lock
|
||||
*/
|
||||
private static void startServiceWithIntent(final int opcode, final Intent intent) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
|
||||
intent.setClass(context, BackgroundWorkerService.class);
|
||||
intent.putExtra(EXTRA_OP_CODE, opcode);
|
||||
sWakeLock.acquire(context, intent, opcode);
|
||||
if (VERBOSE) {
|
||||
LogUtil.v(TAG, "acquiring wakelock for opcode " + opcode);
|
||||
}
|
||||
|
||||
if (context.startService(intent) == null) {
|
||||
LogUtil.e(TAG,
|
||||
"BackgroundWorkerService.startServiceWithAction: failed to start service for "
|
||||
+ opcode);
|
||||
sWakeLock.release(intent, opcode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(final Intent intent) {
|
||||
if (intent == null) {
|
||||
// Shouldn't happen but sometimes does following another crash.
|
||||
LogUtil.w(TAG, "BackgroundWorkerService.onHandleIntent: Called with null intent");
|
||||
return;
|
||||
}
|
||||
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
|
||||
sWakeLock.ensure(intent, opcode);
|
||||
|
||||
try {
|
||||
switch(opcode) {
|
||||
case OP_PROCESS_REQUEST: {
|
||||
final Action action = intent.getParcelableExtra(EXTRA_ACTION);
|
||||
final int attempt = intent.getIntExtra(EXTRA_ATTEMPT, -1);
|
||||
doBackgroundWork(action, attempt);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unrecognized opcode in BackgroundWorkerService");
|
||||
}
|
||||
} finally {
|
||||
sWakeLock.release(intent, opcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local execution of background work for action on ActionService thread
|
||||
*/
|
||||
private void doBackgroundWork(final Action action, final int attempt) {
|
||||
action.markBackgroundWorkStarting();
|
||||
Bundle response = null;
|
||||
try {
|
||||
final LoggingTimer timer = new LoggingTimer(
|
||||
TAG, action.getClass().getSimpleName() + "#doBackgroundWork");
|
||||
timer.start();
|
||||
|
||||
response = action.doBackgroundWork();
|
||||
|
||||
timer.stopAndLog();
|
||||
action.markBackgroundCompletionQueued();
|
||||
mHost.handleResponseFromBackgroundWorker(action, response);
|
||||
} catch (final Exception exception) {
|
||||
final boolean retry = false;
|
||||
LogUtil.e(TAG, "Error in background worker", exception);
|
||||
if (!(exception instanceof DataModelException)) {
|
||||
// DataModelException is expected (sort-of) and handled in handleFailureFromWorker
|
||||
// below, but other exceptions should crash ENG builds
|
||||
Assert.fail("Unexpected error in background worker - abort");
|
||||
}
|
||||
if (retry) {
|
||||
action.markBackgroundWorkQueued();
|
||||
startServiceWithAction(action, attempt + 1);
|
||||
} else {
|
||||
action.markBackgroundCompletionQueued();
|
||||
mHost.handleFailureFromBackgroundWorker(action, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.AccessibilityUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.util.ThreadUtil;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Shows one-time, transient notifications in response to action failures (i.e. permanent failures
|
||||
* when sending a message) by showing toasts.
|
||||
*/
|
||||
public class BugleActionToasts {
|
||||
/**
|
||||
* Called when SendMessageAction or DownloadMmsAction finishes
|
||||
* @param conversationId the conversation of the sent or downloaded message
|
||||
* @param success did the action succeed
|
||||
* @param status the message sending status
|
||||
* @param isSms whether the message is sent using SMS
|
||||
* @param subId the subId of the SIM related to this send
|
||||
* @param isSend whether it is a send (false for download)
|
||||
*/
|
||||
static void onSendMessageOrManualDownloadActionCompleted(
|
||||
final String conversationId,
|
||||
final boolean success,
|
||||
final int status,
|
||||
final boolean isSms,
|
||||
final int subId,
|
||||
final boolean isSend) {
|
||||
// We only show notifications for two cases, i.e. when mobile data is off or when we are
|
||||
// in airplane mode, both of which fail fast with permanent failures.
|
||||
if (!success && status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
|
||||
final PhoneUtils phoneUtils = PhoneUtils.get(subId);
|
||||
if (phoneUtils.isAirplaneModeOn()) {
|
||||
if (isSend) {
|
||||
showToast(R.string.send_message_failure_airplane_mode);
|
||||
} else {
|
||||
showToast(R.string.download_message_failure_airplane_mode);
|
||||
}
|
||||
return;
|
||||
} else if (!isSms && !phoneUtils.isMobileDataEnabled()) {
|
||||
if (isSend) {
|
||||
showToast(R.string.send_message_failure_no_data);
|
||||
} else {
|
||||
showToast(R.string.download_message_failure_no_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (AccessibilityUtil.isTouchExplorationEnabled(Factory.get().getApplicationContext())) {
|
||||
final boolean isFocusedConversation = DataModel.get().isFocusedConversation(conversationId);
|
||||
if (isFocusedConversation && success) {
|
||||
// Using View.announceForAccessibility may be preferable, but we do not have a
|
||||
// View, and so we use a toast instead.
|
||||
showToast(isSend ? R.string.send_message_success
|
||||
: R.string.download_message_success);
|
||||
return;
|
||||
}
|
||||
|
||||
// {@link MessageNotificationState#checkFailedMessages} does not post a notification for
|
||||
// failures in observable conversations. For accessibility, we provide an indication
|
||||
// here.
|
||||
final boolean isObservableConversation = DataModel.get().isNewMessageObservable(
|
||||
conversationId);
|
||||
if (isObservableConversation && !success) {
|
||||
showToast(isSend ? R.string.send_message_failure
|
||||
: R.string.download_message_failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onMessageReceived(final String conversationId,
|
||||
@Nullable final ParticipantData sender, @Nullable final MessageData message) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
if (AccessibilityUtil.isTouchExplorationEnabled(context)) {
|
||||
final boolean isFocusedConversation = DataModel.get().isFocusedConversation(
|
||||
conversationId);
|
||||
if (isFocusedConversation) {
|
||||
final Resources res = context.getResources();
|
||||
final String senderDisplayName = (sender == null)
|
||||
? res.getString(R.string.unknown_sender) : sender.getDisplayName(false);
|
||||
final String announcement = res.getString(
|
||||
R.string.incoming_message_announcement, senderDisplayName,
|
||||
(message == null) ? "" : message.getMessageText());
|
||||
showToast(announcement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onConversationDeleted() {
|
||||
showToast(R.string.conversation_deleted);
|
||||
}
|
||||
|
||||
private static void showToast(final int messageResId) {
|
||||
ThreadUtil.getMainThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(getApplicationContext(),
|
||||
getApplicationContext().getString(messageResId), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void showToast(final String message) {
|
||||
ThreadUtil.getMainThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Context getApplicationContext() {
|
||||
return Factory.get().getApplicationContext();
|
||||
}
|
||||
|
||||
private static class UpdateDestinationBlockedActionToast
|
||||
implements UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener {
|
||||
private final Context mContext;
|
||||
|
||||
UpdateDestinationBlockedActionToast(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateDestinationBlockedAction(
|
||||
final UpdateDestinationBlockedAction action,
|
||||
final boolean success,
|
||||
final boolean block,
|
||||
final String destination) {
|
||||
if (success) {
|
||||
Toast.makeText(mContext,
|
||||
block
|
||||
? R.string.update_destination_blocked
|
||||
: R.string.update_destination_unblocked,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
|
||||
makeUpdateDestinationBlockedActionListener(final Context context) {
|
||||
return new UpdateDestinationBlockedActionToast(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DataModelException;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.widget.WidgetConversationProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Action used to delete a conversation.
|
||||
*/
|
||||
public class DeleteConversationAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
public static void deleteConversation(final String conversationId, final long cutoffTimestamp) {
|
||||
final DeleteConversationAction action = new DeleteConversationAction(conversationId,
|
||||
cutoffTimestamp);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_CUTOFF_TIMESTAMP = "cutoff_timestamp";
|
||||
|
||||
private DeleteConversationAction(final String conversationId, final long cutoffTimestamp) {
|
||||
super();
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
// TODO: Should we set cuttoff timestamp to prevent us deleting new messages?
|
||||
actionParameters.putLong(KEY_CUTOFF_TIMESTAMP, cutoffTimestamp);
|
||||
}
|
||||
|
||||
// Delete conversation from both the local DB and telephony in the background so sync cannot
|
||||
// run concurrently and incorrectly try to recreate the conversation's messages locally. The
|
||||
// telephony database can sometimes be quite slow to delete conversations, so we delete from
|
||||
// the local DB first, notify the UI, and then delete from telephony.
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() throws DataModelException {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final long cutoffTimestamp = actionParameters.getLong(KEY_CUTOFF_TIMESTAMP);
|
||||
|
||||
if (!TextUtils.isEmpty(conversationId)) {
|
||||
// First find the thread id for this conversation.
|
||||
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
|
||||
|
||||
if (BugleDatabaseOperations.deleteConversation(db, conversationId, cutoffTimestamp)) {
|
||||
LogUtil.i(TAG, "DeleteConversationAction: Deleted local conversation "
|
||||
+ conversationId);
|
||||
|
||||
BugleActionToasts.onConversationDeleted();
|
||||
|
||||
// Remove notifications if necessary
|
||||
BugleNotifications.update(true /* silent */, null /* conversationId */,
|
||||
BugleNotifications.UPDATE_MESSAGES);
|
||||
|
||||
// We have changed the conversation list
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
|
||||
// Notify the widget the conversation is deleted so it can go into its configure state.
|
||||
WidgetConversationProvider.notifyConversationDeleted(
|
||||
Factory.get().getApplicationContext(),
|
||||
conversationId);
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Could not delete local conversation "
|
||||
+ conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now delete from telephony DB. MmsSmsProvider throws an exception if the thread id is
|
||||
// less than 0. If it's greater than zero, it will delete all messages with that thread
|
||||
// id, even if there's no corresponding row in the threads table.
|
||||
if (threadId >= 0) {
|
||||
final int count = MmsUtils.deleteThread(threadId, cutoffTimestamp);
|
||||
if (count > 0) {
|
||||
LogUtil.i(TAG, "DeleteConversationAction: Deleted telephony thread "
|
||||
+ threadId + " (cutoffTimestamp = " + cutoffTimestamp + ")");
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Could not delete thread from "
|
||||
+ "telephony: conversationId = " + conversationId + ", thread id = "
|
||||
+ threadId);
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Local conversation " + conversationId
|
||||
+ " has an invalid telephony thread id; will delete messages individually");
|
||||
deleteConversationMessagesFromTelephony();
|
||||
}
|
||||
} else {
|
||||
LogUtil.e(TAG, "DeleteConversationAction: conversationId is empty");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the telephony messages for the local conversation being deleted.
|
||||
* <p>
|
||||
* This is a fallback used when the conversation is not associated with any telephony thread,
|
||||
* or its thread id is invalid (e.g. negative). This is not common, but can happen sometimes
|
||||
* (e.g. the Unknown Sender conversation). In the usual case of deleting a conversation, we
|
||||
* don't need this because the telephony provider automatically deletes messages when a thread
|
||||
* is deleted.
|
||||
*/
|
||||
private void deleteConversationMessagesFromTelephony() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
Assert.notNull(conversationId);
|
||||
|
||||
final List<Uri> messageUris = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
new String[] { MessageColumns.SMS_MESSAGE_URI },
|
||||
MessageColumns.CONVERSATION_ID + "=?",
|
||||
new String[] { conversationId },
|
||||
null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
String messageUri = cursor.getString(0);
|
||||
try {
|
||||
messageUris.add(Uri.parse(messageUri));
|
||||
} catch (Exception e) {
|
||||
LogUtil.e(TAG, "DeleteConversationAction: Could not parse message uri "
|
||||
+ messageUri);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
for (Uri messageUri : messageUris) {
|
||||
int count = MmsUtils.deleteMessage(messageUri);
|
||||
if (count > 0) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "DeleteConversationAction: Deleted telephony message "
|
||||
+ messageUri);
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Could not delete telephony message "
|
||||
+ messageUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
requestBackgroundWork();
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeleteConversationAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<DeleteConversationAction> CREATOR
|
||||
= new Parcelable.Creator<DeleteConversationAction>() {
|
||||
@Override
|
||||
public DeleteConversationAction createFromParcel(final Parcel in) {
|
||||
return new DeleteConversationAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeleteConversationAction[] newArray(final int size) {
|
||||
return new DeleteConversationAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to delete a single message.
|
||||
*/
|
||||
public class DeleteMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
public static void deleteMessage(final String messageId) {
|
||||
final DeleteMessageAction action = new DeleteMessageAction(messageId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
private DeleteMessageAction(final String messageId) {
|
||||
super();
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
}
|
||||
|
||||
// Doing this work in the background so that we're not competing with sync
|
||||
// which could bring the deleted message back to life between the time we deleted
|
||||
// it locally and deleted it in telephony (sync is also done on doBackgroundWork).
|
||||
//
|
||||
// Previously this block of code deleted from telephony first but that can be very
|
||||
// slow (on the order of seconds) so this was modified to first delete locally, trigger
|
||||
// the UI update, then delete from telephony.
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// First find the thread id for this conversation.
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
if (!TextUtils.isEmpty(messageId)) {
|
||||
// Check message still exists
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
if (message != null) {
|
||||
// Delete from local DB
|
||||
int count = BugleDatabaseOperations.deleteMessage(db, messageId);
|
||||
if (count > 0) {
|
||||
LogUtil.i(TAG, "DeleteMessageAction: Deleted local message "
|
||||
+ messageId);
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteMessageAction: Could not delete local message "
|
||||
+ messageId);
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
// We may have changed the conversation list
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
|
||||
final Uri messageUri = message.getSmsMessageUri();
|
||||
if (messageUri != null) {
|
||||
// Delete from telephony DB
|
||||
count = MmsUtils.deleteMessage(messageUri);
|
||||
if (count > 0) {
|
||||
LogUtil.i(TAG, "DeleteMessageAction: Deleted telephony message "
|
||||
+ messageUri);
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteMessageAction: Could not delete message from "
|
||||
+ "telephony: messageId = " + messageId + ", telephony uri = "
|
||||
+ messageUri);
|
||||
}
|
||||
} else {
|
||||
LogUtil.i(TAG, "DeleteMessageAction: Local message " + messageId
|
||||
+ " has no telephony uri.");
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteMessageAction: Message " + messageId + " no longer exists");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the message.
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
requestBackgroundWork();
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeleteMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<DeleteMessageAction> CREATOR
|
||||
= new Parcelable.Creator<DeleteMessageAction>() {
|
||||
@Override
|
||||
public DeleteMessageAction createFromParcel(final Parcel in) {
|
||||
return new DeleteMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeleteMessageAction[] newArray(final int size) {
|
||||
return new DeleteMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Downloads an MMS message.
|
||||
* <p>
|
||||
* This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
|
||||
* access the EXTRA_* fields for setting up the 'downloaded' pending intent.
|
||||
*/
|
||||
public class DownloadMmsAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Interface for DownloadMmsAction listeners
|
||||
*/
|
||||
public interface DownloadMmsActionListener {
|
||||
@RunsOnMainThread
|
||||
abstract void onDownloadMessageStarting(final ActionMonitor monitor,
|
||||
final Object data, final MessageData message);
|
||||
@RunsOnMainThread
|
||||
abstract void onDownloadMessageSucceeded(final ActionMonitor monitor,
|
||||
final Object data, final MessageData message);
|
||||
@RunsOnMainThread
|
||||
abstract void onDownloadMessageFailed(final ActionMonitor monitor,
|
||||
final Object data, final MessageData message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue download of an mms notification message (can only be called during execute of action)
|
||||
*/
|
||||
static boolean queueMmsForDownloadInBackground(final String messageId,
|
||||
final Action processingAction) {
|
||||
// When this method is being called, it is always from auto download
|
||||
final DownloadMmsAction action = new DownloadMmsAction();
|
||||
// This could queue nothing
|
||||
return action.queueAction(messageId, processingAction);
|
||||
}
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_PARTICIPANT_ID = "participant_id";
|
||||
private static final String KEY_CONTENT_LOCATION = "content_location";
|
||||
private static final String KEY_TRANSACTION_ID = "transaction_id";
|
||||
private static final String KEY_NOTIFICATION_URI = "notification_uri";
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
private static final String KEY_AUTO_DOWNLOAD = "auto_download";
|
||||
private static final String KEY_FAILURE_STATUS = "failure_status";
|
||||
|
||||
// Values we attach to the pending intent that's fired when the message is downloaded.
|
||||
// Only applicable when downloading via the platform APIs on L+.
|
||||
public static final String EXTRA_MESSAGE_ID = "message_id";
|
||||
public static final String EXTRA_CONTENT_URI = "content_uri";
|
||||
public static final String EXTRA_NOTIFICATION_URI = "notification_uri";
|
||||
public static final String EXTRA_SUB_ID = "sub_id";
|
||||
public static final String EXTRA_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
public static final String EXTRA_TRANSACTION_ID = "transaction_id";
|
||||
public static final String EXTRA_CONTENT_LOCATION = "content_location";
|
||||
public static final String EXTRA_AUTO_DOWNLOAD = "auto_download";
|
||||
public static final String EXTRA_RECEIVED_TIMESTAMP = "received_timestamp";
|
||||
public static final String EXTRA_CONVERSATION_ID = "conversation_id";
|
||||
public static final String EXTRA_PARTICIPANT_ID = "participant_id";
|
||||
public static final String EXTRA_STATUS_IF_FAILED = "status_if_failed";
|
||||
|
||||
private DownloadMmsAction() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
Assert.fail("DownloadMmsAction must be queued rather than started");
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean queueAction(final String messageId, final Action processingAction) {
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
// Read the message from local db
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
if (message != null && message.canDownloadMessage()) {
|
||||
final Uri notificationUri = message.getSmsMessageUri();
|
||||
final String conversationId = message.getConversationId();
|
||||
final int status = message.getStatus();
|
||||
|
||||
final String selfId = message.getSelfId();
|
||||
final ParticipantData self = BugleDatabaseOperations
|
||||
.getExistingParticipant(db, selfId);
|
||||
final int subId = self.getSubId();
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putString(KEY_PARTICIPANT_ID, message.getParticipantId());
|
||||
actionParameters.putString(KEY_CONTENT_LOCATION, message.getMmsContentLocation());
|
||||
actionParameters.putString(KEY_TRANSACTION_ID, message.getMmsTransactionId());
|
||||
actionParameters.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
|
||||
actionParameters.putBoolean(KEY_AUTO_DOWNLOAD, isAutoDownload(status));
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
if (message.getInDownloadWindow(now)) {
|
||||
// We can still retry
|
||||
actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());
|
||||
|
||||
final int downloadingStatus = getDownloadingStatus(status);
|
||||
// Update message status to indicate downloading.
|
||||
updateMessageStatus(notificationUri, messageId, conversationId,
|
||||
downloadingStatus, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED);
|
||||
// Pre-compute the next status when failed so we don't have to load from db again
|
||||
actionParameters.putInt(KEY_FAILURE_STATUS, getFailureStatus(downloadingStatus));
|
||||
|
||||
// Actual download happens in background
|
||||
processingAction.requestBackgroundWork(this);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG,
|
||||
"DownloadMmsAction: Queued download of MMS message " + messageId);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
LogUtil.w(TAG, "DownloadMmsAction: Download of MMS message " + messageId
|
||||
+ " failed (outside download window)");
|
||||
|
||||
// Retries depleted and we failed. Update the message status so we won't retry again
|
||||
updateMessageStatus(notificationUri, messageId, conversationId,
|
||||
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED,
|
||||
MessageData.RAW_TELEPHONY_STATUS_UNDEFINED);
|
||||
if (status == MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD) {
|
||||
// For auto download failure, we should send a DEFERRED NotifyRespInd
|
||||
// to carrier to indicate we will manual download later
|
||||
ProcessDownloadedMmsAction.sendDeferredRespStatus(
|
||||
messageId, message.getMmsTransactionId(),
|
||||
message.getMmsContentLocation(), subId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out the auto download state of this message based on its starting status
|
||||
*
|
||||
* @param status The starting status of the message.
|
||||
* @return True if this is a message doing auto downloading, false otherwise
|
||||
*/
|
||||
private static boolean isAutoDownload(final int status) {
|
||||
switch (status) {
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
|
||||
return false;
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
||||
return true;
|
||||
default:
|
||||
Assert.fail("isAutoDownload: invalid input status " + status);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the corresponding downloading status based on the starting status of the message
|
||||
*
|
||||
* @param status The starting status of the message.
|
||||
* @return The downloading status
|
||||
*/
|
||||
private static int getDownloadingStatus(final int status) {
|
||||
switch (status) {
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING;
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING;
|
||||
default:
|
||||
Assert.fail("isAutoDownload: invalid input status " + status);
|
||||
return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the corresponding failed status based on the current downloading status
|
||||
*
|
||||
* @param status The downloading status
|
||||
* @return The status the message should have if downloading failed
|
||||
*/
|
||||
private static int getFailureStatus(final int status) {
|
||||
switch (status) {
|
||||
case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD;
|
||||
case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD;
|
||||
default:
|
||||
Assert.fail("isAutoDownload: invalid input status " + status);
|
||||
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final Uri notificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI);
|
||||
final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD);
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID);
|
||||
final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS);
|
||||
|
||||
final long receivedTimestampRoundedToSecond =
|
||||
1000 * ((System.currentTimeMillis() + 500) / 1000);
|
||||
|
||||
LogUtil.i(TAG, "DownloadMmsAction: Downloading MMS message " + messageId
|
||||
+ " (" + (autoDownload ? "auto" : "manual") + ")");
|
||||
|
||||
// Bundle some values we'll need after the message is downloaded (via platform APIs)
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putString(EXTRA_CONVERSATION_ID, conversationId);
|
||||
extras.putString(EXTRA_PARTICIPANT_ID, participantId);
|
||||
extras.putInt(EXTRA_STATUS_IF_FAILED, statusIfFailed);
|
||||
|
||||
// Start the download
|
||||
final MmsUtils.StatusPlusUri status = MmsUtils.downloadMmsMessage(context,
|
||||
notificationUri, subId, subPhoneNumber, transactionId, contentLocation,
|
||||
autoDownload, receivedTimestampRoundedToSecond / 1000L, extras);
|
||||
if (status == MmsUtils.STATUS_PENDING) {
|
||||
// Async download; no status yet
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "DownloadMmsAction: Downloading MMS message " + messageId
|
||||
+ " asynchronously; waiting for pending intent to signal completion");
|
||||
}
|
||||
} else {
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(receivedTimestampRoundedToSecond);
|
||||
// Handle downloaded message
|
||||
ProcessDownloadedMmsAction.processMessageDownloadFastFailed(messageId,
|
||||
notificationUri, conversationId, participantId, contentLocation, subId,
|
||||
subPhoneNumber, statusIfFailed, autoDownload, transactionId,
|
||||
status.resultCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
// Nothing to do here; post-download actions handled by ProcessDownloadedMmsAction
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundFailure() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID);
|
||||
final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS);
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID);
|
||||
|
||||
ProcessDownloadedMmsAction.processDownloadActionFailure(messageId,
|
||||
MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
|
||||
conversationId, participantId, statusIfFailed, subId, transactionId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static void updateMessageStatus(final Uri messageUri, final String messageId,
|
||||
final String conversationId, final int status, final int rawStatus) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
// Downloading status just kept in local DB but need to fix up telephony DB first
|
||||
if (status == MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING ||
|
||||
status == MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING) {
|
||||
MmsUtils.clearMmsStatus(context, messageUri);
|
||||
}
|
||||
// Then mark downloading status in our local DB
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.STATUS, status);
|
||||
values.put(MessageColumns.RAW_TELEPHONY_STATUS, rawStatus);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
BugleDatabaseOperations.updateMessageRowIfExists(db, messageId, values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
|
||||
private DownloadMmsAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<DownloadMmsAction> CREATOR
|
||||
= new Parcelable.Creator<DownloadMmsAction>() {
|
||||
@Override
|
||||
public DownloadMmsAction createFromParcel(final Parcel in) {
|
||||
return new DownloadMmsAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadMmsAction[] newArray(final int size) {
|
||||
return new DownloadMmsAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.util.DebugUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class DumpDatabaseAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
public static final String DUMP_NAME = "db_copy.db";
|
||||
private static final int BUFFER_SIZE = 16384;
|
||||
|
||||
/**
|
||||
* Copy the database to external storage
|
||||
*/
|
||||
public static void dumpDatabase() {
|
||||
final DumpDatabaseAction action = new DumpDatabaseAction();
|
||||
action.start();
|
||||
}
|
||||
|
||||
private DumpDatabaseAction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final String dbName = DatabaseHelper.DATABASE_NAME;
|
||||
BufferedOutputStream bos = null;
|
||||
BufferedInputStream bis = null;
|
||||
|
||||
long originalSize = 0;
|
||||
final File inFile = context.getDatabasePath(dbName);
|
||||
if (inFile.exists() && inFile.isFile()) {
|
||||
originalSize = inFile.length();
|
||||
}
|
||||
final File outFile = DebugUtils.getDebugFile(DUMP_NAME, true);
|
||||
if (outFile != null) {
|
||||
int totalBytes = 0;
|
||||
try {
|
||||
bos = new BufferedOutputStream(new FileOutputStream(outFile));
|
||||
bis = new BufferedInputStream(new FileInputStream(inFile));
|
||||
|
||||
final byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = bis.read(buffer)) > 0) {
|
||||
bos.write(buffer, 0, bytesRead);
|
||||
totalBytes += bytesRead;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
LogUtil.w(TAG, "Exception copying the database;"
|
||||
+ " destination may not be complete.", e);
|
||||
} finally {
|
||||
if (bos != null) {
|
||||
try {
|
||||
bos.close();
|
||||
} catch (final IOException e) {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
if (bis != null) {
|
||||
try {
|
||||
bis.close();
|
||||
} catch (final IOException e) {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
DebugUtils.ensureReadable(outFile);
|
||||
LogUtil.i(TAG, "Dump complete; orig size: " + originalSize +
|
||||
", copy size: " + totalBytes);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DumpDatabaseAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<DumpDatabaseAction> CREATOR
|
||||
= new Parcelable.Creator<DumpDatabaseAction>() {
|
||||
@Override
|
||||
public DumpDatabaseAction createFromParcel(final Parcel in) {
|
||||
return new DumpDatabaseAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DumpDatabaseAction[] newArray(final int size) {
|
||||
return new DumpDatabaseAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to fixup actively downloading or sending status at startup - just in case we
|
||||
* crash - never run this when a message might actually be sending or downloading.
|
||||
*/
|
||||
public class FixupMessageStatusOnStartupAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
public static void fixupMessageStatus() {
|
||||
final FixupMessageStatusOnStartupAction action = new FixupMessageStatusOnStartupAction();
|
||||
action.start();
|
||||
}
|
||||
|
||||
private FixupMessageStatusOnStartupAction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
// Now mark any messages in active sending or downloading state as inactive
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
int downloadFailedCnt = 0;
|
||||
int sendFailedCnt = 0;
|
||||
try {
|
||||
// For both sending and downloading messages, let's assume they failed.
|
||||
// For MMS sent/downloaded via platform, the sent/downloaded pending intent
|
||||
// may come back. That will update the message. User may see the message
|
||||
// in wrong status within a short window if that happens. But this should
|
||||
// rarely happen. This is a simple solution to situations like app gets killed
|
||||
// while the pending intent is still in the fly. Alternatively, we could
|
||||
// keep the status for platform sent/downloaded MMS and timeout these messages.
|
||||
// But that is much more complex.
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED);
|
||||
downloadFailedCnt += db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
|
||||
new String[]{
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING)
|
||||
});
|
||||
values.clear();
|
||||
|
||||
values.clear();
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_OUTGOING_FAILED);
|
||||
sendFailedCnt = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
|
||||
new String[]{
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)
|
||||
});
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "Fixup: Send failed - " + sendFailedCnt
|
||||
+ " Download failed - " + downloadFailedCnt);
|
||||
|
||||
// Don't send contentObserver notifications as displayed text should not change
|
||||
return null;
|
||||
}
|
||||
|
||||
private FixupMessageStatusOnStartupAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<FixupMessageStatusOnStartupAction> CREATOR
|
||||
= new Parcelable.Creator<FixupMessageStatusOnStartupAction>() {
|
||||
@Override
|
||||
public FixupMessageStatusOnStartupAction createFromParcel(final Parcel in) {
|
||||
return new FixupMessageStatusOnStartupAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FixupMessageStatusOnStartupAction[] newArray(final int size) {
|
||||
return new FixupMessageStatusOnStartupAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
|
||||
import com.android.messaging.datamodel.data.LaunchConversationData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Action used to get or create a conversation for a list of conversation participants.
|
||||
*/
|
||||
public class GetOrCreateConversationAction extends Action implements Parcelable {
|
||||
/**
|
||||
* Interface for GetOrCreateConversationAction listeners
|
||||
*/
|
||||
public interface GetOrCreateConversationActionListener {
|
||||
@RunsOnMainThread
|
||||
abstract void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
|
||||
final Object data, final String conversationId);
|
||||
|
||||
@RunsOnMainThread
|
||||
abstract void onGetOrCreateConversationFailed(final ActionMonitor monitor,
|
||||
final Object data);
|
||||
}
|
||||
|
||||
public static GetOrCreateConversationActionMonitor getOrCreateConversation(
|
||||
final ArrayList<ParticipantData> participants, final Object data,
|
||||
final GetOrCreateConversationActionListener listener) {
|
||||
final GetOrCreateConversationActionMonitor monitor = new
|
||||
GetOrCreateConversationActionMonitor(data, listener);
|
||||
final GetOrCreateConversationAction action = new GetOrCreateConversationAction(participants,
|
||||
monitor.getActionKey());
|
||||
action.start(monitor);
|
||||
return monitor;
|
||||
}
|
||||
|
||||
|
||||
public static GetOrCreateConversationActionMonitor getOrCreateConversation(
|
||||
final String[] recipients, final Object data, final LaunchConversationData listener) {
|
||||
final ArrayList<ParticipantData> participants = new ArrayList<>();
|
||||
for (String recipient : recipients) {
|
||||
recipient = recipient.trim();
|
||||
if (!TextUtils.isEmpty(recipient)) {
|
||||
participants.add(ParticipantData.getFromRawPhoneBySystemLocale(recipient));
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "getOrCreateConversation hit empty recipient");
|
||||
}
|
||||
}
|
||||
return getOrCreateConversation(participants, data, listener);
|
||||
}
|
||||
|
||||
private static final String KEY_PARTICIPANTS_LIST = "participants_list";
|
||||
|
||||
private GetOrCreateConversationAction(final ArrayList<ParticipantData> participants,
|
||||
final String actionKey) {
|
||||
super(actionKey);
|
||||
actionParameters.putParcelableArrayList(KEY_PARTICIPANTS_LIST, participants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the conversation or create a new one.
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// First find the thread id for this list of participants.
|
||||
final ArrayList<ParticipantData> participants =
|
||||
actionParameters.getParcelableArrayList(KEY_PARTICIPANTS_LIST);
|
||||
BugleDatabaseOperations.sanitizeConversationParticipants(participants);
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
|
||||
|
||||
final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
|
||||
recipients);
|
||||
|
||||
if (threadId < 0) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't create a threadId in SMS db for numbers : " +
|
||||
LogUtil.sanitizePII(recipients.toString()));
|
||||
// TODO: Add a better way to indicate an error from executeAction.
|
||||
return null;
|
||||
}
|
||||
|
||||
final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
|
||||
false, participants, false, false, null);
|
||||
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* A monitor that notifies a listener upon completion
|
||||
*/
|
||||
public static class GetOrCreateConversationActionMonitor extends ActionMonitor
|
||||
implements ActionCompletedListener {
|
||||
private final GetOrCreateConversationActionListener mListener;
|
||||
|
||||
GetOrCreateConversationActionMonitor(final Object data,
|
||||
final GetOrCreateConversationActionListener listener) {
|
||||
super(STATE_CREATED, generateUniqueActionKey("GetOrCreateConversationAction"), data);
|
||||
setCompletedListener(this);
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionSucceeded(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
if (result == null) {
|
||||
mListener.onGetOrCreateConversationFailed(monitor, data);
|
||||
} else {
|
||||
mListener.onGetOrCreateConversationSucceeded(monitor, data, (String) result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionFailed(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
// TODO: Currently onActionFailed is only called if there is an error in
|
||||
// processing requests, not for errors in the local processing.
|
||||
Assert.fail("Unreachable");
|
||||
mListener.onGetOrCreateConversationFailed(monitor, data);
|
||||
}
|
||||
}
|
||||
|
||||
private GetOrCreateConversationAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<GetOrCreateConversationAction> CREATOR
|
||||
= new Parcelable.Creator<GetOrCreateConversationAction>() {
|
||||
@Override
|
||||
public GetOrCreateConversationAction createFromParcel(final Parcel in) {
|
||||
return new GetOrCreateConversationAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetOrCreateConversationAction[] newArray(final int size) {
|
||||
return new GetOrCreateConversationAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.sms.SmsReleaseStorage;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Action used to handle low storage related issues on the device.
|
||||
*/
|
||||
public class HandleLowStorageAction extends Action implements Parcelable {
|
||||
private static final int SUB_OP_CODE_CLEAR_MEDIA_MESSAGES = 100;
|
||||
private static final int SUB_OP_CODE_CLEAR_OLD_MESSAGES = 101;
|
||||
|
||||
public static void handleDeleteMediaMessages(final long durationInMillis) {
|
||||
final HandleLowStorageAction action = new HandleLowStorageAction(
|
||||
SUB_OP_CODE_CLEAR_MEDIA_MESSAGES, durationInMillis);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void handleDeleteOldMessages(final long durationInMillis) {
|
||||
final HandleLowStorageAction action = new HandleLowStorageAction(
|
||||
SUB_OP_CODE_CLEAR_OLD_MESSAGES, durationInMillis);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_SUB_OP_CODE = "sub_op_code";
|
||||
private static final String KEY_CUTOFF_DURATION_MILLIS = "cutoff_duration_millis";
|
||||
|
||||
private HandleLowStorageAction(final int subOpcode, final long durationInMillis) {
|
||||
super();
|
||||
actionParameters.putInt(KEY_SUB_OP_CODE, subOpcode);
|
||||
actionParameters.putLong(KEY_CUTOFF_DURATION_MILLIS, durationInMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final int subOpCode = actionParameters.getInt(KEY_SUB_OP_CODE);
|
||||
final long durationInMillis = actionParameters.getLong(KEY_CUTOFF_DURATION_MILLIS);
|
||||
switch (subOpCode) {
|
||||
case SUB_OP_CODE_CLEAR_MEDIA_MESSAGES:
|
||||
SmsReleaseStorage.deleteMessages(0, durationInMillis);
|
||||
break;
|
||||
|
||||
case SUB_OP_CODE_CLEAR_OLD_MESSAGES:
|
||||
SmsReleaseStorage.deleteMessages(1, durationInMillis);
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unsupported action type!");
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private HandleLowStorageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<HandleLowStorageAction> CREATOR
|
||||
= new Parcelable.Creator<HandleLowStorageAction>() {
|
||||
@Override
|
||||
public HandleLowStorageAction createFromParcel(final Parcel in) {
|
||||
return new HandleLowStorageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandleLowStorageAction[] newArray(final int size) {
|
||||
return new HandleLowStorageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Action used to convert a draft message to an outgoing message. Its writes SMS messages to
|
||||
* the telephony db, but {@link SendMessageAction} is responsible for inserting MMS message into
|
||||
* the telephony DB. The latter also does the actual sending of the message in the background.
|
||||
* The latter is also responsible for re-sending a failed message.
|
||||
*/
|
||||
public class InsertNewMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static long sLastSentMessageTimestamp = -1;
|
||||
|
||||
/**
|
||||
* Insert message (no listener)
|
||||
*/
|
||||
public static void insertNewMessage(final MessageData message) {
|
||||
final InsertNewMessageAction action = new InsertNewMessageAction(message);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert message (no listener) with a given non-default subId.
|
||||
*/
|
||||
public static void insertNewMessage(final MessageData message, final int subId) {
|
||||
Assert.isFalse(subId == ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final InsertNewMessageAction action = new InsertNewMessageAction(message, subId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert message (no listener)
|
||||
*/
|
||||
public static void insertNewMessage(final int subId, final String recipients,
|
||||
final String messageText, final String subject) {
|
||||
final InsertNewMessageAction action = new InsertNewMessageAction(
|
||||
subId, recipients, messageText, subject);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static long getLastSentMessageTimestamp() {
|
||||
return sLastSentMessageTimestamp;
|
||||
}
|
||||
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_MESSAGE = "message";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_MESSAGE_TEXT = "message_text";
|
||||
private static final String KEY_SUBJECT_TEXT = "subject_text";
|
||||
|
||||
private InsertNewMessageAction(final MessageData message) {
|
||||
this(message, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
}
|
||||
|
||||
private InsertNewMessageAction(final MessageData message, final int subId) {
|
||||
super();
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
}
|
||||
|
||||
private InsertNewMessageAction(final int subId, final String recipients,
|
||||
final String messageText, final String subject) {
|
||||
super();
|
||||
if (TextUtils.isEmpty(recipients) || TextUtils.isEmpty(messageText)) {
|
||||
Assert.fail("InsertNewMessageAction: Can't have empty recipients or message");
|
||||
}
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
actionParameters.putString(KEY_RECIPIENTS, recipients);
|
||||
actionParameters.putString(KEY_MESSAGE_TEXT, messageText);
|
||||
actionParameters.putString(KEY_SUBJECT_TEXT, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add message to database in pending state and queue actual sending
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
LogUtil.i(TAG, "InsertNewMessageAction: inserting new message");
|
||||
MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
|
||||
if (message == null) {
|
||||
LogUtil.i(TAG, "InsertNewMessageAction: Creating MessageData with provided data");
|
||||
message = createMessage();
|
||||
if (message == null) {
|
||||
LogUtil.w(TAG, "InsertNewMessageAction: Could not create MessageData");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = message.getConversationId();
|
||||
|
||||
final ParticipantData self = getSelf(db, conversationId, message);
|
||||
if (self == null) {
|
||||
return null;
|
||||
}
|
||||
message.bindSelfId(self.getId());
|
||||
// If the user taps the Send button before the conversation draft is created/loaded by
|
||||
// ReadDraftDataAction (maybe the action service thread was busy), the MessageData may not
|
||||
// have the participant id set. It should be equal to the self id, so we'll use that.
|
||||
if (message.getParticipantId() == null) {
|
||||
message.bindParticipantId(self.getId());
|
||||
}
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
|
||||
if (recipients.size() < 1) {
|
||||
LogUtil.w(TAG, "InsertNewMessageAction: message recipients is empty");
|
||||
return null;
|
||||
}
|
||||
final int subId = self.getSubId();
|
||||
|
||||
// TODO: Work out whether to send with SMS or MMS (taking into account recipients)?
|
||||
final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
|
||||
if (isSms) {
|
||||
String sendingConversationId = conversationId;
|
||||
if (recipients.size() > 1) {
|
||||
// Broadcast SMS - put message in "fake conversation" before farming out to real 1:1
|
||||
final long laterTimestamp = timestamp + 1;
|
||||
// Send a single message
|
||||
insertBroadcastSmsMessage(conversationId, message, subId,
|
||||
laterTimestamp, recipients);
|
||||
|
||||
sendingConversationId = null;
|
||||
}
|
||||
|
||||
for (final String recipient : recipients) {
|
||||
// Start actual sending
|
||||
insertSendingSmsMessage(message, subId, recipient,
|
||||
timestamp, sendingConversationId);
|
||||
}
|
||||
|
||||
// Can now clear draft from conversation (deleting attachments if necessary)
|
||||
BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
|
||||
null /* message */, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
|
||||
} else {
|
||||
final long timestampRoundedToSecond = 1000 * ((timestamp + 500) / 1000);
|
||||
// Write place holder message directly referencing parts from the draft
|
||||
final MessageData messageToSend = insertSendingMmsMessage(conversationId,
|
||||
message, timestampRoundedToSecond);
|
||||
|
||||
// Can now clear draft from conversation (preserving attachments which are now
|
||||
// referenced by messageToSend)
|
||||
BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
|
||||
messageToSend, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
|
||||
}
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private ParticipantData getSelf(
|
||||
final DatabaseWrapper db, final String conversationId, final MessageData message) {
|
||||
ParticipantData self;
|
||||
// Check if we are asked to bind to a non-default subId. This is directly passed in from
|
||||
// the UI thread so that the sub id may be locked as soon as the user clicks on the Send
|
||||
// button.
|
||||
final int requestedSubId = actionParameters.getInt(
|
||||
KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
if (requestedSubId != ParticipantData.DEFAULT_SELF_SUB_ID) {
|
||||
self = BugleDatabaseOperations.getOrCreateSelf(db, requestedSubId);
|
||||
} else {
|
||||
String selfId = message.getSelfId();
|
||||
if (selfId == null) {
|
||||
// The conversation draft provides no self id hint, meaning that 1) conversation
|
||||
// self id was not loaded AND 2) the user didn't pick a SIM from the SIM selector.
|
||||
// In this case, use the conversation's self id.
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, conversationId);
|
||||
if (conversation != null) {
|
||||
selfId = conversation.getSelfId();
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_DATAMODEL_TAG, "Conversation " + conversationId +
|
||||
"already deleted before sending draft message " +
|
||||
message.getMessageId() + ". Aborting InsertNewMessageAction.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// We do not use SubscriptionManager.DEFAULT_SUB_ID for sending a message, so we need
|
||||
// to bind the message to the system default subscription if it's unbound.
|
||||
final ParticipantData unboundSelf = BugleDatabaseOperations.getExistingParticipant(
|
||||
db, selfId);
|
||||
if (unboundSelf.getSubId() == ParticipantData.DEFAULT_SELF_SUB_ID
|
||||
&& OsUtil.isAtLeastL_MR1()) {
|
||||
final int defaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
|
||||
self = BugleDatabaseOperations.getOrCreateSelf(db, defaultSubId);
|
||||
} else {
|
||||
self = unboundSelf;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/** Create MessageData using KEY_RECIPIENTS, KEY_MESSAGE_TEXT and KEY_SUBJECT */
|
||||
private MessageData createMessage() {
|
||||
// First find the thread id for this list of participants.
|
||||
final String recipientsList = actionParameters.getString(KEY_RECIPIENTS);
|
||||
final String messageText = actionParameters.getString(KEY_MESSAGE_TEXT);
|
||||
final String subjectText = actionParameters.getString(KEY_SUBJECT_TEXT);
|
||||
final int subId = actionParameters.getInt(
|
||||
KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
final ArrayList<ParticipantData> participants = new ArrayList<>();
|
||||
for (final String recipient : recipientsList.split(",")) {
|
||||
participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
|
||||
}
|
||||
if (participants.size() == 0) {
|
||||
Assert.fail("InsertNewMessage: Empty participants");
|
||||
return null;
|
||||
}
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
BugleDatabaseOperations.sanitizeConversationParticipants(participants);
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
|
||||
if (recipients.size() == 0) {
|
||||
Assert.fail("InsertNewMessage: Empty recipients");
|
||||
return null;
|
||||
}
|
||||
|
||||
final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
|
||||
recipients);
|
||||
|
||||
if (threadId < 0) {
|
||||
Assert.fail("InsertNewMessage: Couldn't get threadId in SMS db for these recipients: "
|
||||
+ recipients.toString());
|
||||
// TODO: How do we fail the action?
|
||||
return null;
|
||||
}
|
||||
|
||||
final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
|
||||
false, participants, false, false, null);
|
||||
|
||||
final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
|
||||
|
||||
if (TextUtils.isEmpty(subjectText)) {
|
||||
return MessageData.createDraftSmsMessage(conversationId, self.getId(), messageText);
|
||||
} else {
|
||||
return MessageData.createDraftMmsMessage(conversationId, self.getId(), messageText,
|
||||
subjectText);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertBroadcastSmsMessage(final String conversationId,
|
||||
final MessageData message, final int subId, final long laterTimestamp,
|
||||
final ArrayList<String> recipients) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "InsertNewMessageAction: Inserting broadcast SMS message "
|
||||
+ message.getMessageId());
|
||||
}
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Inform sync that message is being added at timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(laterTimestamp);
|
||||
|
||||
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
|
||||
final String address = TextUtils.join(" ", recipients);
|
||||
|
||||
final String messageText = message.getMessageText();
|
||||
// Insert message into telephony database sms message table
|
||||
final Uri messageUri = MmsUtils.insertSmsMessage(context,
|
||||
Telephony.Sms.CONTENT_URI,
|
||||
subId,
|
||||
address,
|
||||
messageText,
|
||||
laterTimestamp,
|
||||
Telephony.Sms.STATUS_COMPLETE,
|
||||
Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
|
||||
if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
message.updateSendingMessage(conversationId, messageUri, laterTimestamp);
|
||||
message.markMessageSent(laterTimestamp);
|
||||
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), laterTimestamp,
|
||||
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "InsertNewMessageAction: Inserted broadcast SMS message "
|
||||
+ message.getMessageId() + ", uri = " + message.getSmsMessageUri());
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
} else {
|
||||
// Ignore error as we only really care about the individual messages?
|
||||
LogUtil.e(TAG,
|
||||
"InsertNewMessageAction: No uri for broadcast SMS " + message.getMessageId()
|
||||
+ " inserted into telephony DB");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert SMS messaging into our database and telephony db.
|
||||
*/
|
||||
private MessageData insertSendingSmsMessage(final MessageData content, final int subId,
|
||||
final String recipient, final long timestamp, final String sendingConversationId) {
|
||||
sLastSentMessageTimestamp = timestamp;
|
||||
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
|
||||
// Inform sync that message is being added at timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(timestamp);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Send a single message
|
||||
long threadId;
|
||||
String conversationId;
|
||||
if (sendingConversationId == null) {
|
||||
// For 1:1 message generated sending broadcast need to look up threadId+conversationId
|
||||
threadId = MmsUtils.getOrCreateSmsThreadId(context, recipient);
|
||||
conversationId = BugleDatabaseOperations.getOrCreateConversationFromRecipient(
|
||||
db, threadId, false /* sender blocked */,
|
||||
ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
|
||||
} else {
|
||||
// Otherwise just look up threadId
|
||||
threadId = BugleDatabaseOperations.getThreadId(db, sendingConversationId);
|
||||
conversationId = sendingConversationId;
|
||||
}
|
||||
|
||||
final String messageText = content.getMessageText();
|
||||
|
||||
// Insert message into telephony database sms message table
|
||||
final Uri messageUri = MmsUtils.insertSmsMessage(context,
|
||||
Telephony.Sms.CONTENT_URI,
|
||||
subId,
|
||||
recipient,
|
||||
messageText,
|
||||
timestamp,
|
||||
Telephony.Sms.STATUS_NONE,
|
||||
Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
|
||||
|
||||
MessageData message = null;
|
||||
if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
message = MessageData.createDraftSmsMessage(conversationId,
|
||||
content.getSelfId(), messageText);
|
||||
message.updateSendingMessage(conversationId, messageUri, timestamp);
|
||||
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
// Do not update the conversation summary to reflect autogenerated 1:1 messages
|
||||
if (sendingConversationId != null) {
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), timestamp,
|
||||
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "InsertNewMessageAction: Inserted SMS message "
|
||||
+ message.getMessageId() + " (uri = " + message.getSmsMessageUri()
|
||||
+ ", timestamp = " + message.getReceivedTimeStamp() + ")");
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
} else {
|
||||
LogUtil.e(TAG, "InsertNewMessageAction: No uri for SMS inserted into telephony DB");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert MMS messaging into our database.
|
||||
*/
|
||||
private MessageData insertSendingMmsMessage(final String conversationId,
|
||||
final MessageData message, final long timestamp) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
final List<MessagePartData> attachmentsUpdated = new ArrayList<>();
|
||||
try {
|
||||
sLastSentMessageTimestamp = timestamp;
|
||||
|
||||
// Insert "draft" message as placeholder until the final message is written to
|
||||
// the telephony db
|
||||
message.updateSendingMessage(conversationId, null/*messageUri*/, timestamp);
|
||||
|
||||
// No need to inform SyncManager as message currently has no Uri...
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), timestamp,
|
||||
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "InsertNewMessageAction: Inserted MMS message "
|
||||
+ message.getMessageId() + " (timestamp = " + timestamp + ")");
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private InsertNewMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<InsertNewMessageAction> CREATOR
|
||||
= new Parcelable.Creator<InsertNewMessageAction>() {
|
||||
@Override
|
||||
public InsertNewMessageAction createFromParcel(final Parcel in) {
|
||||
return new InsertNewMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertNewMessageAction[] newArray(final int size) {
|
||||
return new InsertNewMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony.Threads;
|
||||
import android.provider.Telephony.ThreadsColumns;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.mmslib.SqliteWrapper;
|
||||
import com.android.messaging.util.DebugUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
public class LogTelephonyDatabaseAction extends Action implements Parcelable {
|
||||
// Because we use sanitizePII, we should also use BUGLE_TAG
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
private static final String[] ALL_THREADS_PROJECTION = {
|
||||
Threads._ID,
|
||||
Threads.DATE,
|
||||
Threads.MESSAGE_COUNT,
|
||||
Threads.RECIPIENT_IDS,
|
||||
Threads.SNIPPET,
|
||||
Threads.SNIPPET_CHARSET,
|
||||
Threads.READ,
|
||||
Threads.ERROR,
|
||||
Threads.HAS_ATTACHMENT };
|
||||
|
||||
// Constants from the Telephony Database
|
||||
private static final int ID = 0;
|
||||
private static final int DATE = 1;
|
||||
private static final int MESSAGE_COUNT = 2;
|
||||
private static final int RECIPIENT_IDS = 3;
|
||||
private static final int SNIPPET = 4;
|
||||
private static final int SNIPPET_CHAR_SET = 5;
|
||||
private static final int READ = 6;
|
||||
private static final int ERROR = 7;
|
||||
private static final int HAS_ATTACHMENT = 8;
|
||||
|
||||
/**
|
||||
* Log telephony data to logcat
|
||||
*/
|
||||
public static void dumpDatabase() {
|
||||
final LogTelephonyDatabaseAction action = new LogTelephonyDatabaseAction();
|
||||
action.start();
|
||||
}
|
||||
|
||||
private LogTelephonyDatabaseAction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
|
||||
if (!DebugUtils.isDebugEnabled()) {
|
||||
LogUtil.e(TAG, "Can't log telephony database unless debugging is enabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.w(TAG, "Can't log telephony database unless DEBUG is turned on for TAG: " +
|
||||
TAG);
|
||||
return null;
|
||||
}
|
||||
|
||||
LogUtil.d(TAG, "\n");
|
||||
LogUtil.d(TAG, "Dump of canoncial_addresses table");
|
||||
LogUtil.d(TAG, "*********************************");
|
||||
|
||||
Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
|
||||
Uri.parse("content://mms-sms/canonical-addresses"), null, null, null, null);
|
||||
|
||||
if (cursor == null) {
|
||||
LogUtil.w(TAG, "null Cursor in content://mms-sms/canonical-addresses");
|
||||
} else {
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
long id = cursor.getLong(0);
|
||||
String number = cursor.getString(1);
|
||||
LogUtil.d(TAG, LogUtil.sanitizePII("id: " + id + " number: " + number));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
LogUtil.d(TAG, "\n");
|
||||
LogUtil.d(TAG, "Dump of threads table");
|
||||
LogUtil.d(TAG, "*********************");
|
||||
|
||||
cursor = SqliteWrapper.query(context, context.getContentResolver(),
|
||||
Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build(),
|
||||
ALL_THREADS_PROJECTION, null, null, "date ASC");
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
LogUtil.d(TAG, LogUtil.sanitizePII("threadId: " + cursor.getLong(ID) +
|
||||
" " + ThreadsColumns.DATE + " : " + cursor.getLong(DATE) +
|
||||
" " + ThreadsColumns.MESSAGE_COUNT + " : " + cursor.getInt(MESSAGE_COUNT) +
|
||||
" " + ThreadsColumns.SNIPPET + " : " + cursor.getString(SNIPPET) +
|
||||
" " + ThreadsColumns.READ + " : " + cursor.getInt(READ) +
|
||||
" " + ThreadsColumns.ERROR + " : " + cursor.getInt(ERROR) +
|
||||
" " + ThreadsColumns.HAS_ATTACHMENT + " : " +
|
||||
cursor.getInt(HAS_ATTACHMENT) +
|
||||
" " + ThreadsColumns.RECIPIENT_IDS + " : " +
|
||||
cursor.getString(RECIPIENT_IDS)));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private LogTelephonyDatabaseAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<LogTelephonyDatabaseAction> CREATOR
|
||||
= new Parcelable.Creator<LogTelephonyDatabaseAction>() {
|
||||
@Override
|
||||
public LogTelephonyDatabaseAction createFromParcel(final Parcel in) {
|
||||
return new LogTelephonyDatabaseAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogTelephonyDatabaseAction[] newArray(final int size) {
|
||||
return new LogTelephonyDatabaseAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to mark all the messages in a conversation as read
|
||||
*/
|
||||
public class MarkAsReadAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
|
||||
/**
|
||||
* Mark all the messages as read for a particular conversation.
|
||||
*/
|
||||
public static void markAsRead(final String conversationId) {
|
||||
final MarkAsReadAction action = new MarkAsReadAction(conversationId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private MarkAsReadAction(final String conversationId) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
|
||||
// TODO: Consider doing this in background service to avoid delaying other actions
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Mark all messages in thread as read in telephony
|
||||
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
|
||||
if (threadId != -1) {
|
||||
MmsUtils.updateSmsReadStatus(threadId, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
// Update local db
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.CONVERSATION_ID, conversationId);
|
||||
values.put(MessageColumns.READ, 1);
|
||||
values.put(MessageColumns.SEEN, 1); // if they read it, they saw it
|
||||
|
||||
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
"(" + MessageColumns.READ + " !=1 OR " +
|
||||
MessageColumns.SEEN + " !=1 ) AND " +
|
||||
MessageColumns.CONVERSATION_ID + "=?",
|
||||
new String[] { conversationId });
|
||||
if (count > 0) {
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
// After marking messages as read, update the notifications. This will
|
||||
// clear the now stale notifications.
|
||||
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
|
||||
return null;
|
||||
}
|
||||
|
||||
private MarkAsReadAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MarkAsReadAction> CREATOR
|
||||
= new Parcelable.Creator<MarkAsReadAction>() {
|
||||
@Override
|
||||
public MarkAsReadAction createFromParcel(final Parcel in) {
|
||||
return new MarkAsReadAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MarkAsReadAction[] newArray(final int size) {
|
||||
return new MarkAsReadAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to mark all messages as seen
|
||||
*/
|
||||
public class MarkAsSeenAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
|
||||
/**
|
||||
* Mark all messages as seen.
|
||||
*/
|
||||
public static void markAllAsSeen() {
|
||||
final MarkAsSeenAction action = new MarkAsSeenAction((String) null/*conversationId*/);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all messages of a given conversation as seen.
|
||||
*/
|
||||
public static void markAsSeen(final String conversationId) {
|
||||
final MarkAsSeenAction action = new MarkAsSeenAction(conversationId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* ctor for MarkAsSeenAction.
|
||||
* @param conversationId the conversation id for which to mark as seen, or null to mark all
|
||||
* messages as seen
|
||||
*/
|
||||
public MarkAsSeenAction(final String conversationId) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId =
|
||||
actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final boolean hasSpecificConversation = !TextUtils.isEmpty(conversationId);
|
||||
|
||||
// Everything in telephony should already have the seen bit set.
|
||||
// Possible exception are messages which did not have seen set and
|
||||
// were sync'ed into bugle.
|
||||
|
||||
// Now mark the messages as seen in the bugle db
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.SEEN, 1);
|
||||
|
||||
if (hasSpecificConversation) {
|
||||
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
MessageColumns.SEEN + " != 1 AND " +
|
||||
MessageColumns.CONVERSATION_ID + "=?",
|
||||
new String[] { conversationId });
|
||||
if (count > 0) {
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
} else {
|
||||
db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
MessageColumns.SEEN + " != 1", null/*selectionArgs*/);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
// After marking messages as seen, update the notifications. This will
|
||||
// clear the now stale notifications.
|
||||
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
|
||||
return null;
|
||||
}
|
||||
|
||||
private MarkAsSeenAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MarkAsSeenAction> CREATOR
|
||||
= new Parcelable.Creator<MarkAsSeenAction>() {
|
||||
@Override
|
||||
public MarkAsSeenAction createFromParcel(final Parcel in) {
|
||||
return new MarkAsSeenAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MarkAsSeenAction[] newArray(final int size) {
|
||||
return new MarkAsSeenAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ProcessDeliveryReportAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_URI = "uri";
|
||||
private static final String KEY_STATUS = "status";
|
||||
|
||||
private ProcessDeliveryReportAction(final Uri uri, final int status) {
|
||||
actionParameters.putParcelable(KEY_URI, uri);
|
||||
actionParameters.putInt(KEY_STATUS, status);
|
||||
}
|
||||
|
||||
public static void deliveryReportReceived(final Uri uri, final int status) {
|
||||
final ProcessDeliveryReportAction action = new ProcessDeliveryReportAction(uri, status);
|
||||
action.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Uri smsMessageUri = actionParameters.getParcelable(KEY_URI);
|
||||
final int status = actionParameters.getInt(KEY_STATUS);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final long messageRowId = ContentUris.parseId(smsMessageUri);
|
||||
if (messageRowId < 0) {
|
||||
LogUtil.e(TAG, "ProcessDeliveryReportAction: can't find message");
|
||||
return null;
|
||||
}
|
||||
final long timeSentInMillis = System.currentTimeMillis();
|
||||
// Update telephony provider
|
||||
if (smsMessageUri != null) {
|
||||
MmsUtils.updateSmsStatusAndDateSent(smsMessageUri, status, timeSentInMillis);
|
||||
}
|
||||
|
||||
// Update local message
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
final int bugleStatus = SyncMessageBatch.bugleStatusForSms(true /*outgoing*/,
|
||||
Telephony.Sms.MESSAGE_TYPE_SENT /* type */, status);
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS, bugleStatus);
|
||||
values.put(DatabaseHelper.MessageColumns.SENT_TIMESTAMP,
|
||||
TimeUnit.MILLISECONDS.toMicros(timeSentInMillis));
|
||||
|
||||
final MessageData messageData =
|
||||
BugleDatabaseOperations.readMessageData(db, smsMessageUri);
|
||||
|
||||
// Check the message was not removed before the delivery report comes in
|
||||
if (messageData != null) {
|
||||
Assert.isTrue(smsMessageUri.equals(messageData.getSmsMessageUri()));
|
||||
|
||||
// Row must exist as was just loaded above (on ActionService thread)
|
||||
BugleDatabaseOperations.updateMessageRow(db, messageData.getMessageId(), values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(messageData.getConversationId());
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ProcessDeliveryReportAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessDeliveryReportAction> CREATOR
|
||||
= new Parcelable.Creator<ProcessDeliveryReportAction>() {
|
||||
@Override
|
||||
public ProcessDeliveryReportAction createFromParcel(final Parcel in) {
|
||||
return new ProcessDeliveryReportAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessDeliveryReportAction[] newArray(final int size) {
|
||||
return new ProcessDeliveryReportAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.telephony.SmsManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DataModelException;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.MmsFileProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.SqliteWrapper;
|
||||
import com.android.messaging.mmslib.pdu.PduHeaders;
|
||||
import com.android.messaging.mmslib.pdu.RetrieveConf;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.MmsSender;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Processes an MMS message after it has been downloaded.
|
||||
* NOTE: This action must queue a ProcessPendingMessagesAction when it is done (success or failure).
|
||||
*/
|
||||
public class ProcessDownloadedMmsAction extends Action {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
// Always set when message downloaded
|
||||
private static final String KEY_DOWNLOADED_BY_PLATFORM = "downloaded_by_platform";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_NOTIFICATION_URI = "notification_uri";
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_PARTICIPANT_ID = "participant_id";
|
||||
private static final String KEY_STATUS_IF_FAILED = "status_if_failed";
|
||||
|
||||
// Set when message downloaded by platform (L+)
|
||||
private static final String KEY_RESULT_CODE = "result_code";
|
||||
private static final String KEY_HTTP_STATUS_CODE = "http_status_code";
|
||||
private static final String KEY_CONTENT_URI = "content_uri";
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
private static final String KEY_TRANSACTION_ID = "transaction_id";
|
||||
private static final String KEY_CONTENT_LOCATION = "content_location";
|
||||
private static final String KEY_AUTO_DOWNLOAD = "auto_download";
|
||||
private static final String KEY_RECEIVED_TIMESTAMP = "received_timestamp";
|
||||
|
||||
// Set when message downloaded by us (legacy)
|
||||
private static final String KEY_STATUS = "status";
|
||||
private static final String KEY_RAW_STATUS = "raw_status";
|
||||
private static final String KEY_MMS_URI = "mms_uri";
|
||||
|
||||
// Used to send a deferred response in response to auto-download failure
|
||||
private static final String KEY_SEND_DEFERRED_RESP_STATUS = "send_deferred_resp_status";
|
||||
|
||||
// Results passed from background worker to processCompletion
|
||||
private static final String BUNDLE_REQUEST_STATUS = "request_status";
|
||||
private static final String BUNDLE_RAW_TELEPHONY_STATUS = "raw_status";
|
||||
private static final String BUNDLE_MMS_URI = "mms_uri";
|
||||
|
||||
// This is called when MMS lib API returns via PendingIntent
|
||||
public static void processMessageDownloaded(final int resultCode, final Bundle extras) {
|
||||
final String messageId = extras.getString(DownloadMmsAction.EXTRA_MESSAGE_ID);
|
||||
final Uri contentUri = extras.getParcelable(DownloadMmsAction.EXTRA_CONTENT_URI);
|
||||
final Uri notificationUri = extras.getParcelable(DownloadMmsAction.EXTRA_NOTIFICATION_URI);
|
||||
final String conversationId = extras.getString(DownloadMmsAction.EXTRA_CONVERSATION_ID);
|
||||
final String participantId = extras.getString(DownloadMmsAction.EXTRA_PARTICIPANT_ID);
|
||||
Assert.notNull(messageId);
|
||||
Assert.notNull(contentUri);
|
||||
Assert.notNull(notificationUri);
|
||||
Assert.notNull(conversationId);
|
||||
Assert.notNull(participantId);
|
||||
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
params.putInt(KEY_HTTP_STATUS_CODE,
|
||||
extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
|
||||
params.putParcelable(KEY_CONTENT_URI, contentUri);
|
||||
params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
|
||||
params.putInt(KEY_SUB_ID,
|
||||
extras.getInt(DownloadMmsAction.EXTRA_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
|
||||
params.putString(KEY_SUB_PHONE_NUMBER,
|
||||
extras.getString(DownloadMmsAction.EXTRA_SUB_PHONE_NUMBER));
|
||||
params.putString(KEY_TRANSACTION_ID,
|
||||
extras.getString(DownloadMmsAction.EXTRA_TRANSACTION_ID));
|
||||
params.putString(KEY_CONTENT_LOCATION,
|
||||
extras.getString(DownloadMmsAction.EXTRA_CONTENT_LOCATION));
|
||||
params.putBoolean(KEY_AUTO_DOWNLOAD,
|
||||
extras.getBoolean(DownloadMmsAction.EXTRA_AUTO_DOWNLOAD));
|
||||
params.putLong(KEY_RECEIVED_TIMESTAMP,
|
||||
extras.getLong(DownloadMmsAction.EXTRA_RECEIVED_TIMESTAMP));
|
||||
params.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
params.putString(KEY_PARTICIPANT_ID, participantId);
|
||||
params.putInt(KEY_STATUS_IF_FAILED,
|
||||
extras.getInt(DownloadMmsAction.EXTRA_STATUS_IF_FAILED));
|
||||
action.start();
|
||||
}
|
||||
|
||||
// This is called for fast failing downloading (due to airplane mode or mobile data )
|
||||
public static void processMessageDownloadFastFailed(final String messageId,
|
||||
final Uri notificationUri, final String conversationId, final String participantId,
|
||||
final String contentLocation, final int subId, final String subPhoneNumber,
|
||||
final int statusIfFailed, final boolean autoDownload, final String transactionId,
|
||||
final int resultCode) {
|
||||
Assert.notNull(messageId);
|
||||
Assert.notNull(notificationUri);
|
||||
Assert.notNull(conversationId);
|
||||
Assert.notNull(participantId);
|
||||
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
params.putString(KEY_SUB_PHONE_NUMBER, subPhoneNumber);
|
||||
params.putString(KEY_CONTENT_LOCATION, contentLocation);
|
||||
params.putBoolean(KEY_AUTO_DOWNLOAD, autoDownload);
|
||||
params.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
params.putString(KEY_PARTICIPANT_ID, participantId);
|
||||
params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
|
||||
params.putString(KEY_TRANSACTION_ID, transactionId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void processDownloadActionFailure(final String messageId, final int status,
|
||||
final int rawStatus, final String conversationId, final String participantId,
|
||||
final int statusIfFailed, final int subId, final String transactionId) {
|
||||
Assert.notNull(messageId);
|
||||
Assert.notNull(conversationId);
|
||||
Assert.notNull(participantId);
|
||||
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, false);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putInt(KEY_STATUS, status);
|
||||
params.putInt(KEY_RAW_STATUS, rawStatus);
|
||||
params.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
params.putString(KEY_PARTICIPANT_ID, participantId);
|
||||
params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
params.putString(KEY_TRANSACTION_ID, transactionId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void sendDeferredRespStatus(final String messageId, final String transactionId,
|
||||
final String contentLocation, final int subId) {
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putString(KEY_TRANSACTION_ID, transactionId);
|
||||
params.putString(KEY_CONTENT_LOCATION, contentLocation);
|
||||
params.putBoolean(KEY_SEND_DEFERRED_RESP_STATUS, true);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private ProcessDownloadedMmsAction() {
|
||||
// Callers must use one of the static methods above
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
// Fire up the background worker
|
||||
requestBackgroundWork();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() throws DataModelException {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
final boolean sendDeferredRespStatus =
|
||||
actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS, false);
|
||||
|
||||
// Send a response indicating that auto-download failed
|
||||
if (sendDeferredRespStatus) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "DownloadMmsAction: Auto-download of message " + messageId
|
||||
+ " failed; sending DEFERRED NotifyRespInd");
|
||||
}
|
||||
MmsUtils.sendNotifyResponseForMmsDownload(
|
||||
context,
|
||||
subId,
|
||||
MmsUtils.stringToBytes(transactionId, "UTF-8"),
|
||||
contentLocation,
|
||||
PduHeaders.STATUS_DEFERRED);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Processing a real MMS download
|
||||
final boolean downloadedByPlatform = actionParameters.getBoolean(
|
||||
KEY_DOWNLOADED_BY_PLATFORM);
|
||||
|
||||
final int status;
|
||||
int rawStatus = MmsUtils.PDU_HEADER_VALUE_UNDEFINED;
|
||||
Uri mmsUri = null;
|
||||
|
||||
if (downloadedByPlatform) {
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI);
|
||||
final File downloadedFile = MmsFileProvider.getFile(contentUri);
|
||||
byte[] downloadedData = null;
|
||||
try {
|
||||
downloadedData = Files.toByteArray(downloadedFile);
|
||||
} catch (final FileNotFoundException e) {
|
||||
LogUtil.e(TAG, "ProcessDownloadedMmsAction: MMS download file not found: "
|
||||
+ downloadedFile.getAbsolutePath());
|
||||
} catch (final IOException e) {
|
||||
LogUtil.e(TAG, "ProcessDownloadedMmsAction: Error reading MMS download file: "
|
||||
+ downloadedFile.getAbsolutePath(), e);
|
||||
}
|
||||
|
||||
// Can delete the temp file now
|
||||
if (downloadedFile.exists()) {
|
||||
downloadedFile.delete();
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ProcessDownloadedMmsAction: Deleted temp file with "
|
||||
+ "downloaded MMS pdu: " + downloadedFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedData != null) {
|
||||
final RetrieveConf retrieveConf =
|
||||
MmsSender.parseRetrieveConf(downloadedData, subId);
|
||||
if (MmsUtils.isDumpMmsEnabled()) {
|
||||
MmsUtils.dumpPdu(downloadedData, retrieveConf);
|
||||
}
|
||||
if (retrieveConf != null) {
|
||||
// Insert the downloaded MMS into telephony
|
||||
final Uri notificationUri = actionParameters.getParcelable(
|
||||
KEY_NOTIFICATION_URI);
|
||||
final String subPhoneNumber = actionParameters.getString(
|
||||
KEY_SUB_PHONE_NUMBER);
|
||||
final boolean autoDownload = actionParameters.getBoolean(
|
||||
KEY_AUTO_DOWNLOAD);
|
||||
final long receivedTimestampInSeconds =
|
||||
actionParameters.getLong(KEY_RECEIVED_TIMESTAMP);
|
||||
|
||||
// Inform sync we're adding a message to telephony
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(receivedTimestampInSeconds * 1000L);
|
||||
|
||||
final MmsUtils.StatusPlusUri result =
|
||||
MmsUtils.insertDownloadedMessageAndSendResponse(context,
|
||||
notificationUri, subId, subPhoneNumber, transactionId,
|
||||
contentLocation, autoDownload, receivedTimestampInSeconds,
|
||||
retrieveConf);
|
||||
status = result.status;
|
||||
rawStatus = result.rawStatus;
|
||||
mmsUri = result.uri;
|
||||
} else {
|
||||
// Invalid response PDU
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
}
|
||||
} else {
|
||||
// Failed to read download file
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "ProcessDownloadedMmsAction: Platform returned error resultCode: "
|
||||
+ resultCode);
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
|
||||
}
|
||||
} else {
|
||||
// Message was already processed by the internal API, or the download action failed.
|
||||
// In either case, we just need to copy the status to the response bundle.
|
||||
status = actionParameters.getInt(KEY_STATUS);
|
||||
rawStatus = actionParameters.getInt(KEY_RAW_STATUS);
|
||||
mmsUri = actionParameters.getParcelable(KEY_MMS_URI);
|
||||
}
|
||||
|
||||
final Bundle response = new Bundle();
|
||||
response.putInt(BUNDLE_REQUEST_STATUS, status);
|
||||
response.putInt(BUNDLE_RAW_TELEPHONY_STATUS, rawStatus);
|
||||
response.putParcelable(BUNDLE_MMS_URI, mmsUri);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
if (response == null) {
|
||||
// No message download to process; doBackgroundWork sent a notify deferred response
|
||||
Assert.isTrue(actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS));
|
||||
return null;
|
||||
}
|
||||
|
||||
final int status = response.getInt(BUNDLE_REQUEST_STATUS);
|
||||
final int rawStatus = response.getInt(BUNDLE_RAW_TELEPHONY_STATUS);
|
||||
final Uri messageUri = response.getParcelable(BUNDLE_MMS_URI);
|
||||
final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
// Do post-processing on downloaded message
|
||||
final MessageData message = processResult(status, rawStatus, messageUri);
|
||||
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
// If we were trying to auto-download but have failed need to send the deferred response
|
||||
if (autoDownload && message == null && status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
sendDeferredRespStatus(messageId, transactionId, contentLocation, subId);
|
||||
}
|
||||
|
||||
if (autoDownload) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
MessageData toastMessage = message;
|
||||
if (toastMessage == null) {
|
||||
// If the downloaded failed (message is null), then we should announce the
|
||||
// receiving of the wap push message. Load the wap push message here instead.
|
||||
toastMessage = BugleDatabaseOperations.readMessageData(db, messageId);
|
||||
}
|
||||
if (toastMessage != null) {
|
||||
final ParticipantData sender = ParticipantData.getFromId(
|
||||
db, toastMessage.getParticipantId());
|
||||
BugleActionToasts.onMessageReceived(
|
||||
toastMessage.getConversationId(), sender, toastMessage);
|
||||
}
|
||||
} else {
|
||||
final boolean success = message != null && status == MmsUtils.MMS_REQUEST_SUCCEEDED;
|
||||
BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
|
||||
// If download failed, use the wap push message's conversation instead
|
||||
success ? message.getConversationId()
|
||||
: actionParameters.getString(KEY_CONVERSATION_ID),
|
||||
success, status, false/*isSms*/, subId, false /*isSend*/);
|
||||
}
|
||||
|
||||
final boolean failed = (messageUri == null);
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(failed, this);
|
||||
if (failed) {
|
||||
BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundFailure() {
|
||||
if (actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS)) {
|
||||
// We can early-out for these failures. processResult is only designed to handle
|
||||
// post-processing of MMS downloads (whether successful or not).
|
||||
LogUtil.w(TAG,
|
||||
"ProcessDownloadedMmsAction: Exception while sending deferred NotifyRespInd");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Background worker threw an exception; require manual retry
|
||||
processResult(MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
|
||||
null /* mmsUri */);
|
||||
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(true /* failed */,
|
||||
this);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private MessageData processResult(final int status, final int rawStatus, final Uri mmsUri) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final Uri mmsNotificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI);
|
||||
final String notificationConversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final String notificationParticipantId = actionParameters.getString(KEY_PARTICIPANT_ID);
|
||||
final int statusIfFailed = actionParameters.getInt(KEY_STATUS_IF_FAILED);
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
Assert.notNull(messageId);
|
||||
|
||||
LogUtil.i(TAG, "ProcessDownloadedMmsAction: Processed MMS download of message " + messageId
|
||||
+ "; status is " + MmsUtils.getRequestStatusDescription(status));
|
||||
|
||||
DatabaseMessages.MmsMessage mms = null;
|
||||
if (status == MmsUtils.MMS_REQUEST_SUCCEEDED && mmsUri != null) {
|
||||
// Delete the initial M-Notification.ind from telephony
|
||||
SqliteWrapper.delete(context, context.getContentResolver(),
|
||||
mmsNotificationUri, null, null);
|
||||
|
||||
// Read the sent MMS from the telephony provider
|
||||
mms = MmsUtils.loadMms(mmsUri);
|
||||
}
|
||||
|
||||
boolean messageInFocusedConversation = false;
|
||||
boolean messageInObservableConversation = false;
|
||||
String conversationId = null;
|
||||
MessageData message = null;
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (mms != null) {
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(mms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
|
||||
final List<String> recipients = MmsUtils.getRecipientsByThread(mms.mThreadId);
|
||||
String from = MmsUtils.getMmsSender(recipients, mms.getUri());
|
||||
if (from == null) {
|
||||
LogUtil.w(TAG,
|
||||
"Downloaded an MMS without sender address; using unknown sender.");
|
||||
from = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
final ParticipantData sender = ParticipantData.getFromRawPhoneBySimLocale(from,
|
||||
subId);
|
||||
final String senderParticipantId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender);
|
||||
if (!senderParticipantId.equals(notificationParticipantId)) {
|
||||
LogUtil.e(TAG, "ProcessDownloadedMmsAction: Downloaded MMS message "
|
||||
+ messageId + " has different sender (participantId = "
|
||||
+ senderParticipantId + ") than notification ("
|
||||
+ notificationParticipantId + ")");
|
||||
}
|
||||
final boolean blockedSender = BugleDatabaseOperations.isBlockedDestination(
|
||||
db, sender.getNormalizedDestination());
|
||||
conversationId = BugleDatabaseOperations.getOrCreateConversationFromThreadId(db,
|
||||
mms.mThreadId, blockedSender, subId);
|
||||
|
||||
messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(conversationId);
|
||||
messageInObservableConversation =
|
||||
DataModel.get().isNewMessageObservable(conversationId);
|
||||
|
||||
// TODO: Also write these values to the telephony provider
|
||||
mms.mRead = messageInFocusedConversation;
|
||||
mms.mSeen = messageInObservableConversation;
|
||||
|
||||
// Translate to our format
|
||||
message = MmsUtils.createMmsMessage(mms, conversationId, senderParticipantId,
|
||||
selfId, MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
|
||||
// Update image sizes.
|
||||
message.updateSizesForImageParts();
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(message.getReceivedTimeStamp());
|
||||
final MessageData current = BugleDatabaseOperations.readMessageData(db, messageId);
|
||||
if (current == null) {
|
||||
LogUtil.w(TAG, "Message deleted prior to update");
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} else {
|
||||
// Overwrite existing notification message
|
||||
message.updateMessageId(messageId);
|
||||
// Write message
|
||||
BugleDatabaseOperations.updateMessageInTransaction(db, message);
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(notificationConversationId, conversationId)) {
|
||||
// If this is a group conversation, the message is moved. So the original
|
||||
// 1v1 conversation (as referenced by notificationConversationId) could
|
||||
// be left with no non-draft message. Delete the conversation if that
|
||||
// happens. See the comment for the method below for why we need to do this.
|
||||
if (!BugleDatabaseOperations.deleteConversationIfEmptyInTransaction(
|
||||
db, notificationConversationId)) {
|
||||
BugleDatabaseOperations.maybeRefreshConversationMetadataInTransaction(
|
||||
db, notificationConversationId, messageId,
|
||||
true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
|
||||
}
|
||||
}
|
||||
|
||||
BugleDatabaseOperations.refreshConversationMetadataInTransaction(db, conversationId,
|
||||
true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
|
||||
} else {
|
||||
messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(notificationConversationId);
|
||||
|
||||
// Default to retry status unless status indicates otherwise
|
||||
int bugleStatus = statusIfFailed;
|
||||
if (status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
|
||||
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED;
|
||||
} else if (status == MmsUtils.MMS_REQUEST_NO_RETRY) {
|
||||
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE;
|
||||
}
|
||||
DownloadMmsAction.updateMessageStatus(mmsNotificationUri, messageId,
|
||||
notificationConversationId, bugleStatus, rawStatus);
|
||||
|
||||
// Log MMS download failed
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
|
||||
// Just in case this was the latest message update the summary data
|
||||
BugleDatabaseOperations.refreshConversationMetadataInTransaction(db,
|
||||
notificationConversationId, true /*shouldAutoSwitchSelfId*/,
|
||||
false /*keepArchived*/);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (mmsUri != null) {
|
||||
// Update mms table with read status now we know the conversation id
|
||||
final ContentValues values = new ContentValues(1);
|
||||
values.put(Mms.READ, messageInFocusedConversation);
|
||||
SqliteWrapper.update(context, context.getContentResolver(), mmsUri, values,
|
||||
null, null);
|
||||
}
|
||||
|
||||
// Show a notification to let the user know a new message has arrived
|
||||
BugleNotifications.update(false /*silent*/, conversationId, BugleNotifications.UPDATE_ALL);
|
||||
|
||||
// Messages may have changed in two conversations
|
||||
if (conversationId != null) {
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(notificationConversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private ProcessDownloadedMmsAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessDownloadedMmsAction> CREATOR
|
||||
= new Parcelable.Creator<ProcessDownloadedMmsAction>() {
|
||||
@Override
|
||||
public ProcessDownloadedMmsAction createFromParcel(final Parcel in) {
|
||||
return new ProcessDownloadedMmsAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessDownloadedMmsAction[] newArray(final int size) {
|
||||
return new ProcessDownloadedMmsAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.telephony.ServiceState;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.BugleGservices;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.ConnectivityUtil.ConnectivityListener;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Action used to lookup any messages in the pending send/download state and either fail them or
|
||||
* retry their action. This action only initiates one retry at a time - further retries should be
|
||||
* triggered by successful sending of a message, network status change or exponential backoff timer.
|
||||
*/
|
||||
public class ProcessPendingMessagesAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final int PENDING_INTENT_REQUEST_CODE = 101;
|
||||
|
||||
public static void processFirstPendingMessage() {
|
||||
// Clear any pending alarms or connectivity events
|
||||
unregister();
|
||||
// Clear retry count
|
||||
setRetry(0);
|
||||
|
||||
// Start action
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void scheduleProcessPendingMessagesAction(final boolean failed,
|
||||
final Action processingAction) {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Scheduling pending messages"
|
||||
+ (failed ? "(message failed)" : ""));
|
||||
// Can safely clear any pending alarms or connectivity events as either an action
|
||||
// is currently running or we will run now or register if pending actions possible.
|
||||
unregister();
|
||||
|
||||
final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp();
|
||||
boolean scheduleAlarm = false;
|
||||
// If message succeeded and if Bugle is default SMS app just carry on with next message
|
||||
if (!failed && isDefaultSmsApp) {
|
||||
// Clear retry attempt count as something just succeeded
|
||||
setRetry(0);
|
||||
|
||||
// Lookup and queue next message for immediate processing by background worker
|
||||
// iff there are no pending messages this will do nothing and return true.
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
if (action.queueActions(processingAction)) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
if (processingAction.hasBackgroundActions()) {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: Action queued");
|
||||
} else {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: No actions to queue");
|
||||
}
|
||||
}
|
||||
// Have queued next action if needed, nothing more to do
|
||||
return;
|
||||
}
|
||||
// In case of error queuing schedule a retry
|
||||
scheduleAlarm = true;
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: Action failed to queue; retrying");
|
||||
}
|
||||
if (getHavePendingMessages() || scheduleAlarm) {
|
||||
// Still have a pending message that needs to be queued for processing
|
||||
final ConnectivityListener listener = new ConnectivityListener() {
|
||||
@Override
|
||||
public void onConnectivityStateChanged(final Context context, final Intent intent) {
|
||||
final int networkType =
|
||||
MmsUtils.getConnectivityEventNetworkType(context, intent);
|
||||
if (networkType != ConnectivityManager.TYPE_MOBILE) {
|
||||
return;
|
||||
}
|
||||
final boolean isConnected = !intent.getBooleanExtra(
|
||||
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
|
||||
// TODO: Should we check in more detail?
|
||||
if (isConnected) {
|
||||
onConnected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneStateChanged(final Context context, final int serviceState) {
|
||||
if (serviceState == ServiceState.STATE_IN_SERVICE) {
|
||||
onConnected();
|
||||
}
|
||||
}
|
||||
|
||||
private void onConnected() {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Now connected; starting action");
|
||||
|
||||
// Clear any pending alarms or connectivity events but leave attempt count alone
|
||||
unregister();
|
||||
|
||||
// Start action
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
action.start();
|
||||
}
|
||||
};
|
||||
// Read and increment attempt number from shared prefs
|
||||
final int retryAttempt = getNextRetry();
|
||||
register(listener, retryAttempt);
|
||||
} else {
|
||||
// No more pending messages (presumably the message that failed has expired) or it
|
||||
// may be possible that a send and a download are already in process.
|
||||
// Clear retry attempt count.
|
||||
// TODO Might be premature if send and download in process...
|
||||
// but worst case means we try to send a bit more often.
|
||||
setRetry(0);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: No more pending messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void register(final ConnectivityListener listener, final int retryAttempt) {
|
||||
int retryNumber = retryAttempt;
|
||||
|
||||
// Register to be notified about connectivity changes
|
||||
DataModel.get().getConnectivityUtil().register(listener);
|
||||
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
final long initialBackoffMs = BugleGservices.get().getLong(
|
||||
BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS,
|
||||
BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS_DEFAULT);
|
||||
final long maxDelayMs = BugleGservices.get().getLong(
|
||||
BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS,
|
||||
BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS_DEFAULT);
|
||||
long delayMs;
|
||||
long nextDelayMs = initialBackoffMs;
|
||||
do {
|
||||
delayMs = nextDelayMs;
|
||||
retryNumber--;
|
||||
nextDelayMs = delayMs * 2;
|
||||
}
|
||||
while (retryNumber > 0 && nextDelayMs < maxDelayMs);
|
||||
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Registering for retry #" + retryAttempt
|
||||
+ " in " + delayMs + " ms");
|
||||
|
||||
action.schedule(PENDING_INTENT_REQUEST_CODE, delayMs);
|
||||
}
|
||||
|
||||
private static void unregister() {
|
||||
// Clear any pending alarms or connectivity events
|
||||
DataModel.get().getConnectivityUtil().unregister();
|
||||
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
action.schedule(PENDING_INTENT_REQUEST_CODE, Long.MAX_VALUE);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: Unregistering for connectivity changed "
|
||||
+ "events and clearing scheduled alarm");
|
||||
}
|
||||
}
|
||||
|
||||
private static void setRetry(final int retryAttempt) {
|
||||
final BuglePrefs prefs = Factory.get().getApplicationPrefs();
|
||||
prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
|
||||
}
|
||||
|
||||
private static int getNextRetry() {
|
||||
final BuglePrefs prefs = Factory.get().getApplicationPrefs();
|
||||
final int retryAttempt =
|
||||
prefs.getInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, 0) + 1;
|
||||
prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
|
||||
return retryAttempt;
|
||||
}
|
||||
|
||||
private ProcessPendingMessagesAction() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the DB and determine if there are any messages we should process
|
||||
* @return true if we have pending messages
|
||||
*/
|
||||
private static boolean getHavePendingMessages() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
final String toSendMessageId = findNextMessageToSend(db, now);
|
||||
if (toSendMessageId != null) {
|
||||
return true;
|
||||
} else {
|
||||
final String toDownloadMessageId = findNextMessageToDownload(db, now);
|
||||
if (toDownloadMessageId != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Messages may be in the process of sending/downloading even when there are no pending
|
||||
// messages...
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue any pending actions
|
||||
* @param actionState
|
||||
* @return true if action queued (or no actions to queue) else false
|
||||
*/
|
||||
private boolean queueActions(final Action processingAction) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final long now = System.currentTimeMillis();
|
||||
boolean succeeded = true;
|
||||
|
||||
// Will queue no more than one message to send plus one message to download
|
||||
// This keeps outgoing messages "in order" but allow downloads to happen even if sending
|
||||
// gets blocked until messages time out. Manual resend bumps messages to head of queue.
|
||||
final String toSendMessageId = findNextMessageToSend(db, now);
|
||||
final String toDownloadMessageId = findNextMessageToDownload(db, now);
|
||||
if (toSendMessageId != null) {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId
|
||||
+ " for sending");
|
||||
// This could queue nothing
|
||||
if (!SendMessageAction.queueForSendInBackground(toSendMessageId, processingAction)) {
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
|
||||
+ toSendMessageId + " for sending");
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
if (toDownloadMessageId != null) {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId
|
||||
+ " for download");
|
||||
// This could queue nothing
|
||||
if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId,
|
||||
processingAction)) {
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
|
||||
+ toDownloadMessageId + " for download");
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
if (toSendMessageId == null && toDownloadMessageId == null) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ProcessPendingMessagesAction: No messages to send or download");
|
||||
}
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
// If triggered by alarm will not have unregistered yet
|
||||
unregister();
|
||||
|
||||
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
queueActions(this);
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: Not default SMS app; rescheduling");
|
||||
}
|
||||
scheduleProcessPendingMessagesAction(true, this);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String findNextMessageToSend(final DatabaseWrapper db, final long now) {
|
||||
String toSendMessageId = null;
|
||||
db.beginTransaction();
|
||||
Cursor sending = null;
|
||||
Cursor cursor = null;
|
||||
int sendingCnt = 0;
|
||||
int pendingCnt = 0;
|
||||
int failedCnt = 0;
|
||||
try {
|
||||
// First check to see if we have any messages already sending
|
||||
sending = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
MessageData.getProjection(),
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
|
||||
new String[]{Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)},
|
||||
null,
|
||||
null,
|
||||
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
|
||||
final boolean messageCurrentlySending = sending.moveToNext();
|
||||
sendingCnt = sending.getCount();
|
||||
// Look for messages we could send
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_OUTGOING_FAILED);
|
||||
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
MessageData.getProjection(),
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN ("
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ","
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ")",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
|
||||
pendingCnt = cursor.getCount();
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
final MessageData message = new MessageData();
|
||||
message.bind(cursor);
|
||||
if (message.getInResendWindow(now)) {
|
||||
// If no messages currently sending
|
||||
if (!messageCurrentlySending) {
|
||||
// Resend this message
|
||||
toSendMessageId = message.getMessageId();
|
||||
// Before queuing the message for resending, check if the message's self is
|
||||
// active. If not, switch back to the system's default subscription.
|
||||
if (OsUtil.isAtLeastL_MR1()) {
|
||||
final ParticipantData messageSelf = BugleDatabaseOperations
|
||||
.getExistingParticipant(db, message.getSelfId());
|
||||
if (messageSelf == null || !messageSelf.isActiveSubscription()) {
|
||||
final ParticipantData defaultSelf = BugleDatabaseOperations
|
||||
.getOrCreateSelf(db, PhoneUtils.getDefault()
|
||||
.getDefaultSmsSubscriptionId());
|
||||
if (defaultSelf != null) {
|
||||
message.bindSelfId(defaultSelf.getId());
|
||||
final ContentValues selfValues = new ContentValues();
|
||||
selfValues.put(MessageColumns.SELF_PARTICIPANT_ID,
|
||||
defaultSelf.getId());
|
||||
BugleDatabaseOperations.updateMessageRow(db,
|
||||
message.getMessageId(), selfValues);
|
||||
MessagingContentProvider.notifyMessagesChanged(
|
||||
message.getConversationId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
failedCnt++;
|
||||
|
||||
// Mark message as failed
|
||||
BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
if (sending != null) {
|
||||
sending.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ProcessPendingMessagesAction: "
|
||||
+ sendingCnt + " messages already sending, "
|
||||
+ pendingCnt + " messages to send, "
|
||||
+ failedCnt + " failed messages");
|
||||
}
|
||||
|
||||
return toSendMessageId;
|
||||
}
|
||||
|
||||
private static String findNextMessageToDownload(final DatabaseWrapper db, final long now) {
|
||||
String toDownloadMessageId = null;
|
||||
db.beginTransaction();
|
||||
Cursor cursor = null;
|
||||
int downloadingCnt = 0;
|
||||
int pendingCnt = 0;
|
||||
try {
|
||||
// First check if we have any messages already downloading
|
||||
downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
|
||||
new String[] {
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING)
|
||||
});
|
||||
|
||||
// TODO: This query is not actually needed if downloadingCnt == 0.
|
||||
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
MessageData.getProjection(),
|
||||
DatabaseHelper.MessageColumns.STATUS + " =? OR "
|
||||
+ DatabaseHelper.MessageColumns.STATUS + " =?",
|
||||
new String[]{
|
||||
Integer.toString(
|
||||
MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD),
|
||||
Integer.toString(
|
||||
MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD)
|
||||
},
|
||||
null,
|
||||
null,
|
||||
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
|
||||
|
||||
pendingCnt = cursor.getCount();
|
||||
|
||||
// If no messages are currently downloading and there is a download pending,
|
||||
// queue the download of the oldest pending message.
|
||||
if (downloadingCnt == 0 && cursor.moveToNext()) {
|
||||
// Always start the next pending message. We will check if a download has
|
||||
// expired in DownloadMmsAction and mark message failed there.
|
||||
final MessageData message = new MessageData();
|
||||
message.bind(cursor);
|
||||
toDownloadMessageId = message.getMessageId();
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ProcessPendingMessagesAction: "
|
||||
+ downloadingCnt + " messages already downloading, "
|
||||
+ pendingCnt + " messages to download");
|
||||
}
|
||||
|
||||
return toDownloadMessageId;
|
||||
}
|
||||
|
||||
private ProcessPendingMessagesAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessPendingMessagesAction> CREATOR
|
||||
= new Parcelable.Creator<ProcessPendingMessagesAction>() {
|
||||
@Override
|
||||
public ProcessPendingMessagesAction createFromParcel(final Parcel in) {
|
||||
return new ProcessPendingMessagesAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessPendingMessagesAction[] newArray(final int size) {
|
||||
return new ProcessPendingMessagesAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.telephony.SmsManager;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MmsFileProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.pdu.SendConf;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.sms.MmsSender;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Update message status to reflect success or failure
|
||||
* Can also update the message itself if a "final" message is now available from telephony db
|
||||
*/
|
||||
public class ProcessSentMessageAction extends Action {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
// These are always set
|
||||
private static final String KEY_SMS = "is_sms";
|
||||
private static final String KEY_SENT_BY_PLATFORM = "sent_by_platform";
|
||||
|
||||
// These are set when we're processing a message sent by the user. They are null for messages
|
||||
// sent automatically (e.g. a NotifyRespInd/AcknowledgeInd sent in response to a download).
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_MESSAGE_URI = "message_uri";
|
||||
private static final String KEY_UPDATED_MESSAGE_URI = "updated_message_uri";
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
|
||||
// These are set for messages sent by the platform (L+)
|
||||
public static final String KEY_RESULT_CODE = "result_code";
|
||||
public static final String KEY_HTTP_STATUS_CODE = "http_status_code";
|
||||
private static final String KEY_CONTENT_URI = "content_uri";
|
||||
private static final String KEY_RESPONSE = "response";
|
||||
private static final String KEY_RESPONSE_IMPORTANT = "response_important";
|
||||
|
||||
// These are set for messages we sent ourself (legacy), or which we fast-failed before sending.
|
||||
private static final String KEY_STATUS = "status";
|
||||
private static final String KEY_RAW_STATUS = "raw_status";
|
||||
|
||||
// This is called when MMS lib API returns via PendingIntent
|
||||
public static void processMmsSent(final int resultCode, final Uri messageUri,
|
||||
final Bundle extras) {
|
||||
final ProcessSentMessageAction action = new ProcessSentMessageAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_SMS, false);
|
||||
params.putBoolean(KEY_SENT_BY_PLATFORM, true);
|
||||
params.putString(KEY_MESSAGE_ID, extras.getString(SendMessageAction.EXTRA_MESSAGE_ID));
|
||||
params.putParcelable(KEY_MESSAGE_URI, messageUri);
|
||||
params.putParcelable(KEY_UPDATED_MESSAGE_URI,
|
||||
extras.getParcelable(SendMessageAction.EXTRA_UPDATED_MESSAGE_URI));
|
||||
params.putInt(KEY_SUB_ID,
|
||||
extras.getInt(SendMessageAction.KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
params.putInt(KEY_HTTP_STATUS_CODE, extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
|
||||
params.putParcelable(KEY_CONTENT_URI,
|
||||
extras.getParcelable(SendMessageAction.EXTRA_CONTENT_URI));
|
||||
params.putByteArray(KEY_RESPONSE, extras.getByteArray(SmsManager.EXTRA_MMS_DATA));
|
||||
params.putBoolean(KEY_RESPONSE_IMPORTANT,
|
||||
extras.getBoolean(SendMessageAction.EXTRA_RESPONSE_IMPORTANT));
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void processMessageSentFastFailed(final String messageId,
|
||||
final Uri messageUri, final Uri updatedMessageUri, final int subId, final boolean isSms,
|
||||
final int status, final int rawStatus, final int resultCode) {
|
||||
final ProcessSentMessageAction action = new ProcessSentMessageAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_SMS, isSms);
|
||||
params.putBoolean(KEY_SENT_BY_PLATFORM, false);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putParcelable(KEY_MESSAGE_URI, messageUri);
|
||||
params.putParcelable(KEY_UPDATED_MESSAGE_URI, updatedMessageUri);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
params.putInt(KEY_STATUS, status);
|
||||
params.putInt(KEY_RAW_STATUS, rawStatus);
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private ProcessSentMessageAction() {
|
||||
// Callers must use one of the static methods above
|
||||
}
|
||||
|
||||
/**
|
||||
* Update message status to reflect success or failure
|
||||
* Can also update the message itself if a "final" message is now available from telephony db
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
|
||||
final Uri updatedMessageUri = actionParameters.getParcelable(KEY_UPDATED_MESSAGE_URI);
|
||||
final boolean isSms = actionParameters.getBoolean(KEY_SMS);
|
||||
final boolean sentByPlatform = actionParameters.getBoolean(KEY_SENT_BY_PLATFORM);
|
||||
|
||||
int status = actionParameters.getInt(KEY_STATUS, MmsUtils.MMS_REQUEST_MANUAL_RETRY);
|
||||
int rawStatus = actionParameters.getInt(KEY_RAW_STATUS,
|
||||
MmsUtils.PDU_HEADER_VALUE_UNDEFINED);
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
if (sentByPlatform) {
|
||||
// Delete temporary file backing the contentUri passed to MMS service
|
||||
final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI);
|
||||
Assert.isTrue(contentUri != null);
|
||||
final File tempFile = MmsFileProvider.getFile(contentUri);
|
||||
long messageSize = 0;
|
||||
if (tempFile.exists()) {
|
||||
messageSize = tempFile.length();
|
||||
tempFile.delete();
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "ProcessSentMessageAction: Deleted temp file with outgoing "
|
||||
+ "MMS pdu: " + contentUri);
|
||||
}
|
||||
}
|
||||
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
final boolean responseImportant = actionParameters.getBoolean(KEY_RESPONSE_IMPORTANT);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (responseImportant) {
|
||||
// Get the status from the response PDU and update telephony
|
||||
final byte[] response = actionParameters.getByteArray(KEY_RESPONSE);
|
||||
final SendConf sendConf = MmsSender.parseSendConf(response, subId);
|
||||
if (sendConf != null) {
|
||||
final MmsUtils.StatusPlusUri result =
|
||||
MmsUtils.updateSentMmsMessageStatus(context, messageUri, sendConf);
|
||||
status = result.status;
|
||||
rawStatus = result.rawStatus;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String errorMsg = "ProcessSentMessageAction: Platform returned error resultCode: "
|
||||
+ resultCode;
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
if (httpStatusCode != 0) {
|
||||
errorMsg += (", HTTP status code: " + httpStatusCode);
|
||||
}
|
||||
LogUtil.w(TAG, errorMsg);
|
||||
status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
|
||||
|
||||
// Check for MMS messages that failed because they exceeded the maximum size,
|
||||
// indicated by an I/O error from the platform.
|
||||
if (resultCode == SmsManager.MMS_ERROR_IO_ERROR) {
|
||||
if (messageSize > MmsConfig.get(subId).getMaxMessageSize()) {
|
||||
rawStatus = MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (messageId != null) {
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
processResult(
|
||||
messageId, updatedMessageUri, status, rawStatus, isSms, this, subId,
|
||||
resultCode, httpStatusCode);
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "ProcessSentMessageAction: No sent message to process (it was "
|
||||
+ "probably a notify response for an MMS download)");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void processResult(final String messageId, Uri updatedMessageUri, int status,
|
||||
final int rawStatus, final boolean isSms, final Action processingAction,
|
||||
final int subId, final int resultCode, final int httpStatusCode) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
final MessageData originalMessage = message;
|
||||
if (message == null) {
|
||||
LogUtil.w(TAG, "ProcessSentMessageAction: Sent message " + messageId
|
||||
+ " missing from local database");
|
||||
return;
|
||||
}
|
||||
final String conversationId = message.getConversationId();
|
||||
if (updatedMessageUri != null) {
|
||||
// Update message if we have newly written final message in the telephony db
|
||||
final MessageData update = MmsUtils.readSendingMmsMessage(updatedMessageUri,
|
||||
conversationId, message.getParticipantId(), message.getSelfId());
|
||||
if (update != null) {
|
||||
// Set message Id of final message to that of the existing place holder.
|
||||
update.updateMessageId(message.getMessageId());
|
||||
// Update image sizes.
|
||||
update.updateSizesForImageParts();
|
||||
// Temp attachments are no longer needed
|
||||
for (final MessagePartData part : message.getParts()) {
|
||||
part.destroySync();
|
||||
}
|
||||
message = update;
|
||||
// processResult will rewrite the complete message as part of update
|
||||
} else {
|
||||
updatedMessageUri = null;
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
LogUtil.e(TAG, "ProcessSentMessageAction: Unable to read sending message");
|
||||
}
|
||||
}
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
boolean failed;
|
||||
if (status == MmsUtils.MMS_REQUEST_SUCCEEDED) {
|
||||
message.markMessageSent(timestamp);
|
||||
failed = false;
|
||||
} else if (status == MmsUtils.MMS_REQUEST_AUTO_RETRY
|
||||
&& message.getInResendWindow(timestamp)) {
|
||||
message.markMessageNotSent(timestamp);
|
||||
message.setRawTelephonyStatus(rawStatus);
|
||||
failed = false;
|
||||
} else {
|
||||
message.markMessageFailed(timestamp);
|
||||
message.setRawTelephonyStatus(rawStatus);
|
||||
message.setMessageSeen(false);
|
||||
failed = true;
|
||||
}
|
||||
|
||||
// We have special handling for when a message to an emergency number fails. In this case,
|
||||
// we notify immediately of any failure (even if we auto-retry), and instruct the user to
|
||||
// try calling the emergency number instead.
|
||||
if (status != MmsUtils.MMS_REQUEST_SUCCEEDED) {
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
|
||||
for (final String recipient : recipients) {
|
||||
if (PhoneNumberUtils.isEmergencyNumber(recipient)) {
|
||||
BugleNotifications.notifyEmergencySmsFailed(recipient, conversationId);
|
||||
message.markMessageFailedEmergencyNumber(timestamp);
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the message status and optionally refresh the message with final parts/values.
|
||||
if (SendMessageAction.updateMessageAndStatus(isSms, message, updatedMessageUri, failed)) {
|
||||
// We shouldn't show any notifications if we're not allowed to modify Telephony for
|
||||
// this message.
|
||||
if (failed) {
|
||||
BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
|
||||
}
|
||||
BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
|
||||
conversationId, !failed, status, isSms, subId, true/*isSend*/);
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "ProcessSentMessageAction: Done sending " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId()
|
||||
+ " in conversation " + conversationId
|
||||
+ "; status is " + MmsUtils.getRequestStatusDescription(status));
|
||||
|
||||
// Whether we succeeded or failed we will check and maybe schedule some more work
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(
|
||||
status != MmsUtils.MMS_REQUEST_SUCCEEDED, processingAction);
|
||||
}
|
||||
|
||||
private ProcessSentMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessSentMessageAction> CREATOR
|
||||
= new Parcelable.Creator<ProcessSentMessageAction>() {
|
||||
@Override
|
||||
public ProcessSentMessageAction createFromParcel(final Parcel in) {
|
||||
return new ProcessSentMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessSentMessageAction[] newArray(final int size) {
|
||||
return new ProcessSentMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
public class ReadDraftDataAction extends Action implements Parcelable {
|
||||
|
||||
/**
|
||||
* Interface for ReadDraftDataAction listeners
|
||||
*/
|
||||
public interface ReadDraftDataActionListener {
|
||||
@RunsOnMainThread
|
||||
abstract void onReadDraftDataSucceeded(final ReadDraftDataAction action,
|
||||
final Object data, final MessageData message,
|
||||
final ConversationListItemData conversation);
|
||||
@RunsOnMainThread
|
||||
abstract void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read draft message and associated data (with listener)
|
||||
*/
|
||||
public static ReadDraftDataActionMonitor readDraftData(final String conversationId,
|
||||
final MessageData incomingDraft, final Object data,
|
||||
final ReadDraftDataActionListener listener) {
|
||||
final ReadDraftDataActionMonitor monitor = new ReadDraftDataActionMonitor(data,
|
||||
listener);
|
||||
final ReadDraftDataAction action = new ReadDraftDataAction(conversationId,
|
||||
incomingDraft, monitor.getActionKey());
|
||||
action.start(monitor);
|
||||
return monitor;
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversationId";
|
||||
private static final String KEY_INCOMING_DRAFT = "draftMessage";
|
||||
|
||||
private ReadDraftDataAction(final String conversationId, final MessageData incomingDraft,
|
||||
final String actionKey) {
|
||||
super(actionKey);
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putParcelable(KEY_INCOMING_DRAFT, incomingDraft);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
class DraftData {
|
||||
public final MessageData message;
|
||||
public final ConversationListItemData conversation;
|
||||
|
||||
DraftData(final MessageData message, final ConversationListItemData conversation) {
|
||||
this.message = message;
|
||||
this.conversation = conversation;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final MessageData incomingDraft = actionParameters.getParcelable(KEY_INCOMING_DRAFT);
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, conversationId);
|
||||
MessageData message = null;
|
||||
if (conversation != null) {
|
||||
if (incomingDraft == null) {
|
||||
message = BugleDatabaseOperations.readDraftMessageData(db, conversationId,
|
||||
conversation.getSelfId());
|
||||
}
|
||||
if (message == null) {
|
||||
message = MessageData.createDraftMessage(conversationId, conversation.getSelfId(),
|
||||
incomingDraft);
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "ReadDraftMessage: created draft. "
|
||||
+ "conversationId=" + conversationId
|
||||
+ " selfId=" + conversation.getSelfId());
|
||||
} else {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "ReadDraftMessage: read draft. "
|
||||
+ "conversationId=" + conversationId
|
||||
+ " selfId=" + conversation.getSelfId());
|
||||
}
|
||||
return new DraftData(message, conversation);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An operation that notifies a listener upon completion
|
||||
*/
|
||||
public static class ReadDraftDataActionMonitor extends ActionMonitor
|
||||
implements ActionCompletedListener {
|
||||
|
||||
private final ReadDraftDataActionListener mListener;
|
||||
|
||||
ReadDraftDataActionMonitor(final Object data,
|
||||
final ReadDraftDataActionListener completed) {
|
||||
super(STATE_CREATED, generateUniqueActionKey("ReadDraftDataAction"), data);
|
||||
setCompletedListener(this);
|
||||
mListener = completed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionSucceeded(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
final DraftData draft = (DraftData) result;
|
||||
if (draft == null) {
|
||||
mListener.onReadDraftDataFailed((ReadDraftDataAction) action, data);
|
||||
} else {
|
||||
mListener.onReadDraftDataSucceeded((ReadDraftDataAction) action, data,
|
||||
draft.message, draft.conversation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionFailed(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
Assert.fail("Reading draft should not fail");
|
||||
}
|
||||
}
|
||||
|
||||
private ReadDraftDataAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ReadDraftDataAction> CREATOR
|
||||
= new Parcelable.Creator<ReadDraftDataAction>() {
|
||||
@Override
|
||||
public ReadDraftDataAction createFromParcel(final Parcel in) {
|
||||
return new ReadDraftDataAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadDraftDataAction[] newArray(final int size) {
|
||||
return new ReadDraftDataAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DataModelException;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.pdu.PduHeaders;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Action used to "receive" an incoming message
|
||||
*/
|
||||
public class ReceiveMmsMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_PUSH_DATA = "push_data";
|
||||
private static final String KEY_TRANSACTION_ID = "transaction_id";
|
||||
private static final String KEY_CONTENT_LOCATION = "content_location";
|
||||
|
||||
/**
|
||||
* Create a message received from a particular number in a particular conversation
|
||||
*/
|
||||
public ReceiveMmsMessageAction(final int subId, final byte[] pushData) {
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
actionParameters.putByteArray(KEY_PUSH_DATA, pushData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final byte[] pushData = actionParameters.getByteArray(KEY_PUSH_DATA);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Write received message to telephony DB
|
||||
MessageData message = null;
|
||||
final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
|
||||
|
||||
final long received = System.currentTimeMillis();
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(received);
|
||||
|
||||
// TODO: Should use local time to set received time in MMS message
|
||||
final DatabaseMessages.MmsMessage mms = MmsUtils.processReceivedPdu(
|
||||
context, pushData, self.getSubId(), self.getNormalizedDestination());
|
||||
|
||||
if (mms != null) {
|
||||
final List<String> recipients = MmsUtils.getRecipientsByThread(mms.mThreadId);
|
||||
String from = MmsUtils.getMmsSender(recipients, mms.getUri());
|
||||
if (from == null) {
|
||||
LogUtil.w(TAG, "Received an MMS without sender address; using unknown sender.");
|
||||
from = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
final ParticipantData rawSender = ParticipantData.getFromRawPhoneBySimLocale(
|
||||
from, subId);
|
||||
final boolean blocked = BugleDatabaseOperations.isBlockedDestination(
|
||||
db, rawSender.getNormalizedDestination());
|
||||
final boolean autoDownload = (!blocked && MmsUtils.allowMmsAutoRetrieve(subId));
|
||||
final String conversationId =
|
||||
BugleDatabaseOperations.getOrCreateConversationFromThreadId(db, mms.mThreadId,
|
||||
blocked, subId);
|
||||
|
||||
final boolean messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(conversationId);
|
||||
final boolean messageInObservableConversation =
|
||||
DataModel.get().isNewMessageObservable(conversationId);
|
||||
|
||||
// TODO: Also write these values to the telephony provider
|
||||
mms.mRead = messageInFocusedConversation;
|
||||
mms.mSeen = messageInObservableConversation || blocked;
|
||||
|
||||
// Write received placeholder message to our DB
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final String participantId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, rawSender);
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
|
||||
message = MmsUtils.createMmsMessage(mms, conversationId, participantId, selfId,
|
||||
(autoDownload ? MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD :
|
||||
MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD));
|
||||
// Write the message
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
if (!autoDownload) {
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), message.getReceivedTimeStamp(),
|
||||
blocked, true /* shouldAutoSwitchSelfId */);
|
||||
final ParticipantData sender = ParticipantData .getFromId(
|
||||
db, participantId);
|
||||
BugleActionToasts.onMessageReceived(conversationId, sender, message);
|
||||
}
|
||||
// else update the conversation once we have downloaded final message (or failed)
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
// Update conversation if not immediately initiating a download
|
||||
if (!autoDownload) {
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
// Show a notification to let the user know a new message has arrived
|
||||
BugleNotifications.update(false/*silent*/, conversationId,
|
||||
BugleNotifications.UPDATE_ALL);
|
||||
|
||||
// Send the NotifyRespInd with DEFERRED status since no auto download
|
||||
actionParameters.putString(KEY_TRANSACTION_ID, mms.mTransactionId);
|
||||
actionParameters.putString(KEY_CONTENT_LOCATION, mms.mContentLocation);
|
||||
requestBackgroundWork();
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "ReceiveMmsMessageAction: Received MMS message " + message.getMessageId()
|
||||
+ " in conversation " + message.getConversationId()
|
||||
+ ", uri = " + message.getSmsMessageUri());
|
||||
} else {
|
||||
LogUtil.e(TAG, "ReceiveMmsMessageAction: Skipping processing of incoming PDU");
|
||||
}
|
||||
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() throws DataModelException {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
MmsUtils.sendNotifyResponseForMmsDownload(
|
||||
context,
|
||||
subId,
|
||||
MmsUtils.stringToBytes(transactionId, "UTF-8"),
|
||||
contentLocation,
|
||||
PduHeaders.STATUS_DEFERRED);
|
||||
// We don't need to return anything.
|
||||
return null;
|
||||
}
|
||||
|
||||
private ReceiveMmsMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ReceiveMmsMessageAction> CREATOR
|
||||
= new Parcelable.Creator<ReceiveMmsMessageAction>() {
|
||||
@Override
|
||||
public ReceiveMmsMessageAction createFromParcel(final Parcel in) {
|
||||
return new ReceiveMmsMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReceiveMmsMessageAction[] newArray(final int size) {
|
||||
return new ReceiveMmsMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony.Sms;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsSmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
/**
|
||||
* Action used to "receive" an incoming message
|
||||
*/
|
||||
public class ReceiveSmsMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_MESSAGE_VALUES = "message_values";
|
||||
|
||||
/**
|
||||
* Create a message received from a particular number in a particular conversation
|
||||
*/
|
||||
public ReceiveSmsMessageAction(final ContentValues messageValues) {
|
||||
actionParameters.putParcelable(KEY_MESSAGE_VALUES, messageValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ContentValues messageValues = actionParameters.getParcelable(KEY_MESSAGE_VALUES);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Get the SIM subscription ID
|
||||
Integer subId = messageValues.getAsInteger(Sms.SUBSCRIPTION_ID);
|
||||
if (subId == null) {
|
||||
subId = ParticipantData.DEFAULT_SELF_SUB_ID;
|
||||
}
|
||||
// Make sure we have a sender address
|
||||
String address = messageValues.getAsString(Sms.ADDRESS);
|
||||
if (TextUtils.isEmpty(address)) {
|
||||
LogUtil.w(TAG, "Received an SMS without an address; using unknown sender.");
|
||||
address = ParticipantData.getUnknownSenderDestination();
|
||||
messageValues.put(Sms.ADDRESS, address);
|
||||
}
|
||||
final ParticipantData rawSender = ParticipantData.getFromRawPhoneBySimLocale(
|
||||
address, subId);
|
||||
|
||||
// TODO: Should use local timestamp for this?
|
||||
final long received = messageValues.getAsLong(Sms.DATE);
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(received);
|
||||
|
||||
// Make sure we've got a thread id
|
||||
final long threadId = MmsSmsUtils.Threads.getOrCreateThreadId(context, address);
|
||||
messageValues.put(Sms.THREAD_ID, threadId);
|
||||
final boolean blocked = BugleDatabaseOperations.isBlockedDestination(
|
||||
db, rawSender.getNormalizedDestination());
|
||||
final String conversationId = BugleDatabaseOperations.
|
||||
getOrCreateConversationFromRecipient(db, threadId, blocked, rawSender);
|
||||
|
||||
final boolean messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(conversationId);
|
||||
final boolean messageInObservableConversation =
|
||||
DataModel.get().isNewMessageObservable(conversationId);
|
||||
|
||||
MessageData message = null;
|
||||
// Only the primary user gets to insert the message into the telephony db and into bugle's
|
||||
// db. The secondary user goes through this path, but skips doing the actual insert. It
|
||||
// goes through this path because it needs to compute messageInFocusedConversation in order
|
||||
// to calculate whether to skip the notification and play a soft sound if the user is
|
||||
// already in the conversation.
|
||||
if (!OsUtil.isSecondaryUser()) {
|
||||
final boolean read = messageValues.getAsBoolean(Sms.Inbox.READ)
|
||||
|| messageInFocusedConversation;
|
||||
// If you have read it you have seen it
|
||||
final boolean seen = read || messageInObservableConversation || blocked;
|
||||
messageValues.put(Sms.Inbox.READ, read ? Integer.valueOf(1) : Integer.valueOf(0));
|
||||
|
||||
// incoming messages are marked as seen in the telephony db
|
||||
messageValues.put(Sms.Inbox.SEEN, 1);
|
||||
|
||||
// Insert into telephony
|
||||
final Uri messageUri = context.getContentResolver().insert(Sms.Inbox.CONTENT_URI,
|
||||
messageValues);
|
||||
|
||||
if (messageUri != null) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ReceiveSmsMessageAction: Inserted SMS message into telephony, "
|
||||
+ "uri = " + messageUri);
|
||||
}
|
||||
} else {
|
||||
LogUtil.e(TAG, "ReceiveSmsMessageAction: Failed to insert SMS into telephony!");
|
||||
}
|
||||
|
||||
final String text = messageValues.getAsString(Sms.BODY);
|
||||
final String subject = messageValues.getAsString(Sms.SUBJECT);
|
||||
final long sent = messageValues.getAsLong(Sms.DATE_SENT);
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(subId);
|
||||
final Integer pathPresent = messageValues.getAsInteger(Sms.REPLY_PATH_PRESENT);
|
||||
final String smsServiceCenter = messageValues.getAsString(Sms.SERVICE_CENTER);
|
||||
String conversationServiceCenter = null;
|
||||
// Only set service center if message REPLY_PATH_PRESENT = 1
|
||||
if (pathPresent != null && pathPresent == 1 && !TextUtils.isEmpty(smsServiceCenter)) {
|
||||
conversationServiceCenter = smsServiceCenter;
|
||||
}
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final String participantId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, rawSender);
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
|
||||
message = MessageData.createReceivedSmsMessage(messageUri, conversationId,
|
||||
participantId, selfId, text, subject, sent, received, seen, read);
|
||||
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db, conversationId,
|
||||
message.getMessageId(), message.getReceivedTimeStamp(), blocked,
|
||||
conversationServiceCenter, true /* shouldAutoSwitchSelfId */);
|
||||
|
||||
final ParticipantData sender = ParticipantData.getFromId(db, participantId);
|
||||
BugleActionToasts.onMessageReceived(conversationId, sender, message);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
LogUtil.i(TAG, "ReceiveSmsMessageAction: Received SMS message " + message.getMessageId()
|
||||
+ " in conversation " + message.getConversationId()
|
||||
+ ", uri = " + messageUri);
|
||||
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ReceiveSmsMessageAction: Not inserting received SMS message for "
|
||||
+ "secondary user.");
|
||||
}
|
||||
}
|
||||
// Show a notification to let the user know a new message has arrived
|
||||
BugleNotifications.update(false/*silent*/, conversationId, BugleNotifications.UPDATE_ALL);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private ReceiveSmsMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ReceiveSmsMessageAction> CREATOR
|
||||
= new Parcelable.Creator<ReceiveSmsMessageAction>() {
|
||||
@Override
|
||||
public ReceiveSmsMessageAction createFromParcel(final Parcel in) {
|
||||
return new ReceiveSmsMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReceiveSmsMessageAction[] newArray(final int size) {
|
||||
return new ReceiveSmsMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action to manually start an MMS download (after failed or manual mms download)
|
||||
*/
|
||||
public class RedownloadMmsAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final int REQUEST_CODE_PENDING_INTENT = 102;
|
||||
|
||||
/**
|
||||
* Download an MMS message
|
||||
*/
|
||||
public static void redownloadMessage(final String messageId) {
|
||||
final RedownloadMmsAction action = new RedownloadMmsAction(messageId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pending intent of for downloading an MMS
|
||||
*/
|
||||
public static PendingIntent getPendingIntentForRedownloadMms(
|
||||
final Context context, final String messageId) {
|
||||
final Action action = new RedownloadMmsAction(messageId);
|
||||
return ActionService.makeStartActionPendingIntent(context,
|
||||
action, REQUEST_CODE_PENDING_INTENT, false /*launchesAnActivity*/);
|
||||
}
|
||||
|
||||
// Core parameters needed for all types of message
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
/**
|
||||
* Constructor used for retrying sending in the background (only message id available)
|
||||
*/
|
||||
RedownloadMmsAction(final String messageId) {
|
||||
super();
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read message from database and change status to allow downloading
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
// Check message can be redownloaded
|
||||
if (message != null && message.canRedownloadMessage()) {
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
|
||||
final ContentValues values = new ContentValues(2);
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD);
|
||||
values.put(DatabaseHelper.MessageColumns.RETRY_START_TIMESTAMP, timestamp);
|
||||
|
||||
// Row must exist as was just loaded above (on ActionService thread)
|
||||
BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
|
||||
// Whether we succeeded or failed we will check and maybe schedule some more work
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
} else {
|
||||
message = null;
|
||||
LogUtil.e(LogUtil.BUGLE_TAG,
|
||||
"Attempt to download a missing or un-redownloadable message");
|
||||
}
|
||||
// Immediately update the notifications in case we came from the download action from a
|
||||
// heads-up notification. This will dismiss the heads-up notification.
|
||||
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
|
||||
return message;
|
||||
}
|
||||
|
||||
private RedownloadMmsAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<RedownloadMmsAction> CREATOR
|
||||
= new Parcelable.Creator<RedownloadMmsAction>() {
|
||||
@Override
|
||||
public RedownloadMmsAction createFromParcel(final Parcel in) {
|
||||
return new RedownloadMmsAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedownloadMmsAction[] newArray(final int size) {
|
||||
return new RedownloadMmsAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to manually resend an outgoing message
|
||||
*/
|
||||
public class ResendMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Manual send of existing message (no listener)
|
||||
*/
|
||||
public static void resendMessage(final String messageId) {
|
||||
final ResendMessageAction action = new ResendMessageAction(messageId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
// Core parameters needed for all types of message
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
/**
|
||||
* Constructor used for retrying sending in the background (only message id available)
|
||||
*/
|
||||
ResendMessageAction(final String messageId) {
|
||||
super();
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read message from database and change status to allow sending
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
// Check message can be resent
|
||||
if (message != null && message.canResendMessage()) {
|
||||
final boolean isMms = message.getIsMms();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
if (isMms) {
|
||||
// MMS expects timestamp rounded to nearest second
|
||||
timestamp = 1000 * ((timestamp + 500) / 1000);
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "ResendMessageAction: Resending message " + messageId
|
||||
+ "; changed timestamp from " + message.getReceivedTimeStamp() + " to "
|
||||
+ timestamp);
|
||||
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.STATUS, MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND);
|
||||
values.put(MessageColumns.RECEIVED_TIMESTAMP, timestamp);
|
||||
values.put(MessageColumns.SENT_TIMESTAMP, timestamp);
|
||||
values.put(MessageColumns.RETRY_START_TIMESTAMP, timestamp);
|
||||
|
||||
// Row must exist as was just loaded above (on ActionService thread)
|
||||
BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
|
||||
// Whether we succeeded or failed we will check and maybe schedule some more work
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
|
||||
return message;
|
||||
} else {
|
||||
String error = "ResendMessageAction: Cannot resend message " + messageId + "; ";
|
||||
if (message != null) {
|
||||
error += ("status = " + MessageData.getStatusDescription(message.getStatus()));
|
||||
} else {
|
||||
error += "not found in database";
|
||||
}
|
||||
LogUtil.e(TAG, error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResendMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ResendMessageAction> CREATOR
|
||||
= new Parcelable.Creator<ResendMessageAction>() {
|
||||
@Override
|
||||
public ResendMessageAction createFromParcel(final Parcel in) {
|
||||
return new ResendMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResendMessageAction[] newArray(final int size) {
|
||||
return new ResendMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.provider.Telephony.Sms;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Action used to send an outgoing message. It writes MMS messages to the telephony db
|
||||
* ({@link InsertNewMessageAction}) writes SMS messages to the telephony db). It also
|
||||
* initiates the actual sending. It will all be used for re-sending a failed message.
|
||||
* NOTE: This action must queue a ProcessPendingMessagesAction when it is done (success or failure).
|
||||
* <p>
|
||||
* This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
|
||||
* access the EXTRA_* fields for setting up the 'sent' pending intent.
|
||||
*/
|
||||
public class SendMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Queue sending of existing message (can only be called during execute of action)
|
||||
*/
|
||||
static boolean queueForSendInBackground(final String messageId,
|
||||
final Action processingAction) {
|
||||
final SendMessageAction action = new SendMessageAction();
|
||||
return action.queueAction(messageId, processingAction);
|
||||
}
|
||||
|
||||
public static final boolean DEFAULT_DELIVERY_REPORT_MODE = false;
|
||||
public static final int MAX_SMS_RETRY = 3;
|
||||
|
||||
// Core parameters needed for all types of message
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_MESSAGE = "message";
|
||||
private static final String KEY_MESSAGE_URI = "message_uri";
|
||||
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
|
||||
// For sms messages a few extra values are included in the bundle
|
||||
private static final String KEY_RECIPIENT = "recipient";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_SMS_SERVICE_CENTER = "sms_service_center";
|
||||
|
||||
// Values we attach to the pending intent that's fired when the message is sent.
|
||||
// Only applicable when sending via the platform APIs on L+.
|
||||
public static final String KEY_SUB_ID = "sub_id";
|
||||
public static final String EXTRA_MESSAGE_ID = "message_id";
|
||||
public static final String EXTRA_UPDATED_MESSAGE_URI = "updated_message_uri";
|
||||
public static final String EXTRA_CONTENT_URI = "content_uri";
|
||||
public static final String EXTRA_RESPONSE_IMPORTANT = "response_important";
|
||||
|
||||
/**
|
||||
* Constructor used for retrying sending in the background (only message id available)
|
||||
*/
|
||||
private SendMessageAction() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read message from database and queue actual sending
|
||||
*/
|
||||
private boolean queueAction(final String messageId, final Action processingAction) {
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
// Check message can be resent
|
||||
if (message != null && message.canSendMessage()) {
|
||||
final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
|
||||
|
||||
final ParticipantData self = BugleDatabaseOperations.getExistingParticipant(
|
||||
db, message.getSelfId());
|
||||
final Uri messageUri = message.getSmsMessageUri();
|
||||
final String conversationId = message.getConversationId();
|
||||
|
||||
// Update message status
|
||||
if (message.getYetToSend()) {
|
||||
// Initial sending of message
|
||||
message.markMessageSending(timestamp);
|
||||
} else {
|
||||
// Automatic resend of message
|
||||
message.markMessageResending(timestamp);
|
||||
}
|
||||
if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) {
|
||||
// If message is missing in the telephony database we don't need to send it
|
||||
return false;
|
||||
}
|
||||
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
|
||||
|
||||
// Update action state with parameters needed for background sending
|
||||
actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri);
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients);
|
||||
actionParameters.putInt(KEY_SUB_ID, self.getSubId());
|
||||
actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());
|
||||
|
||||
if (isSms) {
|
||||
final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation(
|
||||
db, conversationId);
|
||||
actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc);
|
||||
|
||||
if (recipients.size() == 1) {
|
||||
final String recipient = recipients.get(0);
|
||||
|
||||
actionParameters.putString(KEY_RECIPIENT, recipient);
|
||||
// Queue actual sending for SMS
|
||||
processingAction.requestBackgroundWork(this);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId
|
||||
+ " for sending");
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed");
|
||||
}
|
||||
} else {
|
||||
// Queue actual sending for MMS
|
||||
processingAction.requestBackgroundWork(this);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId
|
||||
+ " for sending");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Never called
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
Assert.fail("SendMessageAction must be queued rather than started");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message on background worker thread
|
||||
*/
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
|
||||
Uri updatedMessageUri = null;
|
||||
final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);
|
||||
|
||||
LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message "
|
||||
+ messageId + " in conversation " + message.getConversationId());
|
||||
|
||||
int status;
|
||||
int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
|
||||
int resultCode = MessageData.UNKNOWN_RESULT_CODE;
|
||||
if (isSms) {
|
||||
Assert.notNull(messageUri);
|
||||
final String recipient = actionParameters.getString(KEY_RECIPIENT);
|
||||
final String messageText = message.getMessageText();
|
||||
final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER);
|
||||
final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId);
|
||||
|
||||
status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId,
|
||||
smsServiceCenter, deliveryReportRequired);
|
||||
} else {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ArrayList<String> recipients =
|
||||
actionParameters.getStringArrayList(KEY_RECIPIENTS);
|
||||
if (messageUri == null) {
|
||||
final long timestamp = message.getReceivedTimeStamp();
|
||||
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(timestamp);
|
||||
|
||||
// For MMS messages first need to write to telephony (resizing images if needed)
|
||||
updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients,
|
||||
message, subId, subPhoneNumber, timestamp);
|
||||
if (updatedMessageUri != null) {
|
||||
messageUri = updatedMessageUri;
|
||||
// To prevent Sync seeing inconsistent state must write to DB on this thread
|
||||
updateMessageUri(messageId, updatedMessageUri);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId
|
||||
+ " with new uri " + messageUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (messageUri != null) {
|
||||
// Actually send the MMS
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri);
|
||||
final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId,
|
||||
messageUri, extras);
|
||||
if (result == MmsUtils.STATUS_PENDING) {
|
||||
// Async send, so no status yet
|
||||
LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId
|
||||
+ " asynchronously; waiting for callback to finish processing");
|
||||
return null;
|
||||
}
|
||||
status = result.status;
|
||||
rawStatus = result.rawStatus;
|
||||
resultCode = result.resultCode;
|
||||
} else {
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
}
|
||||
}
|
||||
|
||||
// When we fast-fail before calling the MMS lib APIs (e.g. airplane mode,
|
||||
// sending message is deleted).
|
||||
ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri,
|
||||
updatedMessageUri, subId, isSms, status, rawStatus, resultCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateMessageUri(final String messageId, final Uri updatedMessageUri) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString());
|
||||
BugleDatabaseOperations.updateMessageRow(db, messageId, values);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
// Nothing to do here, post-send tasks handled by ProcessSentMessageAction
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update message status to reflect success or failure
|
||||
*/
|
||||
@Override
|
||||
protected Object processBackgroundFailure() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
|
||||
final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE);
|
||||
final int httpStatusCode =
|
||||
actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE);
|
||||
|
||||
ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */,
|
||||
MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
|
||||
isSms, this, subId, resultCode, httpStatusCode);
|
||||
|
||||
// Whether we succeeded or failed we will check and maybe schedule some more work
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(true, this);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the message status (and message itself if necessary)
|
||||
* @param isSms whether this is an SMS or MMS
|
||||
* @param message message to update
|
||||
* @param updatedMessageUri message uri for newly-inserted messages; null otherwise
|
||||
* @param clearSeen whether the message 'seen' status should be reset if error occurs
|
||||
*/
|
||||
public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message,
|
||||
final Uri updatedMessageUri, final boolean clearSeen) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// TODO: We're optimistically setting the type/box of outgoing messages to
|
||||
// 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX
|
||||
// instead, but if we do that, it's possible that the Messaging app will try to send them
|
||||
// as part of its clean-up logic that runs when it starts (http://b/18155366).
|
||||
//
|
||||
// We also use the wrong status when inserting queued SMS messages in
|
||||
// InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be
|
||||
// QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX).
|
||||
|
||||
boolean updatedTelephony = true;
|
||||
int messageBox;
|
||||
int type;
|
||||
switch(message.getStatus()) {
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
|
||||
type = Sms.MESSAGE_TYPE_SENT;
|
||||
messageBox = Mms.MESSAGE_BOX_SENT;
|
||||
break;
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
||||
type = Sms.MESSAGE_TYPE_SENT;
|
||||
messageBox = Mms.MESSAGE_BOX_SENT;
|
||||
break;
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
|
||||
type = Sms.MESSAGE_TYPE_SENT;
|
||||
messageBox = Mms.MESSAGE_BOX_SENT;
|
||||
break;
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
||||
type = Sms.MESSAGE_TYPE_FAILED;
|
||||
messageBox = Mms.MESSAGE_BOX_FAILED;
|
||||
break;
|
||||
default:
|
||||
type = Sms.MESSAGE_TYPE_ALL;
|
||||
messageBox = Mms.MESSAGE_BOX_ALL;
|
||||
break;
|
||||
}
|
||||
// First in the telephony DB
|
||||
if (isSms) {
|
||||
// Ignore update message Uri
|
||||
if (type != Sms.MESSAGE_TYPE_ALL) {
|
||||
if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(),
|
||||
type, message.getReceivedTimeStamp())) {
|
||||
message.markMessageFailed(message.getSentTimeStamp());
|
||||
updatedTelephony = false;
|
||||
}
|
||||
}
|
||||
} else if (message.getSmsMessageUri() != null) {
|
||||
if (messageBox != Mms.MESSAGE_BOX_ALL) {
|
||||
if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(),
|
||||
messageBox, message.getReceivedTimeStamp())) {
|
||||
message.markMessageFailed(message.getSentTimeStamp());
|
||||
updatedTelephony = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updatedTelephony) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId()
|
||||
+ " in telephony (" + message.getSmsMessageUri() + ")");
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId()
|
||||
+ " in telephony (" + message.getSmsMessageUri() + "); marking message failed");
|
||||
}
|
||||
|
||||
// Update the local DB
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (updatedMessageUri != null) {
|
||||
// Update all message and part fields
|
||||
BugleDatabaseOperations.updateMessageInTransaction(db, message);
|
||||
BugleDatabaseOperations.refreshConversationMetadataInTransaction(
|
||||
db, message.getConversationId(), false/* shouldAutoSwitchSelfId */,
|
||||
false/*archived*/);
|
||||
} else {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.STATUS, message.getStatus());
|
||||
|
||||
if (clearSeen) {
|
||||
// When a message fails to send, the message needs to
|
||||
// be unseen to be selected as an error notification.
|
||||
values.put(MessageColumns.SEEN, 0);
|
||||
}
|
||||
values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp());
|
||||
values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus());
|
||||
|
||||
BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(),
|
||||
values);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId() + " in local db. Timestamp = "
|
||||
+ message.getReceivedTimeStamp());
|
||||
}
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
if (updatedMessageUri != null) {
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
}
|
||||
|
||||
return updatedTelephony;
|
||||
}
|
||||
|
||||
private SendMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<SendMessageAction> CREATOR
|
||||
= new Parcelable.Creator<SendMessageAction>() {
|
||||
@Override
|
||||
public SendMessageAction createFromParcel(final Parcel in) {
|
||||
return new SendMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageAction[] newArray(final int size) {
|
||||
return new SendMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,712 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.provider.Telephony.Sms;
|
||||
import android.support.v4.util.LongSparseArray;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.mmslib.SqliteWrapper;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.DatabaseMessages.DatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Class holding a pair of cursors - one for local db and one for telephony provider - allowing
|
||||
* synchronous stepping through messages as part of sync.
|
||||
*/
|
||||
class SyncCursorPair {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
static final long SYNC_COMPLETE = -1L;
|
||||
static final long SYNC_STARTING = Long.MAX_VALUE;
|
||||
|
||||
private CursorIterator mLocalCursorIterator;
|
||||
private CursorIterator mRemoteCursorsIterator;
|
||||
|
||||
private final String mLocalSelection;
|
||||
private final String mRemoteSmsSelection;
|
||||
private final String mRemoteMmsSelection;
|
||||
|
||||
/**
|
||||
* Check if SMS has been synchronized. We compare the counts of messages on both
|
||||
* sides and return true if they are equal.
|
||||
*
|
||||
* Note that this may not be the most reliable way to tell if messages are in sync.
|
||||
* For example, the local misses one message and has one obsolete message.
|
||||
* However, we have background sms sync once a while, also some other events might
|
||||
* trigger a full sync. So we will eventually catch up. And this should be rare to
|
||||
* happen.
|
||||
*
|
||||
* @return If sms is in sync with telephony sms/mms providers
|
||||
*/
|
||||
static boolean allSynchronized(final DatabaseWrapper db) {
|
||||
return isSynchronized(db, LOCAL_MESSAGES_SELECTION, null,
|
||||
getSmsTypeSelectionSql(), null, getMmsTypeSelectionSql(), null);
|
||||
}
|
||||
|
||||
SyncCursorPair(final long lowerBound, final long upperBound) {
|
||||
mLocalSelection = getTimeConstrainedQuery(
|
||||
LOCAL_MESSAGES_SELECTION,
|
||||
MessageColumns.RECEIVED_TIMESTAMP,
|
||||
lowerBound,
|
||||
upperBound,
|
||||
null /* threadColumn */, null /* threadId */);
|
||||
mRemoteSmsSelection = getTimeConstrainedQuery(
|
||||
getSmsTypeSelectionSql(),
|
||||
"date",
|
||||
lowerBound,
|
||||
upperBound,
|
||||
null /* threadColumn */, null /* threadId */);
|
||||
mRemoteMmsSelection = getTimeConstrainedQuery(
|
||||
getMmsTypeSelectionSql(),
|
||||
"date",
|
||||
((lowerBound < 0) ? lowerBound : (lowerBound + 999) / 1000), /*seconds*/
|
||||
((upperBound < 0) ? upperBound : (upperBound + 999) / 1000), /*seconds*/
|
||||
null /* threadColumn */, null /* threadId */);
|
||||
}
|
||||
|
||||
SyncCursorPair(final long threadId, final String conversationId) {
|
||||
mLocalSelection = getTimeConstrainedQuery(
|
||||
LOCAL_MESSAGES_SELECTION,
|
||||
MessageColumns.RECEIVED_TIMESTAMP,
|
||||
-1L,
|
||||
-1L,
|
||||
MessageColumns.CONVERSATION_ID, conversationId);
|
||||
// Find all SMS messages (excluding drafts) within the sync window
|
||||
mRemoteSmsSelection = getTimeConstrainedQuery(
|
||||
getSmsTypeSelectionSql(),
|
||||
"date",
|
||||
-1L,
|
||||
-1L,
|
||||
Sms.THREAD_ID, Long.toString(threadId));
|
||||
mRemoteMmsSelection = getTimeConstrainedQuery(
|
||||
getMmsTypeSelectionSql(),
|
||||
"date",
|
||||
-1L, /*seconds*/
|
||||
-1L, /*seconds*/
|
||||
Mms.THREAD_ID, Long.toString(threadId));
|
||||
}
|
||||
|
||||
void query(final DatabaseWrapper db) {
|
||||
// Load local messages in the sync window
|
||||
mLocalCursorIterator = new LocalCursorIterator(db, mLocalSelection);
|
||||
// Load remote messages in the sync window
|
||||
mRemoteCursorsIterator = new RemoteCursorsIterator(mRemoteSmsSelection,
|
||||
mRemoteMmsSelection);
|
||||
}
|
||||
|
||||
boolean isSynchronized(final DatabaseWrapper db) {
|
||||
return isSynchronized(db, mLocalSelection, null, mRemoteSmsSelection,
|
||||
null, mRemoteMmsSelection, null);
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (mLocalCursorIterator != null) {
|
||||
mLocalCursorIterator.close();
|
||||
}
|
||||
if (mRemoteCursorsIterator != null) {
|
||||
mRemoteCursorsIterator.close();
|
||||
}
|
||||
}
|
||||
|
||||
long scan(final int maxMessagesToScan,
|
||||
final int maxMessagesToUpdate, final ArrayList<SmsMessage> smsToAdd,
|
||||
final LongSparseArray<MmsMessage> mmsToAdd,
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete,
|
||||
final SyncManager.ThreadInfoCache threadInfoCache) {
|
||||
// Set of local messages matched with the timestamp of a remote message
|
||||
final Set<DatabaseMessage> matchedLocalMessages = Sets.newHashSet();
|
||||
// Set of remote messages matched with the timestamp of a local message
|
||||
final Set<DatabaseMessage> matchedRemoteMessages = Sets.newHashSet();
|
||||
long lastTimestampMillis = SYNC_STARTING;
|
||||
// Number of messages scanned local and remote
|
||||
int localCount = 0;
|
||||
int remoteCount = 0;
|
||||
// Seed the initial values of remote and local messages for comparison
|
||||
DatabaseMessage remoteMessage = mRemoteCursorsIterator.next();
|
||||
DatabaseMessage localMessage = mLocalCursorIterator.next();
|
||||
// Iterate through messages on both sides in reverse time order
|
||||
// Import messages in remote not in local, delete messages in local not in remote
|
||||
while (localCount + remoteCount < maxMessagesToScan && smsToAdd.size()
|
||||
+ mmsToAdd.size() + messagesToDelete.size() < maxMessagesToUpdate) {
|
||||
if (remoteMessage == null && localMessage == null) {
|
||||
// No more message on both sides - scan complete
|
||||
lastTimestampMillis = SYNC_COMPLETE;
|
||||
break;
|
||||
} else if ((remoteMessage == null && localMessage != null) ||
|
||||
(localMessage != null && remoteMessage != null &&
|
||||
localMessage.getTimestampInMillis()
|
||||
> remoteMessage.getTimestampInMillis())) {
|
||||
// Found a local message that is not in remote db
|
||||
// Delete the local message
|
||||
messagesToDelete.add((LocalDatabaseMessage) localMessage);
|
||||
lastTimestampMillis = Math.min(lastTimestampMillis,
|
||||
localMessage.getTimestampInMillis());
|
||||
// Advance to next local message
|
||||
localMessage = mLocalCursorIterator.next();
|
||||
localCount += 1;
|
||||
} else if ((localMessage == null && remoteMessage != null) ||
|
||||
(localMessage != null && remoteMessage != null &&
|
||||
localMessage.getTimestampInMillis()
|
||||
< remoteMessage.getTimestampInMillis())) {
|
||||
// Found a remote message that is not in local db
|
||||
// Add the remote message
|
||||
saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache);
|
||||
lastTimestampMillis = Math.min(lastTimestampMillis,
|
||||
remoteMessage.getTimestampInMillis());
|
||||
// Advance to next remote message
|
||||
remoteMessage = mRemoteCursorsIterator.next();
|
||||
remoteCount += 1;
|
||||
} else {
|
||||
// Found remote and local messages at the same timestamp
|
||||
final long matchedTimestamp = localMessage.getTimestampInMillis();
|
||||
lastTimestampMillis = Math.min(lastTimestampMillis, matchedTimestamp);
|
||||
// Get the next local and remote messages
|
||||
final DatabaseMessage remoteMessagePeek = mRemoteCursorsIterator.next();
|
||||
final DatabaseMessage localMessagePeek = mLocalCursorIterator.next();
|
||||
// Check if only one message on each side matches the current timestamp
|
||||
// by looking at the next messages on both sides. If they are either null
|
||||
// (meaning no more messages) or having a different timestamp. We want
|
||||
// to optimize for this since this is the most common case when majority
|
||||
// of the messages are in sync (so they one-to-one pair up at each timestamp),
|
||||
// by not allocating the data structures required to compare a set of
|
||||
// messages from both sides.
|
||||
if ((remoteMessagePeek == null ||
|
||||
remoteMessagePeek.getTimestampInMillis() != matchedTimestamp) &&
|
||||
(localMessagePeek == null ||
|
||||
localMessagePeek.getTimestampInMillis() != matchedTimestamp)) {
|
||||
// Optimize the common case where only one message on each side
|
||||
// that matches the same timestamp
|
||||
if (!remoteMessage.equals(localMessage)) {
|
||||
// local != remote
|
||||
// Delete local message
|
||||
messagesToDelete.add((LocalDatabaseMessage) localMessage);
|
||||
// Add remote message
|
||||
saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache);
|
||||
}
|
||||
// Get next local and remote messages
|
||||
localMessage = localMessagePeek;
|
||||
remoteMessage = remoteMessagePeek;
|
||||
localCount += 1;
|
||||
remoteCount += 1;
|
||||
} else {
|
||||
// Rare case in which multiple messages are in the same timestamp
|
||||
// on either or both sides
|
||||
// Gather all the matched remote messages
|
||||
matchedRemoteMessages.clear();
|
||||
matchedRemoteMessages.add(remoteMessage);
|
||||
remoteCount += 1;
|
||||
remoteMessage = remoteMessagePeek;
|
||||
while (remoteMessage != null &&
|
||||
remoteMessage.getTimestampInMillis() == matchedTimestamp) {
|
||||
Assert.isTrue(!matchedRemoteMessages.contains(remoteMessage));
|
||||
matchedRemoteMessages.add(remoteMessage);
|
||||
remoteCount += 1;
|
||||
remoteMessage = mRemoteCursorsIterator.next();
|
||||
}
|
||||
// Gather all the matched local messages
|
||||
matchedLocalMessages.clear();
|
||||
matchedLocalMessages.add(localMessage);
|
||||
localCount += 1;
|
||||
localMessage = localMessagePeek;
|
||||
while (localMessage != null &&
|
||||
localMessage.getTimestampInMillis() == matchedTimestamp) {
|
||||
if (matchedLocalMessages.contains(localMessage)) {
|
||||
// Duplicate message is local database is deleted
|
||||
messagesToDelete.add((LocalDatabaseMessage) localMessage);
|
||||
} else {
|
||||
matchedLocalMessages.add(localMessage);
|
||||
}
|
||||
localCount += 1;
|
||||
localMessage = mLocalCursorIterator.next();
|
||||
}
|
||||
// Delete messages local only
|
||||
for (final DatabaseMessage msg : Sets.difference(
|
||||
matchedLocalMessages, matchedRemoteMessages)) {
|
||||
messagesToDelete.add((LocalDatabaseMessage) msg);
|
||||
}
|
||||
// Add messages remote only
|
||||
for (final DatabaseMessage msg : Sets.difference(
|
||||
matchedRemoteMessages, matchedLocalMessages)) {
|
||||
saveMessageToAdd(smsToAdd, mmsToAdd, msg, threadInfoCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastTimestampMillis;
|
||||
}
|
||||
|
||||
DatabaseMessage getLocalMessage() {
|
||||
return mLocalCursorIterator.next();
|
||||
}
|
||||
|
||||
DatabaseMessage getRemoteMessage() {
|
||||
return mRemoteCursorsIterator.next();
|
||||
}
|
||||
|
||||
int getLocalPosition() {
|
||||
return mLocalCursorIterator.getPosition();
|
||||
}
|
||||
|
||||
int getRemotePosition() {
|
||||
return mRemoteCursorsIterator.getPosition();
|
||||
}
|
||||
|
||||
int getLocalCount() {
|
||||
return mLocalCursorIterator.getCount();
|
||||
}
|
||||
|
||||
int getRemoteCount() {
|
||||
return mRemoteCursorsIterator.getCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* An iterator for a database cursor
|
||||
*/
|
||||
interface CursorIterator {
|
||||
/**
|
||||
* Move to next element in the cursor
|
||||
*
|
||||
* @return The next element (which becomes the current)
|
||||
*/
|
||||
public DatabaseMessage next();
|
||||
/**
|
||||
* Close the cursor
|
||||
*/
|
||||
public void close();
|
||||
/**
|
||||
* Get the position
|
||||
*/
|
||||
public int getPosition();
|
||||
/**
|
||||
* Get the count
|
||||
*/
|
||||
public int getCount();
|
||||
}
|
||||
|
||||
private static final String ORDER_BY_DATE_DESC = "date DESC";
|
||||
|
||||
// A subquery that selects SMS/MMS messages in Bugle which are also in telephony
|
||||
private static final String LOCAL_MESSAGES_SELECTION = String.format(
|
||||
Locale.US,
|
||||
"(%s NOTNULL)",
|
||||
MessageColumns.SMS_MESSAGE_URI);
|
||||
|
||||
private static final String ORDER_BY_TIMESTAMP_DESC =
|
||||
MessageColumns.RECEIVED_TIMESTAMP + " DESC";
|
||||
|
||||
// TODO : This should move into the provider
|
||||
private static class LocalMessageQuery {
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
MessageColumns._ID,
|
||||
MessageColumns.RECEIVED_TIMESTAMP,
|
||||
MessageColumns.SMS_MESSAGE_URI,
|
||||
MessageColumns.PROTOCOL,
|
||||
MessageColumns.CONVERSATION_ID,
|
||||
};
|
||||
private static final int INDEX_MESSAGE_ID = 0;
|
||||
private static final int INDEX_MESSAGE_TIMESTAMP = 1;
|
||||
private static final int INDEX_SMS_MESSAGE_URI = 2;
|
||||
private static final int INDEX_MESSAGE_SMS_TYPE = 3;
|
||||
private static final int INDEX_CONVERSATION_ID = 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class provides the same DatabaseMessage interface over a local SMS db message
|
||||
*/
|
||||
private static LocalDatabaseMessage getLocalDatabaseMessage(final Cursor cursor) {
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
return new LocalDatabaseMessage(
|
||||
cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_ID),
|
||||
cursor.getInt(LocalMessageQuery.INDEX_MESSAGE_SMS_TYPE),
|
||||
cursor.getString(LocalMessageQuery.INDEX_SMS_MESSAGE_URI),
|
||||
cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_TIMESTAMP),
|
||||
cursor.getString(LocalMessageQuery.INDEX_CONVERSATION_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* The buffered cursor iterator for local SMS
|
||||
*/
|
||||
private static class LocalCursorIterator implements CursorIterator {
|
||||
private Cursor mCursor;
|
||||
private final DatabaseWrapper mDatabase;
|
||||
|
||||
LocalCursorIterator(final DatabaseWrapper database, final String selection)
|
||||
throws SQLiteException {
|
||||
mDatabase = database;
|
||||
try {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncCursorPair: Querying for local messages; selection = "
|
||||
+ selection);
|
||||
}
|
||||
mCursor = mDatabase.query(
|
||||
DatabaseHelper.MESSAGES_TABLE,
|
||||
LocalMessageQuery.PROJECTION,
|
||||
selection,
|
||||
null /*selectionArgs*/,
|
||||
null/*groupBy*/,
|
||||
null/*having*/,
|
||||
ORDER_BY_TIMESTAMP_DESC);
|
||||
} catch (final SQLiteException e) {
|
||||
LogUtil.e(TAG, "SyncCursorPair: failed to query local sms/mms", e);
|
||||
// Can't query local database. So let's throw up the exception and abort sync
|
||||
// because we may end up import duplicate messages.
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseMessage next() {
|
||||
if (mCursor != null && mCursor.moveToNext()) {
|
||||
return getLocalDatabaseMessage(mCursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return (mCursor == null ? 0 : mCursor.getCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return (mCursor == null ? 0 : mCursor.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mCursor != null) {
|
||||
mCursor.close();
|
||||
mCursor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The cursor iterator for remote sms.
|
||||
* Since SMS and MMS are stored in different tables in telephony provider,
|
||||
* this class merges the two cursors and provides a unified view of messages
|
||||
* from both cursors. Note that the order is DESC.
|
||||
*/
|
||||
private static class RemoteCursorsIterator implements CursorIterator {
|
||||
private Cursor mSmsCursor;
|
||||
private Cursor mMmsCursor;
|
||||
private DatabaseMessage mNextSms;
|
||||
private DatabaseMessage mNextMms;
|
||||
|
||||
RemoteCursorsIterator(final String smsSelection, final String mmsSelection)
|
||||
throws SQLiteException {
|
||||
mSmsCursor = null;
|
||||
mMmsCursor = null;
|
||||
try {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncCursorPair: Querying for remote SMS; selection = "
|
||||
+ smsSelection);
|
||||
}
|
||||
mSmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Sms.CONTENT_URI,
|
||||
SmsMessage.getProjection(),
|
||||
smsSelection,
|
||||
null /* selectionArgs */,
|
||||
ORDER_BY_DATE_DESC);
|
||||
if (mSmsCursor == null) {
|
||||
LogUtil.w(TAG, "SyncCursorPair: Remote SMS query returned null cursor; "
|
||||
+ "need to cancel sync");
|
||||
throw new RuntimeException("Null cursor from remote SMS query");
|
||||
}
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncCursorPair: Querying for remote MMS; selection = "
|
||||
+ mmsSelection);
|
||||
}
|
||||
mMmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Mms.CONTENT_URI,
|
||||
DatabaseMessages.MmsMessage.getProjection(),
|
||||
mmsSelection,
|
||||
null /* selectionArgs */,
|
||||
ORDER_BY_DATE_DESC);
|
||||
if (mMmsCursor == null) {
|
||||
LogUtil.w(TAG, "SyncCursorPair: Remote MMS query returned null cursor; "
|
||||
+ "need to cancel sync");
|
||||
throw new RuntimeException("Null cursor from remote MMS query");
|
||||
}
|
||||
// Move to the first element in the combined stream from both cursors
|
||||
mNextSms = getSmsCursorNext();
|
||||
mNextMms = getMmsCursorNext();
|
||||
} catch (final SQLiteException e) {
|
||||
LogUtil.e(TAG, "SyncCursorPair: failed to query remote messages", e);
|
||||
// If we ignore this, the following code would think there is no remote message
|
||||
// and will delete all the local sms. We should be cautious here. So instead,
|
||||
// let's throw the exception to the caller and abort sms sync. We do the same
|
||||
// thing if either of the remote cursors is null.
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseMessage next() {
|
||||
DatabaseMessage result = null;
|
||||
if (mNextSms != null && mNextMms != null) {
|
||||
if (mNextSms.getTimestampInMillis() >= mNextMms.getTimestampInMillis()) {
|
||||
result = mNextSms;
|
||||
mNextSms = getSmsCursorNext();
|
||||
} else {
|
||||
result = mNextMms;
|
||||
mNextMms = getMmsCursorNext();
|
||||
}
|
||||
} else {
|
||||
if (mNextSms != null) {
|
||||
result = mNextSms;
|
||||
mNextSms = getSmsCursorNext();
|
||||
} else {
|
||||
result = mNextMms;
|
||||
mNextMms = getMmsCursorNext();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private DatabaseMessage getSmsCursorNext() {
|
||||
if (mSmsCursor != null && mSmsCursor.moveToNext()) {
|
||||
return SmsMessage.get(mSmsCursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseMessage getMmsCursorNext() {
|
||||
if (mMmsCursor != null && mMmsCursor.moveToNext()) {
|
||||
return MmsMessage.get(mMmsCursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Return approximate cursor position allowing for read ahead on two cursors (hence -1)
|
||||
public int getPosition() {
|
||||
return (mSmsCursor == null ? 0 : mSmsCursor.getPosition()) +
|
||||
(mMmsCursor == null ? 0 : mMmsCursor.getPosition()) - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return (mSmsCursor == null ? 0 : mSmsCursor.getCount()) +
|
||||
(mMmsCursor == null ? 0 : mMmsCursor.getCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mSmsCursor != null) {
|
||||
mSmsCursor.close();
|
||||
mSmsCursor = null;
|
||||
}
|
||||
if (mMmsCursor != null) {
|
||||
mMmsCursor.close();
|
||||
mMmsCursor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type selection for importing sms messages. Only SENT and INBOX messages are imported.
|
||||
*
|
||||
* @return The SQL selection for importing sms messages
|
||||
*/
|
||||
public static String getSmsTypeSelectionSql() {
|
||||
return MmsUtils.getSmsTypeSelectionSql();
|
||||
}
|
||||
|
||||
/**
|
||||
* Type selection for importing mms messages.
|
||||
*
|
||||
* Criteria:
|
||||
* MESSAGE_BOX is INBOX, SENT or OUTBOX
|
||||
* MESSAGE_TYPE is SEND_REQ (sent), RETRIEVE_CONF (received) or NOTIFICATION_IND (download)
|
||||
*
|
||||
* @return The SQL selection for importing mms messages. This selects the message type,
|
||||
* not including the selection on timestamp.
|
||||
*/
|
||||
public static String getMmsTypeSelectionSql() {
|
||||
return MmsUtils.getMmsTypeSelectionSql();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a SQL selection string using an existing selection and time window limits
|
||||
* The limits are not applied if the value is < 0
|
||||
*
|
||||
* @param typeSelection The existing selection
|
||||
* @param from The inclusive lower bound
|
||||
* @param to The exclusive upper bound
|
||||
* @return The created SQL selection
|
||||
*/
|
||||
private static String getTimeConstrainedQuery(final String typeSelection,
|
||||
final String timeColumn, final long from, final long to,
|
||||
final String threadColumn, final String threadId) {
|
||||
final StringBuilder queryBuilder = new StringBuilder();
|
||||
queryBuilder.append(typeSelection);
|
||||
if (from > 0) {
|
||||
queryBuilder.append(" AND ").append(timeColumn).append(">=").append(from);
|
||||
}
|
||||
if (to > 0) {
|
||||
queryBuilder.append(" AND ").append(timeColumn).append("<").append(to);
|
||||
}
|
||||
if (!TextUtils.isEmpty(threadColumn) && !TextUtils.isEmpty(threadId)) {
|
||||
queryBuilder.append(" AND ").append(threadColumn).append("=").append(threadId);
|
||||
}
|
||||
return queryBuilder.toString();
|
||||
}
|
||||
|
||||
private static final String[] COUNT_PROJECTION = new String[] { "count()" };
|
||||
|
||||
private static int getCountFromCursor(final Cursor cursor) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
// We should only return a number if we were able to read it from the cursor.
|
||||
// Otherwise, we throw an exception to cancel the sync.
|
||||
String cursorDesc = "";
|
||||
if (cursor == null) {
|
||||
cursorDesc = "null";
|
||||
} else if (cursor.getCount() == 0) {
|
||||
cursorDesc = "empty";
|
||||
}
|
||||
throw new IllegalArgumentException("Cannot get count from " + cursorDesc + " cursor");
|
||||
}
|
||||
|
||||
private void saveMessageToAdd(final List<SmsMessage> smsToAdd,
|
||||
final LongSparseArray<MmsMessage> mmsToAdd, final DatabaseMessage message,
|
||||
final ThreadInfoCache threadInfoCache) {
|
||||
long threadId;
|
||||
if (message.getProtocol() == MessageData.PROTOCOL_MMS) {
|
||||
final MmsMessage mms = (MmsMessage) message;
|
||||
mmsToAdd.append(mms.getId(), mms);
|
||||
threadId = mms.mThreadId;
|
||||
} else {
|
||||
final SmsMessage sms = (SmsMessage) message;
|
||||
smsToAdd.add(sms);
|
||||
threadId = sms.mThreadId;
|
||||
}
|
||||
// Cache the lookup and canonicalization of the phone number outside of the transaction...
|
||||
threadInfoCache.getThreadRecipients(threadId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SMS has been synchronized. We compare the counts of messages on both
|
||||
* sides and return true if they are equal.
|
||||
*
|
||||
* Note that this may not be the most reliable way to tell if messages are in sync.
|
||||
* For example, the local misses one message and has one obsolete message.
|
||||
* However, we have background sms sync once a while, also some other events might
|
||||
* trigger a full sync. So we will eventually catch up. And this should be rare to
|
||||
* happen.
|
||||
*
|
||||
* @return If sms is in sync with telephony sms/mms providers
|
||||
*/
|
||||
private static boolean isSynchronized(final DatabaseWrapper db, final String localSelection,
|
||||
final String[] localSelectionArgs, final String smsSelection,
|
||||
final String[] smsSelectionArgs, final String mmsSelection,
|
||||
final String[] mmsSelectionArgs) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
Cursor localCursor = null;
|
||||
Cursor remoteSmsCursor = null;
|
||||
Cursor remoteMmsCursor = null;
|
||||
try {
|
||||
localCursor = db.query(
|
||||
DatabaseHelper.MESSAGES_TABLE,
|
||||
COUNT_PROJECTION,
|
||||
localSelection,
|
||||
localSelectionArgs,
|
||||
null/*groupBy*/,
|
||||
null/*having*/,
|
||||
null/*orderBy*/);
|
||||
final int localCount = getCountFromCursor(localCursor);
|
||||
remoteSmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Sms.CONTENT_URI,
|
||||
COUNT_PROJECTION,
|
||||
smsSelection,
|
||||
smsSelectionArgs,
|
||||
null/*orderBy*/);
|
||||
final int smsCount = getCountFromCursor(remoteSmsCursor);
|
||||
remoteMmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Mms.CONTENT_URI,
|
||||
COUNT_PROJECTION,
|
||||
mmsSelection,
|
||||
mmsSelectionArgs,
|
||||
null/*orderBy*/);
|
||||
final int mmsCount = getCountFromCursor(remoteMmsCursor);
|
||||
final int remoteCount = smsCount + mmsCount;
|
||||
final boolean isInSync = (localCount == remoteCount);
|
||||
if (isInSync) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncCursorPair: Same # of local and remote messages = "
|
||||
+ localCount);
|
||||
}
|
||||
} else {
|
||||
LogUtil.i(TAG, "SyncCursorPair: Not in sync; # local messages = " + localCount
|
||||
+ ", # remote message = " + remoteCount);
|
||||
}
|
||||
return isInSync;
|
||||
} catch (final Exception e) {
|
||||
LogUtil.e(TAG, "SyncCursorPair: failed to query local or remote message counts", e);
|
||||
// If something is wrong in querying database, assume we are synced so
|
||||
// we don't retry indefinitely
|
||||
} finally {
|
||||
if (localCursor != null) {
|
||||
localCursor.close();
|
||||
}
|
||||
if (remoteSmsCursor != null) {
|
||||
remoteSmsCursor.close();
|
||||
}
|
||||
if (remoteMmsCursor != null) {
|
||||
remoteMmsCursor.close();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.provider.Telephony;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.provider.Telephony.Sms;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.pdu.PduHeaders;
|
||||
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Update local database with a batch of messages to add/delete in one transaction
|
||||
*/
|
||||
class SyncMessageBatch {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
// Variables used during executeAction
|
||||
private final HashSet<String> mConversationsToUpdate;
|
||||
// Cache of thread->conversationId map
|
||||
private final ThreadInfoCache mCache;
|
||||
|
||||
// Set of SMS messages to add
|
||||
private final ArrayList<SmsMessage> mSmsToAdd;
|
||||
// Set of MMS messages to add
|
||||
private final ArrayList<MmsMessage> mMmsToAdd;
|
||||
// Set of local messages to delete
|
||||
private final ArrayList<LocalDatabaseMessage> mMessagesToDelete;
|
||||
|
||||
SyncMessageBatch(final ArrayList<SmsMessage> smsToAdd,
|
||||
final ArrayList<MmsMessage> mmsToAdd,
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete,
|
||||
final ThreadInfoCache cache) {
|
||||
mSmsToAdd = smsToAdd;
|
||||
mMmsToAdd = mmsToAdd;
|
||||
mMessagesToDelete = messagesToDelete;
|
||||
mCache = cache;
|
||||
mConversationsToUpdate = new HashSet<String>();
|
||||
}
|
||||
|
||||
void updateLocalDatabase() {
|
||||
// Perform local database changes in one transaction
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// Store all the SMS messages
|
||||
for (final SmsMessage sms : mSmsToAdd) {
|
||||
storeSms(db, sms);
|
||||
}
|
||||
// Store all the MMS messages
|
||||
for (final MmsMessage mms : mMmsToAdd) {
|
||||
storeMms(db, mms);
|
||||
}
|
||||
// Keep track of conversations with messages deleted
|
||||
for (final LocalDatabaseMessage message : mMessagesToDelete) {
|
||||
mConversationsToUpdate.add(message.getConversationId());
|
||||
}
|
||||
// Batch delete local messages
|
||||
batchDelete(db, DatabaseHelper.MESSAGES_TABLE, MessageColumns._ID,
|
||||
messageListToIds(mMessagesToDelete));
|
||||
|
||||
for (final LocalDatabaseMessage message : mMessagesToDelete) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Deleted message " + message.getLocalId()
|
||||
+ " for SMS/MMS " + message.getUri() + " with timestamp "
|
||||
+ message.getTimestampInMillis());
|
||||
}
|
||||
}
|
||||
|
||||
// Update conversation state for imported messages, like snippet,
|
||||
updateConversations(db);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] messageListToIds(final List<LocalDatabaseMessage> messagesToDelete) {
|
||||
final String[] ids = new String[messagesToDelete.size()];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
ids[i] = Long.toString(messagesToDelete.get(i).getLocalId());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the SMS message into local database.
|
||||
*
|
||||
* @param sms
|
||||
*/
|
||||
private void storeSms(final DatabaseWrapper db, final SmsMessage sms) {
|
||||
if (sms.mBody == null) {
|
||||
LogUtil.w(TAG, "SyncMessageBatch: SMS " + sms.mUri + " has no body; adding empty one");
|
||||
// try to fix it
|
||||
sms.mBody = "";
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(sms.mAddress)) {
|
||||
LogUtil.e(TAG, "SyncMessageBatch: SMS has no address; using unknown sender");
|
||||
// try to fix it
|
||||
sms.mAddress = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
|
||||
// TODO : We need to also deal with messages in a failed/retry state
|
||||
final boolean isOutgoing = sms.mType != Sms.MESSAGE_TYPE_INBOX;
|
||||
|
||||
final String otherPhoneNumber = sms.mAddress;
|
||||
|
||||
// A forced resync of all messages should still keep the archived states.
|
||||
// The database upgrade code notifies sync manager of this. We need to
|
||||
// honor the original customization to this conversation if created.
|
||||
final String conversationId = mCache.getOrCreateConversation(db, sms.mThreadId, sms.mSubId,
|
||||
DataModel.get().getSyncManager().getCustomizationForThread(sms.mThreadId));
|
||||
if (conversationId == null) {
|
||||
// Cannot create conversation for this message? This should not happen.
|
||||
LogUtil.e(TAG, "SyncMessageBatch: Failed to create conversation for SMS thread "
|
||||
+ sms.mThreadId);
|
||||
return;
|
||||
}
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(sms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
final ParticipantData sender = isOutgoing ?
|
||||
self :
|
||||
ParticipantData.getFromRawPhoneBySimLocale(otherPhoneNumber, sms.getSubId());
|
||||
final String participantId = (isOutgoing ? selfId :
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender));
|
||||
|
||||
final int bugleStatus = bugleStatusForSms(isOutgoing, sms.mType, sms.mStatus);
|
||||
|
||||
final MessageData message = MessageData.createSmsMessage(
|
||||
sms.mUri,
|
||||
participantId,
|
||||
selfId,
|
||||
conversationId,
|
||||
bugleStatus,
|
||||
sms.mSeen,
|
||||
sms.mRead,
|
||||
sms.mTimestampSentInMillis,
|
||||
sms.mTimestampInMillis,
|
||||
sms.mBody);
|
||||
|
||||
// Inserting sms content into messages table
|
||||
try {
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
rethrowSQLiteConstraintExceptionWithDetails(e, db, sms.mUri, sms.mThreadId,
|
||||
conversationId, selfId, participantId);
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Inserted new message " + message.getMessageId()
|
||||
+ " for SMS " + message.getSmsMessageUri() + " received at "
|
||||
+ message.getReceivedTimeStamp());
|
||||
}
|
||||
|
||||
// Keep track of updated conversation for later updating the conversation snippet, etc.
|
||||
mConversationsToUpdate.add(conversationId);
|
||||
}
|
||||
|
||||
public static int bugleStatusForSms(final boolean isOutgoing, final int type,
|
||||
final int status) {
|
||||
int bugleStatus = MessageData.BUGLE_STATUS_UNKNOWN;
|
||||
// For a message we sync either
|
||||
if (isOutgoing) {
|
||||
// Outgoing message not yet been sent
|
||||
if (type == Telephony.Sms.MESSAGE_TYPE_FAILED ||
|
||||
type == Telephony.Sms.MESSAGE_TYPE_OUTBOX ||
|
||||
type == Telephony.Sms.MESSAGE_TYPE_QUEUED ||
|
||||
(type == Telephony.Sms.MESSAGE_TYPE_SENT &&
|
||||
status == Telephony.Sms.STATUS_FAILED)) {
|
||||
// Not sent counts as failed and available for manual resend
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_FAILED;
|
||||
} else if (status == Sms.STATUS_COMPLETE) {
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_DELIVERED;
|
||||
} else {
|
||||
// Otherwise outgoing message is complete
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_COMPLETE;
|
||||
}
|
||||
} else {
|
||||
// All incoming SMS messages are complete
|
||||
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_COMPLETE;
|
||||
}
|
||||
return bugleStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the MMS message into local database
|
||||
*
|
||||
* @param mms
|
||||
*/
|
||||
private void storeMms(final DatabaseWrapper db, final MmsMessage mms) {
|
||||
if (mms.mParts.size() < 1) {
|
||||
LogUtil.w(TAG, "SyncMessageBatch: MMS " + mms.mUri + " has no parts");
|
||||
}
|
||||
|
||||
// TODO : We need to also deal with messages in a failed/retry state
|
||||
final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX;
|
||||
final boolean isNotification = (mms.mMmsMessageType ==
|
||||
PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
|
||||
|
||||
final String senderId = mms.mSender;
|
||||
|
||||
// A forced resync of all messages should still keep the archived states.
|
||||
// The database upgrade code notifies sync manager of this. We need to
|
||||
// honor the original customization to this conversation if created.
|
||||
final String conversationId = mCache.getOrCreateConversation(db, mms.mThreadId, mms.mSubId,
|
||||
DataModel.get().getSyncManager().getCustomizationForThread(mms.mThreadId));
|
||||
if (conversationId == null) {
|
||||
LogUtil.e(TAG, "SyncMessageBatch: Failed to create conversation for MMS thread "
|
||||
+ mms.mThreadId);
|
||||
return;
|
||||
}
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(mms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
final ParticipantData sender = isOutgoing ?
|
||||
self : ParticipantData.getFromRawPhoneBySimLocale(senderId, mms.getSubId());
|
||||
final String participantId = (isOutgoing ? selfId :
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender));
|
||||
|
||||
final int bugleStatus = MmsUtils.bugleStatusForMms(isOutgoing, isNotification, mms.mType);
|
||||
|
||||
// Import message and all of the parts.
|
||||
// TODO : For now we are importing these in the order we found them in the MMS
|
||||
// database. Ideally we would load and parse the SMIL which describes how the parts relate
|
||||
// to one another.
|
||||
|
||||
// TODO: Need to set correct status on message
|
||||
final MessageData message = MmsUtils.createMmsMessage(mms, conversationId, participantId,
|
||||
selfId, bugleStatus);
|
||||
|
||||
// Inserting mms content into messages table
|
||||
try {
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
rethrowSQLiteConstraintExceptionWithDetails(e, db, mms.mUri, mms.mThreadId,
|
||||
conversationId, selfId, participantId);
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Inserted new message " + message.getMessageId()
|
||||
+ " for MMS " + message.getSmsMessageUri() + " received at "
|
||||
+ message.getReceivedTimeStamp());
|
||||
}
|
||||
|
||||
// Keep track of updated conversation for later updating the conversation snippet, etc.
|
||||
mConversationsToUpdate.add(conversationId);
|
||||
}
|
||||
|
||||
// TODO: Remove this after we no longer see this crash (b/18375758)
|
||||
private static void rethrowSQLiteConstraintExceptionWithDetails(SQLiteConstraintException e,
|
||||
DatabaseWrapper db, String messageUri, long threadId, String conversationId,
|
||||
String selfId, String senderId) {
|
||||
// Add some extra debug information to the exception for tracking down b/18375758.
|
||||
// The default detail message for SQLiteConstraintException tells us that a foreign
|
||||
// key constraint failed, but not which one! Messages have foreign keys to 3 tables:
|
||||
// conversations, participants (self), participants (sender). We'll query each one
|
||||
// to determine which one(s) violated the constraint, and then throw a new exception
|
||||
// with those details.
|
||||
|
||||
String foundConversationId = null;
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
// Look for an existing conversation in the db with the conversation id
|
||||
cursor = db.rawQuery("SELECT " + ConversationColumns._ID
|
||||
+ " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
|
||||
+ " WHERE " + ConversationColumns._ID + "=" + conversationId,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
Assert.isTrue(cursor.getCount() == 1);
|
||||
foundConversationId = cursor.getString(0);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
ParticipantData foundSelfParticipant =
|
||||
BugleDatabaseOperations.getExistingParticipant(db, selfId);
|
||||
ParticipantData foundSenderParticipant =
|
||||
BugleDatabaseOperations.getExistingParticipant(db, senderId);
|
||||
|
||||
String errorMsg = "SQLiteConstraintException while inserting message for " + messageUri
|
||||
+ "; conversation id from getOrCreateConversation = " + conversationId
|
||||
+ " (lookup thread = " + threadId + "), found conversation id = "
|
||||
+ foundConversationId + ", found self participant = "
|
||||
+ LogUtil.sanitizePII(foundSelfParticipant.getNormalizedDestination())
|
||||
+ " (lookup id = " + selfId + "), found sender participant = "
|
||||
+ LogUtil.sanitizePII(foundSenderParticipant.getNormalizedDestination())
|
||||
+ " (lookup id = " + senderId + ")";
|
||||
throw new RuntimeException(errorMsg, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the tracked latest message info to update conversations, including
|
||||
* latest chat message and sort timestamp.
|
||||
*/
|
||||
private void updateConversations(final DatabaseWrapper db) {
|
||||
for (final String conversationId : mConversationsToUpdate) {
|
||||
if (BugleDatabaseOperations.deleteConversationIfEmptyInTransaction(db,
|
||||
conversationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final boolean archived = mCache.isArchived(conversationId);
|
||||
// Always attempt to auto-switch conversation self id for sync/import case.
|
||||
BugleDatabaseOperations.maybeRefreshConversationMetadataInTransaction(db,
|
||||
conversationId, true /*shouldAutoSwitchSelfId*/, archived /*keepArchived*/);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Batch delete database rows by matching a column with a list of values, usually some
|
||||
* kind of IDs.
|
||||
*
|
||||
* @param table
|
||||
* @param column
|
||||
* @param ids
|
||||
* @return Total number of deleted messages
|
||||
*/
|
||||
private static int batchDelete(final DatabaseWrapper db, final String table,
|
||||
final String column, final String[] ids) {
|
||||
int totalDeleted = 0;
|
||||
final int totalIds = ids.length;
|
||||
for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) {
|
||||
final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding
|
||||
final int count = end - start;
|
||||
final String batchSelection = String.format(
|
||||
Locale.US,
|
||||
"%s IN %s",
|
||||
column,
|
||||
MmsUtils.getSqlInOperand(count));
|
||||
final String[] batchSelectionArgs = Arrays.copyOfRange(ids, start, end);
|
||||
final int deleted = db.delete(
|
||||
table,
|
||||
batchSelection,
|
||||
batchSelectionArgs);
|
||||
totalDeleted += deleted;
|
||||
}
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.support.v4.util.LongSparseArray;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.SqliteWrapper;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.BugleGservices;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.ContentType;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Action used to sync messages from smsmms db to local database
|
||||
*/
|
||||
public class SyncMessagesAction extends Action implements Parcelable {
|
||||
static final long SYNC_FAILED = Long.MIN_VALUE;
|
||||
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_START_TIMESTAMP = "start_timestamp";
|
||||
private static final String KEY_MAX_UPDATE = "max_update";
|
||||
private static final String KEY_LOWER_BOUND = "lower_bound";
|
||||
private static final String KEY_UPPER_BOUND = "upper_bound";
|
||||
private static final String BUNDLE_KEY_LAST_TIMESTAMP = "last_timestamp";
|
||||
private static final String BUNDLE_KEY_SMS_MESSAGES = "sms_to_add";
|
||||
private static final String BUNDLE_KEY_MMS_MESSAGES = "mms_to_add";
|
||||
private static final String BUNDLE_KEY_MESSAGES_TO_DELETE = "messages_to_delete";
|
||||
|
||||
/**
|
||||
* Start a full sync (backed off a few seconds to avoid pulling sending/receiving messages).
|
||||
*/
|
||||
public static void fullSync() {
|
||||
final BugleGservices bugleGservices = BugleGservices.get();
|
||||
final long smsSyncBackoffTimeMillis = bugleGservices.getLong(
|
||||
BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS,
|
||||
BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
// TODO: Could base this off most recent message in db but now should be okay...
|
||||
final long startTimestamp = now - smsSyncBackoffTimeMillis;
|
||||
|
||||
final SyncMessagesAction action = new SyncMessagesAction(-1L, startTimestamp,
|
||||
0, startTimestamp);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an incremental sync to pull messages since last sync (backed off a few seconds)..
|
||||
*/
|
||||
public static void sync() {
|
||||
final BugleGservices bugleGservices = BugleGservices.get();
|
||||
final long smsSyncBackoffTimeMillis = bugleGservices.getLong(
|
||||
BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS,
|
||||
BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
// TODO: Could base this off most recent message in db but now should be okay...
|
||||
final long startTimestamp = now - smsSyncBackoffTimeMillis;
|
||||
|
||||
sync(startTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an incremental sync when the application starts up (no back off as not yet
|
||||
* sending/receiving).
|
||||
*/
|
||||
public static void immediateSync() {
|
||||
final long now = System.currentTimeMillis();
|
||||
// TODO: Could base this off most recent message in db but now should be okay...
|
||||
final long startTimestamp = now;
|
||||
|
||||
sync(startTimestamp);
|
||||
}
|
||||
|
||||
private static void sync(final long startTimestamp) {
|
||||
if (!OsUtil.hasSmsPermission()) {
|
||||
// Sync requires READ_SMS permission
|
||||
return;
|
||||
}
|
||||
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
// Lower bound is end of previous sync
|
||||
final long syncLowerBoundTimeMillis = prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME,
|
||||
BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT);
|
||||
|
||||
final SyncMessagesAction action = new SyncMessagesAction(syncLowerBoundTimeMillis,
|
||||
startTimestamp, 0, startTimestamp);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private SyncMessagesAction(final long lowerBound, final long upperBound,
|
||||
final int maxMessagesToUpdate, final long startTimestamp) {
|
||||
actionParameters.putLong(KEY_LOWER_BOUND, lowerBound);
|
||||
actionParameters.putLong(KEY_UPPER_BOUND, upperBound);
|
||||
actionParameters.putInt(KEY_MAX_UPDATE, maxMessagesToUpdate);
|
||||
actionParameters.putLong(KEY_START_TIMESTAMP, startTimestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
|
||||
final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
|
||||
final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
|
||||
final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Request to sync messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis + " (start timestamp = "
|
||||
+ startTimestamp + ", message update limit = " + initialMaxMessagesToUpdate
|
||||
+ ")");
|
||||
}
|
||||
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
if (lowerBoundTimeMillis >= 0) {
|
||||
// Cursors
|
||||
final SyncCursorPair cursors = new SyncCursorPair(-1L, lowerBoundTimeMillis);
|
||||
final boolean inSync = cursors.isSynchronized(db);
|
||||
if (!inSync) {
|
||||
if (syncManager.delayUntilFullSync(startTimestamp) == 0) {
|
||||
lowerBoundTimeMillis = -1;
|
||||
actionParameters.putLong(KEY_LOWER_BOUND, lowerBoundTimeMillis);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Messages before "
|
||||
+ lowerBoundTimeMillis + " not in sync; promoting to full sync");
|
||||
}
|
||||
} else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Messages before "
|
||||
+ lowerBoundTimeMillis + " not in sync; will do incremental sync");
|
||||
}
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Messages before " + lowerBoundTimeMillis
|
||||
+ " are in sync");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if sync allowed (can be too soon after last or one is already running)
|
||||
if (syncManager.shouldSync(lowerBoundTimeMillis < 0, startTimestamp)) {
|
||||
syncManager.startSyncBatch(upperBoundTimeMillis);
|
||||
requestBackgroundWork();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final BugleGservices bugleGservices = BugleGservices.get();
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final int maxMessagesToScan = bugleGservices.getInt(
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN,
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN_DEFAULT);
|
||||
|
||||
final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
|
||||
final int smsSyncSubsequentBatchSizeMin = bugleGservices.getInt(
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN,
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN_DEFAULT);
|
||||
final int smsSyncSubsequentBatchSizeMax = bugleGservices.getInt(
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX,
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX_DEFAULT);
|
||||
|
||||
// Cap sync size to GServices limits
|
||||
final int maxMessagesToUpdate = Math.max(smsSyncSubsequentBatchSizeMin,
|
||||
Math.min(initialMaxMessagesToUpdate, smsSyncSubsequentBatchSizeMax));
|
||||
|
||||
final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
|
||||
final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
|
||||
|
||||
LogUtil.i(TAG, "SyncMessagesAction: Starting batch for messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis
|
||||
+ " (message update limit = " + maxMessagesToUpdate + ", message scan limit = "
|
||||
+ maxMessagesToScan + ")");
|
||||
|
||||
// Clear last change time so that we can work out if this batch is dirty when it completes
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
|
||||
// Clear the singleton cache that maps threads to recipients and to conversations.
|
||||
final SyncManager.ThreadInfoCache cache = syncManager.getThreadInfoCache();
|
||||
cache.clear();
|
||||
|
||||
// Sms messages to store
|
||||
final ArrayList<SmsMessage> smsToAdd = new ArrayList<SmsMessage>();
|
||||
// Mms messages to store
|
||||
final LongSparseArray<MmsMessage> mmsToAdd = new LongSparseArray<MmsMessage>();
|
||||
// List of local SMS/MMS to remove
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete =
|
||||
new ArrayList<LocalDatabaseMessage>();
|
||||
|
||||
long lastTimestampMillis = SYNC_FAILED;
|
||||
if (syncManager.isSyncing(upperBoundTimeMillis)) {
|
||||
// Cursors
|
||||
final SyncCursorPair cursors = new SyncCursorPair(lowerBoundTimeMillis,
|
||||
upperBoundTimeMillis);
|
||||
|
||||
// Actually compare the messages using cursor pair
|
||||
lastTimestampMillis = syncCursorPair(db, cursors, smsToAdd, mmsToAdd,
|
||||
messagesToDelete, maxMessagesToScan, maxMessagesToUpdate, cache);
|
||||
}
|
||||
final Bundle response = new Bundle();
|
||||
|
||||
// If comparison succeeds bundle up the changes for processing in ActionService
|
||||
if (lastTimestampMillis > SYNC_FAILED) {
|
||||
final ArrayList<MmsMessage> mmsToAddList = new ArrayList<MmsMessage>();
|
||||
for (int i = 0; i < mmsToAdd.size(); i++) {
|
||||
final MmsMessage mms = mmsToAdd.valueAt(i);
|
||||
mmsToAddList.add(mms);
|
||||
}
|
||||
|
||||
response.putParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES, smsToAdd);
|
||||
response.putParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES, mmsToAddList);
|
||||
response.putParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE, messagesToDelete);
|
||||
}
|
||||
response.putLong(BUNDLE_KEY_LAST_TIMESTAMP, lastTimestampMillis);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare messages based on timestamp and uri
|
||||
* @param db local database wrapper
|
||||
* @param cursors cursor pair holding references to local and remote messages
|
||||
* @param smsToAdd newly found sms messages to add
|
||||
* @param mmsToAdd newly found mms messages to add
|
||||
* @param messagesToDelete messages not found needing deletion
|
||||
* @param maxMessagesToScan max messages to scan for changes
|
||||
* @param maxMessagesToUpdate max messages to return for updates
|
||||
* @param cache cache for conversation id / thread id / recipient set mapping
|
||||
* @return timestamp of the oldest message seen during the sync scan
|
||||
*/
|
||||
private long syncCursorPair(final DatabaseWrapper db, final SyncCursorPair cursors,
|
||||
final ArrayList<SmsMessage> smsToAdd, final LongSparseArray<MmsMessage> mmsToAdd,
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete, final int maxMessagesToScan,
|
||||
final int maxMessagesToUpdate, final ThreadInfoCache cache) {
|
||||
long lastTimestampMillis;
|
||||
final long startTimeMillis = SystemClock.elapsedRealtime();
|
||||
|
||||
// Number of messages scanned local and remote
|
||||
int localPos = 0;
|
||||
int remotePos = 0;
|
||||
int localTotal = 0;
|
||||
int remoteTotal = 0;
|
||||
// Scan through the messages on both sides and prepare messages for local message table
|
||||
// changes (including adding and deleting)
|
||||
try {
|
||||
cursors.query(db);
|
||||
|
||||
localTotal = cursors.getLocalCount();
|
||||
remoteTotal = cursors.getRemoteCount();
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Scanning cursors (local count = " + localTotal
|
||||
+ ", remote count = " + remoteTotal + ", message update limit = "
|
||||
+ maxMessagesToUpdate + ", message scan limit = " + maxMessagesToScan
|
||||
+ ")");
|
||||
}
|
||||
|
||||
lastTimestampMillis = cursors.scan(maxMessagesToScan, maxMessagesToUpdate,
|
||||
smsToAdd, mmsToAdd, messagesToDelete, cache);
|
||||
|
||||
localPos = cursors.getLocalPosition();
|
||||
remotePos = cursors.getRemotePosition();
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Scanned cursors (local position = " + localPos
|
||||
+ " of " + localTotal + ", remote position = " + remotePos + " of "
|
||||
+ remoteTotal + ")");
|
||||
}
|
||||
|
||||
// Batch loading the parts of the MMS messages in this batch
|
||||
loadMmsParts(mmsToAdd);
|
||||
// Lookup senders for incoming mms messages
|
||||
setMmsSenders(mmsToAdd, cache);
|
||||
} catch (final SQLiteException e) {
|
||||
LogUtil.e(TAG, "SyncMessagesAction: Database exception", e);
|
||||
// Let's abort
|
||||
lastTimestampMillis = SYNC_FAILED;
|
||||
} catch (final Exception e) {
|
||||
// We want to catch anything unexpected since this is running in a separate thread
|
||||
// and any unexpected exception will just fail this thread silently.
|
||||
// Let's crash for dogfooders!
|
||||
LogUtil.wtf(TAG, "SyncMessagesAction: unexpected failure in scan", e);
|
||||
lastTimestampMillis = SYNC_FAILED;
|
||||
} finally {
|
||||
if (cursors != null) {
|
||||
cursors.close();
|
||||
}
|
||||
}
|
||||
|
||||
final long endTimeMillis = SystemClock.elapsedRealtime();
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Scan complete (took "
|
||||
+ (endTimeMillis - startTimeMillis) + " ms). " + smsToAdd.size()
|
||||
+ " remote SMS to add, " + mmsToAdd.size() + " MMS to add, "
|
||||
+ messagesToDelete.size() + " local messages to delete. "
|
||||
+ "Oldest timestamp seen = " + lastTimestampMillis);
|
||||
}
|
||||
|
||||
return lastTimestampMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform local database updates and schedule follow on sync actions
|
||||
*/
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
final long lastTimestampMillis = response.getLong(BUNDLE_KEY_LAST_TIMESTAMP);
|
||||
final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
|
||||
final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
|
||||
final int maxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
|
||||
final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);
|
||||
|
||||
// Check with the sync manager if any conflicting updates have been made to databases
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
final boolean orphan = !syncManager.isSyncing(upperBoundTimeMillis);
|
||||
|
||||
// lastTimestampMillis used to indicate failure
|
||||
if (orphan) {
|
||||
// This batch does not match current in progress timestamp.
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Ignoring orphan sync batch for messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis);
|
||||
} else {
|
||||
final boolean dirty = syncManager.isBatchDirty(lastTimestampMillis);
|
||||
if (lastTimestampMillis == SYNC_FAILED) {
|
||||
LogUtil.e(TAG, "SyncMessagesAction: Sync failed - terminating");
|
||||
|
||||
// Failed - update last sync times to throttle our failure rate
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
// Save sync completion time so next sync will start from here
|
||||
prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
|
||||
// Remember last full sync so that don't start background full sync right away
|
||||
prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);
|
||||
|
||||
syncManager.complete();
|
||||
} else if (dirty) {
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Redoing dirty sync batch of messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis);
|
||||
|
||||
// Redo this batch
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(lowerBoundTimeMillis, upperBoundTimeMillis,
|
||||
maxMessagesToUpdate, startTimestamp);
|
||||
|
||||
syncManager.startSyncBatch(upperBoundTimeMillis);
|
||||
requestBackgroundWork(nextBatch);
|
||||
} else {
|
||||
// Succeeded
|
||||
final ArrayList<SmsMessage> smsToAdd =
|
||||
response.getParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES);
|
||||
final ArrayList<MmsMessage> mmsToAdd =
|
||||
response.getParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES);
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete =
|
||||
response.getParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE);
|
||||
|
||||
final int messagesUpdated = smsToAdd.size() + mmsToAdd.size()
|
||||
+ messagesToDelete.size();
|
||||
|
||||
// Perform local database changes in one transaction
|
||||
long txnTimeMillis = 0;
|
||||
if (messagesUpdated > 0) {
|
||||
final long startTimeMillis = SystemClock.elapsedRealtime();
|
||||
final SyncMessageBatch batch = new SyncMessageBatch(smsToAdd, mmsToAdd,
|
||||
messagesToDelete, syncManager.getThreadInfoCache());
|
||||
batch.updateLocalDatabase();
|
||||
final long endTimeMillis = SystemClock.elapsedRealtime();
|
||||
txnTimeMillis = endTimeMillis - startTimeMillis;
|
||||
|
||||
LogUtil.i(TAG, "SyncMessagesAction: Updated local database "
|
||||
+ "(took " + txnTimeMillis + " ms). Added "
|
||||
+ smsToAdd.size() + " SMS, added " + mmsToAdd.size() + " MMS, deleted "
|
||||
+ messagesToDelete.size() + " messages.");
|
||||
|
||||
// TODO: Investigate whether we can make this more fine-grained.
|
||||
MessagingContentProvider.notifyEverythingChanged();
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: No local database updates to make");
|
||||
}
|
||||
|
||||
if (!syncManager.getHasFirstSyncCompleted()) {
|
||||
// If we have never completed a sync before (fresh install) and there are
|
||||
// no messages, still inform the UI of a change so it can update syncing
|
||||
// messages shown to the user
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
}
|
||||
}
|
||||
// Determine if there are more messages that need to be scanned
|
||||
if (lastTimestampMillis >= 0 && lastTimestampMillis >= lowerBoundTimeMillis) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: More messages to sync; scheduling next "
|
||||
+ "sync batch now.");
|
||||
}
|
||||
|
||||
// Include final millisecond of last sync in next sync
|
||||
final long newUpperBoundTimeMillis = lastTimestampMillis + 1;
|
||||
final int newMaxMessagesToUpdate = nextBatchSize(messagesUpdated,
|
||||
txnTimeMillis);
|
||||
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(lowerBoundTimeMillis, newUpperBoundTimeMillis,
|
||||
newMaxMessagesToUpdate, startTimestamp);
|
||||
|
||||
// Proceed with next batch
|
||||
syncManager.startSyncBatch(newUpperBoundTimeMillis);
|
||||
requestBackgroundWork(nextBatch);
|
||||
} else {
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
// Save sync completion time so next sync will start from here
|
||||
prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
|
||||
if (lowerBoundTimeMillis < 0) {
|
||||
// Remember last full sync so that don't start another full sync right away
|
||||
prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);
|
||||
}
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
// After any sync check if new messages have arrived
|
||||
final SyncCursorPair recents = new SyncCursorPair(startTimestamp, now);
|
||||
final SyncCursorPair olders = new SyncCursorPair(-1L, startTimestamp);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
if (!recents.isSynchronized(db)) {
|
||||
LogUtil.i(TAG, "SyncMessagesAction: Changed messages after sync; "
|
||||
+ "scheduling an incremental sync now.");
|
||||
|
||||
// Just add a new batch for recent messages
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(startTimestamp, now, 0, startTimestamp);
|
||||
syncManager.startSyncBatch(now);
|
||||
requestBackgroundWork(nextBatch);
|
||||
// After partial sync verify sync state
|
||||
} else if (lowerBoundTimeMillis >= 0 && !olders.isSynchronized(db)) {
|
||||
// Add a batch going back to start of time
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Changed messages before sync batch; "
|
||||
+ "scheduling a full sync now.");
|
||||
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(-1L, startTimestamp, 0, startTimestamp);
|
||||
|
||||
syncManager.startSyncBatch(startTimestamp);
|
||||
requestBackgroundWork(nextBatch);
|
||||
} else {
|
||||
LogUtil.i(TAG, "SyncMessagesAction: All messages now in sync");
|
||||
|
||||
// All done, in sync
|
||||
syncManager.complete();
|
||||
}
|
||||
}
|
||||
// Either sync should be complete or we should have a follow up request
|
||||
Assert.isTrue(hasBackgroundActions() || !syncManager.isSyncing());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide the next batch size based on the stats we collected with past batch
|
||||
* @param messagesUpdated number of messages updated in this batch
|
||||
* @param txnTimeMillis time the transaction took in ms
|
||||
* @return Target number of messages to sync for next batch
|
||||
*/
|
||||
private static int nextBatchSize(final int messagesUpdated, final long txnTimeMillis) {
|
||||
final BugleGservices bugleGservices = BugleGservices.get();
|
||||
final long smsSyncSubsequentBatchTimeLimitMillis = bugleGservices.getLong(
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS,
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS_DEFAULT);
|
||||
|
||||
if (txnTimeMillis <= 0) {
|
||||
return 0;
|
||||
}
|
||||
// Number of messages we can sync within the batch time limit using
|
||||
// the average sync time calculated based on the stats we collected
|
||||
// in previous batch
|
||||
return (int) ((double) (messagesUpdated) / (double) txnTimeMillis
|
||||
* smsSyncSubsequentBatchTimeLimitMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch loading MMS parts for the messages in current batch
|
||||
*/
|
||||
private void loadMmsParts(final LongSparseArray<MmsMessage> mmses) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int totalIds = mmses.size();
|
||||
for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) {
|
||||
final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding
|
||||
final int count = end - start;
|
||||
final String batchSelection = String.format(
|
||||
Locale.US,
|
||||
"%s != '%s' AND %s IN %s",
|
||||
Mms.Part.CONTENT_TYPE,
|
||||
ContentType.APP_SMIL,
|
||||
Mms.Part.MSG_ID,
|
||||
MmsUtils.getSqlInOperand(count));
|
||||
final String[] batchSelectionArgs = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
batchSelectionArgs[i] = Long.toString(mmses.valueAt(start + i).getId());
|
||||
}
|
||||
final Cursor cursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
MmsUtils.MMS_PART_CONTENT_URI,
|
||||
DatabaseMessages.MmsPart.PROJECTION,
|
||||
batchSelection,
|
||||
batchSelectionArgs,
|
||||
null/*sortOrder*/);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
// Delay loading the media content for parsing for efficiency
|
||||
// TODO: load the media and fill in the dimensions when
|
||||
// we actually display it
|
||||
final DatabaseMessages.MmsPart part =
|
||||
DatabaseMessages.MmsPart.get(cursor, false/*loadMedia*/);
|
||||
final DatabaseMessages.MmsMessage mms = mmses.get(part.mMessageId);
|
||||
if (mms != null) {
|
||||
mms.addPart(part);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch loading MMS sender for the messages in current batch
|
||||
*/
|
||||
private void setMmsSenders(final LongSparseArray<MmsMessage> mmses,
|
||||
final ThreadInfoCache cache) {
|
||||
// Store all the MMS messages
|
||||
for (int i = 0; i < mmses.size(); i++) {
|
||||
final MmsMessage mms = mmses.valueAt(i);
|
||||
|
||||
final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX;
|
||||
String senderId = null;
|
||||
if (!isOutgoing) {
|
||||
// We only need to find out sender phone number for received message
|
||||
senderId = getMmsSender(mms, cache);
|
||||
if (senderId == null) {
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Could not find sender of incoming MMS "
|
||||
+ "message " + mms.getUri() + "; using 'unknown sender' instead");
|
||||
senderId = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
}
|
||||
mms.setSender(senderId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out the sender of an MMS message
|
||||
*/
|
||||
private String getMmsSender(final MmsMessage mms, final ThreadInfoCache cache) {
|
||||
final List<String> recipients = cache.getThreadRecipients(mms.mThreadId);
|
||||
Assert.notNull(recipients);
|
||||
Assert.isTrue(recipients.size() > 0);
|
||||
|
||||
if (recipients.size() == 1
|
||||
&& recipients.get(0).equals(ParticipantData.getUnknownSenderDestination())) {
|
||||
LogUtil.w(TAG, "SyncMessagesAction: MMS message " + mms.mUri + " has unknown sender "
|
||||
+ "(thread id = " + mms.mThreadId + ")");
|
||||
}
|
||||
|
||||
return MmsUtils.getMmsSender(recipients, mms.mUri);
|
||||
}
|
||||
|
||||
private SyncMessagesAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<SyncMessagesAction> CREATOR
|
||||
= new Parcelable.Creator<SyncMessagesAction>() {
|
||||
@Override
|
||||
public SyncMessagesAction createFromParcel(final Parcel in) {
|
||||
return new SyncMessagesAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SyncMessagesAction[] newArray(final int size) {
|
||||
return new SyncMessagesAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class UpdateConversationArchiveStatusAction extends Action {
|
||||
|
||||
public static void archiveConversation(final String conversationId) {
|
||||
final UpdateConversationArchiveStatusAction action =
|
||||
new UpdateConversationArchiveStatusAction(conversationId, true /* isArchive */);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void unarchiveConversation(final String conversationId) {
|
||||
final UpdateConversationArchiveStatusAction action =
|
||||
new UpdateConversationArchiveStatusAction(conversationId, false /* isArchive */);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_IS_ARCHIVE = "is_archive";
|
||||
|
||||
protected UpdateConversationArchiveStatusAction(
|
||||
final String conversationId, final boolean isArchive) {
|
||||
Assert.isTrue(!TextUtils.isEmpty(conversationId));
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putBoolean(KEY_IS_ARCHIVE, isArchive);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final boolean isArchived = actionParameters.getBoolean(KEY_IS_ARCHIVE);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
BugleDatabaseOperations.updateConversationArchiveStatusInTransaction(
|
||||
db, conversationId, isArchived);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected UpdateConversationArchiveStatusAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateConversationArchiveStatusAction> CREATOR
|
||||
= new Parcelable.Creator<UpdateConversationArchiveStatusAction>() {
|
||||
@Override
|
||||
public UpdateConversationArchiveStatusAction createFromParcel(final Parcel in) {
|
||||
return new UpdateConversationArchiveStatusAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateConversationArchiveStatusAction[] newArray(final int size) {
|
||||
return new UpdateConversationArchiveStatusAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Action used to update conversation options such as notification settings.
|
||||
*/
|
||||
public class UpdateConversationOptionsAction extends Action
|
||||
implements Parcelable {
|
||||
/**
|
||||
* Enable/disable conversation notifications.
|
||||
*/
|
||||
public static void enableConversationNotifications(final String conversationId,
|
||||
final boolean enableNotification) {
|
||||
Assert.notNull(conversationId);
|
||||
|
||||
final UpdateConversationOptionsAction action = new UpdateConversationOptionsAction(
|
||||
conversationId, enableNotification, null, null);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets conversation notification sound.
|
||||
*/
|
||||
public static void setConversationNotificationSound(final String conversationId,
|
||||
final String ringtoneUri) {
|
||||
Assert.notNull(conversationId);
|
||||
|
||||
final UpdateConversationOptionsAction action = new UpdateConversationOptionsAction(
|
||||
conversationId, null, ringtoneUri, null);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable vibrations for conversation notification.
|
||||
*/
|
||||
public static void enableVibrationForConversationNotification(final String conversationId,
|
||||
final boolean enableVibration) {
|
||||
Assert.notNull(conversationId);
|
||||
|
||||
final UpdateConversationOptionsAction action = new UpdateConversationOptionsAction(
|
||||
conversationId, null, null, enableVibration);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
|
||||
// Keys for all settable settings.
|
||||
private static final String KEY_ENABLE_NOTIFICATION = "enable_notification";
|
||||
private static final String KEY_RINGTONE_URI = "ringtone_uri";
|
||||
private static final String KEY_ENABLE_VIBRATION = "enable_vibration";
|
||||
|
||||
protected UpdateConversationOptionsAction(final String conversationId,
|
||||
final Boolean enableNotification, final String ringtoneUri,
|
||||
final Boolean enableVibration) {
|
||||
Assert.notNull(conversationId);
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
if (enableNotification != null) {
|
||||
actionParameters.putBoolean(KEY_ENABLE_NOTIFICATION, enableNotification);
|
||||
}
|
||||
|
||||
if (ringtoneUri != null) {
|
||||
actionParameters.putString(KEY_RINGTONE_URI, ringtoneUri);
|
||||
}
|
||||
|
||||
if (enableVibration != null) {
|
||||
actionParameters.putBoolean(KEY_ENABLE_VIBRATION, enableVibration);
|
||||
}
|
||||
}
|
||||
|
||||
protected void putOptionValuesInTransaction(final ContentValues values,
|
||||
final DatabaseWrapper dbWrapper) {
|
||||
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
||||
if (actionParameters.containsKey(KEY_ENABLE_NOTIFICATION)) {
|
||||
values.put(ConversationColumns.NOTIFICATION_ENABLED,
|
||||
actionParameters.getBoolean(KEY_ENABLE_NOTIFICATION));
|
||||
}
|
||||
|
||||
if (actionParameters.containsKey(KEY_RINGTONE_URI)) {
|
||||
values.put(ConversationColumns.NOTIFICATION_SOUND_URI,
|
||||
actionParameters.getString(KEY_RINGTONE_URI));
|
||||
}
|
||||
|
||||
if (actionParameters.containsKey(KEY_ENABLE_VIBRATION)) {
|
||||
values.put(ConversationColumns.NOTIFICATION_VIBRATION,
|
||||
actionParameters.getBoolean(KEY_ENABLE_VIBRATION));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
putOptionValuesInTransaction(values, db);
|
||||
|
||||
BugleDatabaseOperations.updateConversationRowIfExists(db, conversationId, values);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected UpdateConversationOptionsAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateConversationOptionsAction> CREATOR
|
||||
= new Parcelable.Creator<UpdateConversationOptionsAction>() {
|
||||
@Override
|
||||
public UpdateConversationOptionsAction createFromParcel(final Parcel in) {
|
||||
return new UpdateConversationOptionsAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateConversationOptionsAction[] newArray(final int size) {
|
||||
return new UpdateConversationOptionsAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class UpdateDestinationBlockedAction extends Action {
|
||||
public interface UpdateDestinationBlockedActionListener {
|
||||
@Assert.RunsOnMainThread
|
||||
abstract void onUpdateDestinationBlockedAction(final UpdateDestinationBlockedAction action,
|
||||
final boolean success,
|
||||
final boolean block,
|
||||
final String destination);
|
||||
}
|
||||
|
||||
public static class UpdateDestinationBlockedActionMonitor extends ActionMonitor
|
||||
implements ActionMonitor.ActionCompletedListener {
|
||||
private final UpdateDestinationBlockedActionListener mListener;
|
||||
|
||||
public UpdateDestinationBlockedActionMonitor(
|
||||
Object data, UpdateDestinationBlockedActionListener mListener) {
|
||||
super(STATE_CREATED, generateUniqueActionKey("UpdateDestinationBlockedAction"), data);
|
||||
setCompletedListener(this);
|
||||
this.mListener = mListener;
|
||||
}
|
||||
|
||||
private void onActionDone(final boolean succeeded,
|
||||
final ActionMonitor monitor,
|
||||
final Action action,
|
||||
final Object data,
|
||||
final Object result) {
|
||||
mListener.onUpdateDestinationBlockedAction(
|
||||
(UpdateDestinationBlockedAction) action,
|
||||
succeeded,
|
||||
action.actionParameters.getBoolean(KEY_BLOCKED),
|
||||
action.actionParameters.getString(KEY_DESTINATION));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionSucceeded(final ActionMonitor monitor,
|
||||
final Action action,
|
||||
final Object data,
|
||||
final Object result) {
|
||||
onActionDone(true, monitor, action, data, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionFailed(final ActionMonitor monitor,
|
||||
final Action action,
|
||||
final Object data,
|
||||
final Object result) {
|
||||
onActionDone(false, monitor, action, data, result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static UpdateDestinationBlockedActionMonitor updateDestinationBlocked(
|
||||
final String destination, final boolean blocked, final String conversationId,
|
||||
final UpdateDestinationBlockedActionListener listener) {
|
||||
Assert.notNull(listener);
|
||||
final UpdateDestinationBlockedActionMonitor monitor =
|
||||
new UpdateDestinationBlockedActionMonitor(null, listener);
|
||||
final UpdateDestinationBlockedAction action =
|
||||
new UpdateDestinationBlockedAction(destination, blocked, conversationId,
|
||||
monitor.getActionKey());
|
||||
action.start(monitor);
|
||||
return monitor;
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_DESTINATION = "destination";
|
||||
private static final String KEY_BLOCKED = "blocked";
|
||||
|
||||
protected UpdateDestinationBlockedAction(
|
||||
final String destination, final boolean blocked, final String conversationId,
|
||||
final String actionKey) {
|
||||
super(actionKey);
|
||||
Assert.isTrue(!TextUtils.isEmpty(destination));
|
||||
actionParameters.putString(KEY_DESTINATION, destination);
|
||||
actionParameters.putBoolean(KEY_BLOCKED, blocked);
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String destination = actionParameters.getString(KEY_DESTINATION);
|
||||
final boolean isBlocked = actionParameters.getBoolean(KEY_BLOCKED);
|
||||
String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
BugleDatabaseOperations.updateDestination(db, destination, isBlocked);
|
||||
if (conversationId == null) {
|
||||
conversationId = BugleDatabaseOperations
|
||||
.getConversationFromOtherParticipantDestination(db, destination);
|
||||
}
|
||||
if (conversationId != null) {
|
||||
if (isBlocked) {
|
||||
UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
|
||||
} else {
|
||||
UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
|
||||
}
|
||||
MessagingContentProvider.notifyParticipantsChanged(conversationId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected UpdateDestinationBlockedAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateDestinationBlockedAction> CREATOR
|
||||
= new Parcelable.Creator<UpdateDestinationBlockedAction>() {
|
||||
@Override
|
||||
public UpdateDestinationBlockedAction createFromParcel(final Parcel in) {
|
||||
return new UpdateDestinationBlockedAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateDestinationBlockedAction[] newArray(final int size) {
|
||||
return new UpdateDestinationBlockedAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
|
||||
/**
|
||||
* Updates the message notification (generally, to include voice replies we've
|
||||
* made since the notification was first posted).
|
||||
*/
|
||||
public class UpdateMessageNotificationAction extends Action {
|
||||
|
||||
public static void updateMessageNotification() {
|
||||
new UpdateMessageNotificationAction().start();
|
||||
}
|
||||
|
||||
private UpdateMessageNotificationAction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
BugleNotifications.update(true /* silent */, BugleNotifications.UPDATE_MESSAGES);
|
||||
return null;
|
||||
}
|
||||
|
||||
private UpdateMessageNotificationAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateMessageNotificationAction> CREATOR
|
||||
= new Parcelable.Creator<UpdateMessageNotificationAction>() {
|
||||
@Override
|
||||
public UpdateMessageNotificationAction createFromParcel(final Parcel in) {
|
||||
return new UpdateMessageNotificationAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateMessageNotificationAction[] newArray(final int size) {
|
||||
return new UpdateMessageNotificationAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Action used to update size fields of a single part
|
||||
*/
|
||||
public class UpdateMessagePartSizeAction extends Action implements Parcelable {
|
||||
/**
|
||||
* Update size of part
|
||||
*/
|
||||
public static void updateSize(final String partId, final int width, final int height) {
|
||||
Assert.notNull(partId);
|
||||
Assert.inRange(width, 0, Integer.MAX_VALUE);
|
||||
Assert.inRange(height, 0, Integer.MAX_VALUE);
|
||||
|
||||
final UpdateMessagePartSizeAction action = new UpdateMessagePartSizeAction(
|
||||
partId, width, height);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_PART_ID = "part_id";
|
||||
private static final String KEY_WIDTH = "width";
|
||||
private static final String KEY_HEIGHT = "height";
|
||||
|
||||
private UpdateMessagePartSizeAction(final String partId, final int width, final int height) {
|
||||
actionParameters.putString(KEY_PART_ID, partId);
|
||||
actionParameters.putInt(KEY_WIDTH, width);
|
||||
actionParameters.putInt(KEY_HEIGHT, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String partId = actionParameters.getString(KEY_PART_ID);
|
||||
final int width = actionParameters.getInt(KEY_WIDTH);
|
||||
final int height = actionParameters.getInt(KEY_HEIGHT);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
|
||||
values.put(PartColumns.WIDTH, width);
|
||||
values.put(PartColumns.HEIGHT, height);
|
||||
|
||||
// Part may have been deleted so allow update to fail without asserting
|
||||
BugleDatabaseOperations.updateRowIfExists(db, DatabaseHelper.PARTS_TABLE,
|
||||
PartColumns._ID, partId, values);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private UpdateMessagePartSizeAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateMessagePartSizeAction> CREATOR
|
||||
= new Parcelable.Creator<UpdateMessagePartSizeAction>() {
|
||||
@Override
|
||||
public UpdateMessagePartSizeAction createFromParcel(final Parcel in) {
|
||||
return new UpdateMessagePartSizeAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateMessagePartSizeAction[] newArray(final int size) {
|
||||
return new UpdateMessagePartSizeAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
public class WriteDraftMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Set draft message (no listener)
|
||||
*/
|
||||
public static void writeDraftMessage(final String conversationId, final MessageData message) {
|
||||
final WriteDraftMessageAction action = new WriteDraftMessageAction(conversationId, message);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversationId";
|
||||
private static final String KEY_MESSAGE = "message";
|
||||
|
||||
private WriteDraftMessageAction(final String conversationId, final MessageData message) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
|
||||
if (message.getSelfId() == null || message.getParticipantId() == null) {
|
||||
// This could happen when this occurs before the draft message is loaded
|
||||
// In this case, we just use the conversation's current self id as draft's
|
||||
// self id and/or participant id
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, conversationId);
|
||||
if (conversation != null) {
|
||||
final String senderAndSelf = conversation.getSelfId();
|
||||
if (message.getSelfId() == null) {
|
||||
message.bindSelfId(senderAndSelf);
|
||||
}
|
||||
if (message.getParticipantId() == null) {
|
||||
message.bindParticipantId(senderAndSelf);
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_DATAMODEL_TAG, "Conversation " + conversationId +
|
||||
"already deleted before saving draft message " +
|
||||
message.getMessageId() + ". Aborting WriteDraftMessageAction.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Drafts are only kept in the local DB...
|
||||
final String messageId = BugleDatabaseOperations.updateDraftMessageData(
|
||||
db, conversationId, message, BugleDatabaseOperations.UPDATE_MODE_ADD_DRAFT);
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
return messageId;
|
||||
}
|
||||
|
||||
private WriteDraftMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<WriteDraftMessageAction> CREATOR
|
||||
= new Parcelable.Creator<WriteDraftMessageAction>() {
|
||||
@Override
|
||||
public WriteDraftMessageAction createFromParcel(final Parcel in) {
|
||||
return new WriteDraftMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteDraftMessageAction[] newArray(final int size) {
|
||||
return new WriteDraftMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user