/*
* 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 androidx.collection.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:
*
* - Local data change only : STATE_QUEUED - STATE_EXECUTING
*
- Background worker request only : STATE_BACKGROUND_ACTIONS_QUEUED
* - STATE_EXECUTING_BACKGROUND_ACTION
* - STATE_BACKGROUND_COMPLETION_QUEUED
* - STATE_PROCESSING_BACKGROUND_RESPONSE
*
- 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
*
*/
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 sActionMonitors =
new SimpleArrayMap();
/**
* 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);
}
}
}
}