1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.messaging.datamodel.action;
18 
19 import android.os.Handler;
20 import androidx.collection.SimpleArrayMap;
21 import android.text.TextUtils;
22 
23 import com.android.messaging.util.Assert.RunsOnAnyThread;
24 import com.android.messaging.util.Assert.RunsOnMainThread;
25 import com.android.messaging.util.LogUtil;
26 import com.android.messaging.util.ThreadUtil;
27 import com.google.common.annotations.VisibleForTesting;
28 
29 import java.text.SimpleDateFormat;
30 import java.util.Date;
31 import java.util.TimeZone;
32 
33 /**
34  * Base class for action monitors
35  * Actions come in various flavors but
36  *  o) Fire and forget - no monitor
37  *  o) Immediate local processing only - will trigger ActionCompletedListener when done
38  *  o) Background worker processing only - will trigger ActionCompletedListener when done
39  *  o) Immediate local processing followed by background work followed by more local processing
40  *      - will trigger ActionExecutedListener once local processing complete and
41  *        ActionCompletedListener when second set of local process (dealing with background
42  *         worker response) is complete
43  */
44 public class ActionMonitor {
45     private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
46 
47     /**
48      * Interface used to notify on completion of local execution for an action
49      */
50     public interface ActionExecutedListener {
51         /**
52          * @param result value returned by {@link Action#executeAction}
53          */
54         @RunsOnMainThread
onActionExecuted(ActionMonitor monitor, final Action action, final Object data, final Object result)55         abstract void onActionExecuted(ActionMonitor monitor, final Action action,
56                 final Object data, final Object result);
57     }
58 
59     /**
60      * Interface used to notify action completion
61      */
62     public interface ActionCompletedListener {
63         /**
64          * @param result object returned from processing the action. This is the value returned by
65          *               {@link Action#executeAction} if there is no background work, or
66          *               else the value returned by
67          *               {@link Action#processBackgroundResponse}
68          */
69         @RunsOnMainThread
onActionSucceeded(ActionMonitor monitor, final Action action, final Object data, final Object result)70         abstract void onActionSucceeded(ActionMonitor monitor,
71                 final Action action, final Object data, final Object result);
72         /**
73          * @param result value returned by {@link Action#processBackgroundFailure}
74          */
75         @RunsOnMainThread
onActionFailed(ActionMonitor monitor, final Action action, final Object data, final Object result)76         abstract void onActionFailed(ActionMonitor monitor, final Action action,
77                 final Object data, final Object result);
78     }
79 
80     /**
81      * Interface for being notified of action state changes - used for profiling, testing only
82      */
83     protected interface ActionStateChangedListener {
84         /**
85          * @param action the action that is changing state
86          * @param state the new state of the action
87          */
88         @RunsOnAnyThread
onActionStateChanged(Action action, int state)89         void onActionStateChanged(Action action, int state);
90     }
91 
92     /**
93      * Operations always start out as STATE_CREATED and finish as STATE_COMPLETE.
94      * Some common state transition sequences in between include:
95      * <ul>
96      *   <li>Local data change only : STATE_QUEUED - STATE_EXECUTING
97      *   <li>Background worker request only : STATE_BACKGROUND_ACTIONS_QUEUED
98      *      - STATE_EXECUTING_BACKGROUND_ACTION
99      *      - STATE_BACKGROUND_COMPLETION_QUEUED
100      *      - STATE_PROCESSING_BACKGROUND_RESPONSE
101      *   <li>Local plus background worker request : STATE_QUEUED - STATE_EXECUTING
102      *      - STATE_BACKGROUND_ACTIONS_QUEUED
103      *      - STATE_EXECUTING_BACKGROUND_ACTION
104      *      - STATE_BACKGROUND_COMPLETION_QUEUED
105      *      - STATE_PROCESSING_BACKGROUND_RESPONSE
106      * </ul>
107      */
108     protected static final int STATE_UNDEFINED = 0;
109     protected static final int STATE_CREATED = 1; // Just created
110     protected static final int STATE_QUEUED = 2; // Action queued for processing
111     protected static final int STATE_EXECUTING = 3; // Action processing on datamodel thread
112     protected static final int STATE_BACKGROUND_ACTIONS_QUEUED = 4;
113     protected static final int STATE_EXECUTING_BACKGROUND_ACTION = 5;
114     // The background work has completed, either returning a success response or resulting in a
115     // failure
116     protected static final int STATE_BACKGROUND_COMPLETION_QUEUED = 6;
117     protected static final int STATE_PROCESSING_BACKGROUND_RESPONSE = 7;
118     protected static final int STATE_COMPLETE = 8; // Action complete
119 
120     /**
121      * Lock used to protect access to state and listeners
122      */
123     private final Object mLock = new Object();
124 
125     /**
126      * Current state of action
127      */
128     @VisibleForTesting
129     protected int mState;
130 
131     /**
132      * Listener which is notified on action completion
133      */
134     private ActionCompletedListener mCompletedListener;
135 
136     /**
137      * Listener which is notified on action executed
138      */
139     private ActionExecutedListener mExecutedListener;
140 
141     /**
142      * Listener which is notified of state changes
143      */
144     private ActionStateChangedListener mStateChangedListener;
145 
146     /**
147      * Handler used to post results back to caller
148      */
149     private final Handler mHandler;
150 
151     /**
152      * Data passed back to listeners (associated with the action when it is created)
153      */
154     private final Object mData;
155 
156     /**
157      * The action key is used to determine equivalence of operations and their requests
158      */
159     private final String mActionKey;
160 
161     /**
162      * Get action key identifying associated action
163      */
getActionKey()164     public String getActionKey() {
165         return mActionKey;
166     }
167 
168     /**
169      * Unregister listeners so that they will not be called back - override this method if needed
170      */
unregister()171     public void unregister() {
172         clearListeners();
173     }
174 
175     /**
176      * Unregister listeners so that they will not be called
177      */
clearListeners()178     protected final void clearListeners() {
179         synchronized (mLock) {
180             mCompletedListener = null;
181             mExecutedListener = null;
182         }
183     }
184 
185     /**
186      * Create a monitor associated with a particular action instance
187      */
ActionMonitor(final int initialState, final String actionKey, final Object data)188     protected ActionMonitor(final int initialState, final String actionKey,
189             final Object data) {
190         mHandler = ThreadUtil.getMainThreadHandler();
191         mActionKey = actionKey;
192         mState = initialState;
193         mData = data;
194     }
195 
196     /**
197      * Return flag to indicate if action is complete
198      */
isComplete()199     public boolean isComplete() {
200         boolean complete = false;
201         synchronized (mLock) {
202             complete = (mState == STATE_COMPLETE);
203         }
204         return complete;
205     }
206 
207     /**
208      * Set listener that will be called with action completed result
209      */
setCompletedListener(final ActionCompletedListener listener)210     protected final void setCompletedListener(final ActionCompletedListener listener) {
211         synchronized (mLock) {
212             mCompletedListener = listener;
213         }
214     }
215 
216     /**
217      * Set listener that will be called with local execution result
218      */
setExecutedListener(final ActionExecutedListener listener)219     protected final void setExecutedListener(final ActionExecutedListener listener) {
220         synchronized (mLock) {
221             mExecutedListener = listener;
222         }
223     }
224 
225     /**
226      * Set listener that will be called with local execution result
227      */
setStateChangedListener(final ActionStateChangedListener listener)228     protected final void setStateChangedListener(final ActionStateChangedListener listener) {
229         synchronized (mLock) {
230             mStateChangedListener = listener;
231         }
232     }
233 
234     /**
235      * Perform a state update transition
236      * @param action - action whose state is updating
237      * @param expectedOldState - expected existing state of action (can be UNKNOWN)
238      * @param newState - new state which will be set
239      */
240     @VisibleForTesting
updateState(final Action action, final int expectedOldState, final int newState)241     protected void updateState(final Action action, final int expectedOldState,
242             final int newState) {
243         ActionStateChangedListener listener = null;
244         synchronized (mLock) {
245             if (expectedOldState != STATE_UNDEFINED &&
246                     mState != expectedOldState) {
247                 throw new IllegalStateException("On updateState to " + newState + " was " + mState
248                         + " expecting " + expectedOldState);
249             }
250             if (newState != mState) {
251                 mState = newState;
252                 listener = mStateChangedListener;
253             }
254         }
255         if (listener != null) {
256             listener.onActionStateChanged(action, newState);
257         }
258     }
259 
260     /**
261      * Perform a state update transition
262      * @param action - action whose state is updating
263      * @param expectedOldState - expected existing state of action (can be UNKNOWN)
264      * @param newState - new state which will be set
265      */
setState(final Action action, final int expectedOldState, final int newState)266     static void setState(final Action action, final int expectedOldState,
267             final int newState) {
268         int oldMonitorState = expectedOldState;
269         int newMonitorState = newState;
270         final ActionMonitor monitor
271                 = ActionMonitor.lookupActionMonitor(action.actionKey);
272         if (monitor != null) {
273             oldMonitorState = monitor.mState;
274             monitor.updateState(action, expectedOldState, newState);
275             newMonitorState = monitor.mState;
276         }
277         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
278             final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
279             df.setTimeZone(TimeZone.getTimeZone("UTC"));
280             LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
281                     + "UTC State = " + oldMonitorState + " - " + newMonitorState);
282         }
283     }
284 
285     /**
286      * Mark action complete
287      * @param action - action whose state is updating
288      * @param expectedOldState - expected existing state of action (can be UNKNOWN)
289      * @param result - object returned from processing the action. This is the value returned by
290      *                 {@link Action#executeAction} if there is no background work, or
291      *                 else the value returned by {@link Action#processBackgroundResponse}
292      *                 or {@link Action#processBackgroundFailure}
293      */
complete(final Action action, final int expectedOldState, final Object result, final boolean succeeded)294     private final void complete(final Action action,
295             final int expectedOldState, final Object result,
296             final boolean succeeded) {
297         ActionCompletedListener completedListener = null;
298         synchronized (mLock) {
299             setState(action, expectedOldState, STATE_COMPLETE);
300             completedListener = mCompletedListener;
301             mExecutedListener = null;
302             mStateChangedListener = null;
303         }
304         if (completedListener != null) {
305             // Marshal to UI thread
306             mHandler.post(new Runnable() {
307                 @Override
308                 public void run() {
309                     ActionCompletedListener listener = null;
310                     synchronized (mLock) {
311                         if (mCompletedListener != null) {
312                             listener = mCompletedListener;
313                         }
314                         mCompletedListener = null;
315                     }
316                     if (listener != null) {
317                         if (succeeded) {
318                             listener.onActionSucceeded(ActionMonitor.this,
319                                     action, mData, result);
320                         } else {
321                             listener.onActionFailed(ActionMonitor.this,
322                                     action, mData, result);
323                         }
324                     }
325                 }
326             });
327         }
328     }
329 
330     /**
331      * Mark action complete
332      * @param action - action whose state is updating
333      * @param expectedOldState - expected existing state of action (can be UNKNOWN)
334      * @param result - object returned from processing the action. This is the value returned by
335      *                 {@link Action#executeAction} if there is no background work, or
336      *                 else the value returned by {@link Action#processBackgroundResponse}
337      *                 or {@link Action#processBackgroundFailure}
338      */
setCompleteState(final Action action, final int expectedOldState, final Object result, final boolean succeeded)339     static void setCompleteState(final Action action, final int expectedOldState,
340             final Object result, final boolean succeeded) {
341         int oldMonitorState = expectedOldState;
342         final ActionMonitor monitor
343                 = ActionMonitor.lookupActionMonitor(action.actionKey);
344         if (monitor != null) {
345             oldMonitorState = monitor.mState;
346             monitor.complete(action, expectedOldState, result, succeeded);
347             unregisterActionMonitorIfComplete(action.actionKey, monitor);
348         }
349         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
350             final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
351             df.setTimeZone(TimeZone.getTimeZone("UTC"));
352             LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
353                     + "UTC State = " + oldMonitorState + " - " + STATE_COMPLETE);
354         }
355     }
356 
357     /**
358      * Mark action complete
359      * @param action - action whose state is updating
360      * @param expectedOldState - expected existing state of action (can be UNKNOWN)
361      * @param hasBackgroundActions - has the completing action requested background work
362      * @param result - the return value of {@link Action#executeAction}
363      */
executed(final Action action, final int expectedOldState, final boolean hasBackgroundActions, final Object result)364     final void executed(final Action action,
365             final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
366         ActionExecutedListener executedListener = null;
367         synchronized (mLock) {
368             if (hasBackgroundActions) {
369                 setState(action, expectedOldState, STATE_BACKGROUND_ACTIONS_QUEUED);
370             }
371             executedListener = mExecutedListener;
372         }
373         if (executedListener != null) {
374             // Marshal to UI thread
375             mHandler.post(new Runnable() {
376                 @Override
377                 public void run() {
378                     ActionExecutedListener listener = null;
379                     synchronized (mLock) {
380                         if (mExecutedListener != null) {
381                             listener = mExecutedListener;
382                             mExecutedListener = null;
383                         }
384                     }
385                     if (listener != null) {
386                         listener.onActionExecuted(ActionMonitor.this,
387                                 action, mData, result);
388                     }
389                 }
390             });
391         }
392     }
393 
394     /**
395      * Mark action complete
396      * @param action - action whose state is updating
397      * @param expectedOldState - expected existing state of action (can be UNKNOWN)
398      * @param hasBackgroundActions - has the completing action requested background work
399      * @param result - the return value of {@link Action#executeAction}
400      */
setExecutedState(final Action action, final int expectedOldState, final boolean hasBackgroundActions, final Object result)401     static void setExecutedState(final Action action,
402             final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
403         int oldMonitorState = expectedOldState;
404         final ActionMonitor monitor
405                 = ActionMonitor.lookupActionMonitor(action.actionKey);
406         if (monitor != null) {
407             oldMonitorState = monitor.mState;
408             monitor.executed(action, expectedOldState, hasBackgroundActions, result);
409         }
410         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
411             final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
412             df.setTimeZone(TimeZone.getTimeZone("UTC"));
413             LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
414                     + "UTC State = " + oldMonitorState + " - EXECUTED");
415         }
416     }
417 
418     /**
419      * Map of action monitors indexed by actionKey
420      */
421     @VisibleForTesting
422     static SimpleArrayMap<String, ActionMonitor> sActionMonitors =
423             new SimpleArrayMap<String, ActionMonitor>();
424 
425     /**
426      * Insert new monitor into map
427      */
registerActionMonitor(final String actionKey, final ActionMonitor monitor)428     static void registerActionMonitor(final String actionKey,
429             final ActionMonitor monitor) {
430         if (monitor != null
431                 && (TextUtils.isEmpty(monitor.getActionKey())
432                         || TextUtils.isEmpty(actionKey)
433                         || !actionKey.equals(monitor.getActionKey()))) {
434             throw new IllegalArgumentException("Monitor key " + monitor.getActionKey()
435                     + " not compatible with action key " + actionKey);
436         }
437         synchronized (sActionMonitors) {
438             sActionMonitors.put(actionKey, monitor);
439         }
440     }
441 
442     /**
443      * Find monitor associated with particular action
444      */
lookupActionMonitor(final String actionKey)445     private static ActionMonitor lookupActionMonitor(final String actionKey) {
446         ActionMonitor monitor = null;
447         synchronized (sActionMonitors) {
448             monitor = sActionMonitors.get(actionKey);
449         }
450         return monitor;
451     }
452 
453     /**
454      * Remove monitor from map
455      */
456     @VisibleForTesting
unregisterActionMonitor(final String actionKey, final ActionMonitor monitor)457     static void unregisterActionMonitor(final String actionKey,
458             final ActionMonitor monitor) {
459         if (monitor != null) {
460             synchronized (sActionMonitors) {
461                 sActionMonitors.remove(actionKey);
462             }
463         }
464     }
465 
466     /**
467      * Remove monitor from map if the action is complete
468      */
unregisterActionMonitorIfComplete(final String actionKey, final ActionMonitor monitor)469     static void unregisterActionMonitorIfComplete(final String actionKey,
470             final ActionMonitor monitor) {
471         if (monitor != null && monitor.isComplete()) {
472             synchronized (sActionMonitors) {
473                 sActionMonitors.remove(actionKey);
474             }
475         }
476     }
477 }
478