/* * Copyright (C) 2017 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.googlecode.android_scripting.facade; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.event.Event; import com.googlecode.android_scripting.event.EventObserver; import com.googlecode.android_scripting.future.FutureResult; import com.googlecode.android_scripting.jsonrpc.JsonBuilder; import com.googlecode.android_scripting.jsonrpc.RpcReceiver; import com.googlecode.android_scripting.rpc.Rpc; import com.googlecode.android_scripting.rpc.RpcDefault; import com.googlecode.android_scripting.rpc.RpcDeprecated; import com.googlecode.android_scripting.rpc.RpcName; import com.googlecode.android_scripting.rpc.RpcOptional; import com.googlecode.android_scripting.rpc.RpcParameter; import org.json.JSONException; import java.util.HashMap; import java.util.List; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; /** * Manage the event queue.
*
* Usage Notes:
* EventFacade APIs interact with the Event Queue (a data buffer containing up to 1024 event * entries).
* Events are automatically entered into the Event Queue following API calls such as startSensing() * and startLocating().
* The Event Facade provides control over how events are entered into (and removed from) the Event * Queue.
* The Event Queue provides a useful means of recording background events (such as sensor data) when * the phone is busy with foreground activities. */ public class EventFacade extends RpcReceiver { /** * The maximum length of the event queue. Old events will be discarded when this limit is * exceeded. */ private static final int MAX_QUEUE_SIZE = 1024; private final Queue mEventQueue = new ConcurrentLinkedQueue(); private final CopyOnWriteArrayList mGlobalEventObservers = new CopyOnWriteArrayList(); private final Multimap mNamedEventObservers = Multimaps .synchronizedListMultimap(ArrayListMultimap.create()); private final HashMap mBroadcastListeners = new HashMap(); private final Context mContext; public EventFacade(FacadeManager manager) { super(manager); mContext = manager.getService().getApplicationContext(); Log.v("Creating new EventFacade Instance()"); } /** * Example (python): droid.eventClearBuffer() */ @Rpc(description = "Clears all events from the event buffer.") public void eventClearBuffer() { mEventQueue.clear(); } /** * Registers a listener for a new broadcast signal */ @Rpc(description = "Registers a listener for a new broadcast signal") public boolean eventRegisterForBroadcast( @RpcParameter(name = "category") String category, @RpcParameter(name = "enqueue", description = "Should this events be added to the event queue or only dispatched") @RpcDefault(value = "true") Boolean enqueue) { if (mBroadcastListeners.containsKey(category)) { return false; } BroadcastListener b = new BroadcastListener(this, enqueue.booleanValue()); IntentFilter c = new IntentFilter(category); mContext.registerReceiver(b, c); mBroadcastListeners.put(category, b); return true; } @Rpc(description = "Stop listening for a broadcast signal") public void eventUnregisterForBroadcast( @RpcParameter(name = "category") String category) { if (!mBroadcastListeners.containsKey(category)) { return; } mContext.unregisterReceiver(mBroadcastListeners.get(category)); mBroadcastListeners.remove(category); } @Rpc(description = "Lists all the broadcast signals we are listening for") public Set eventGetBrodcastCategories() { return mBroadcastListeners.keySet(); } /** * Actual data returned in the map will depend on the type of event. *

*

     * Example (python):
     *     import android, time
     *     droid = android.Android()
     *     droid.startSensing()
     *     time.sleep(1)
     *     droid.eventClearBuffer()
     *     time.sleep(1)
     *     e = eventPoll(1).result
     *     event_entry_number = 0
     *     x = e[event_entry_ number]['data']['xforce']
     * 
*

* e has the format:
* [{u'data': {u'accuracy': 0, u'pitch': -0.48766891956329345, u'xmag': -5.6875, u'azimuth': * 0.3312483489513397, u'zforce': 8.3492730000000002, u'yforce': 4.5628165999999997, u'time': * 1297072704.813, u'ymag': -11.125, u'zmag': -42.375, u'roll': -0.059393649548292161, * u'xforce': 0.42223078000000003}, u'name': u'sensors', u'time': 1297072704813000L}]
* x has the string value of the x force data (0.42223078000000003) at the time of the event * entry. */ @Rpc(description = "Returns and removes the oldest n events (i.e. location or sensor update, etc.) from the event buffer.", returns = "A List of Maps of event properties.") public List eventPoll( @RpcParameter(name = "number_of_events") @RpcDefault("1") Integer number_of_events) { List events = Lists.newArrayList(); for (int i = 0; i < number_of_events; i++) { Event event = mEventQueue.poll(); if (event == null) { break; } events.add(event); } return events; } @Rpc(description = "Blocks until an event with the supplied name occurs. Event is removed from the buffer if removeEvent is True.", returns = "Map of event properties.") public Event eventWaitFor( @RpcParameter(name = "eventName") final String eventName, @RpcParameter(name = "removeEvent") final Boolean removeEvent, @RpcParameter(name = "timeout", description = "the maximum time to wait (in ms)") @RpcOptional Integer timeout) throws InterruptedException { Event result = null; final FutureResult futureEvent; synchronized (mEventQueue) { // First check to make sure it isn't already there for (Event event : mEventQueue) { if (event.getName().equals(eventName)) { result = event; if (removeEvent) mEventQueue.remove(event); return result; } } futureEvent = new FutureResult(); addNamedEventObserver(eventName, new EventObserver() { @Override public void onEventReceived(Event event) { if (event.getName().equals(eventName)) { synchronized (futureEvent) { if (!futureEvent.isDone()) { futureEvent.set(event); // TODO: Remove log. Log.v(String.format("Removing observer (%s) got event (%s)", this, event)); removeNamedEventObserver(eventName, this); } if (removeEvent) mEventQueue.remove(event); } } } }); } if (futureEvent != null) { if (timeout != null) { result = futureEvent.get(timeout, TimeUnit.MILLISECONDS); } else { result = futureEvent.get(); } } return result; } @Rpc(description = "Blocks until an event occurs. The returned event is removed from the buffer.", returns = "Map of event properties.") public Event eventWait( @RpcParameter(name = "timeout", description = "the maximum time to wait") @RpcOptional Integer timeout) throws InterruptedException { Event result = null; final FutureResult futureEvent = new FutureResult(); EventObserver observer; synchronized (mEventQueue) { // Anything in queue? if (mEventQueue.size() > 0) { return mEventQueue.poll(); // return it. } observer = new EventObserver() { @Override public void onEventReceived(Event event) { // set up observer for any events. synchronized (futureEvent) { if (!futureEvent.isDone()) { futureEvent.set(event); // TODO: Remove log. Log.v(String.format("onEventReceived for event (%s)", event)); } } } }; addGlobalEventObserver(observer); } if (timeout != null) { result = futureEvent.get(timeout, TimeUnit.MILLISECONDS); } else { result = futureEvent.get(); } if (result != null) { mEventQueue.remove(result); } // TODO: Remove log. Log.v(String.format("Removing observer (%s) got event (%s)", observer, result)); if (observer != null) { removeEventObserver(observer); // Make quite sure this goes away. } return result; } /** *

     * Example:
     *   import android
     *   from datetime import datetime
     *   droid = android.Android()
     *   t = datetime.now()
     *   droid.eventPost('Some Event', t)
     * 
*/ @Rpc(description = "Post an event to the event queue.") public void eventPost( @RpcParameter(name = "name", description = "Name of event") String name, @RpcParameter(name = "data", description = "Data contained in event.") String data, @RpcParameter(name = "enqueue", description = "Set to False if you don't want your events to be added to the event queue, just dispatched.") @RpcOptional @RpcDefault("false") Boolean enqueue) { postEvent(name, data, enqueue.booleanValue()); } /** * Post an event and queue it */ public void postEvent(String name, Object data) { postEvent(name, data, true); } /** * Posts an event with to the event queue. */ public void postEvent(String name, Object data, boolean enqueue) { Event event = new Event(name, data); if (enqueue) { Log.v(String.format("postEvent(%s)", name)); synchronized (mEventQueue) { while (mEventQueue.size() >= MAX_QUEUE_SIZE) { mEventQueue.remove(); } mEventQueue.add(event); // b/77306870: Posting to the EventObservers when enqueuing an event must be // done when mEventQueue is locked. Otherwise, we can run into the following // race condition: // 1) postEvent() adds the event to the event queue, and releases mEventQueue. // Here, the thread is put to sleep. // 2) eventWait() is called when an event is queued, and exits immediately. // 3) eventWait() is called a second time, finds no event and creates a // GlobalEventObserver. // 4) postEvent() wakes back up, and continues to post the event to the observers. // The same event sent to the first eventWait call is sent to the // second eventWait call's observer, causing a duplicated received // event. postEventToNamedObservers(event); postEventToGlobalObservers(event); } } else { postEventToNamedObservers(event); postEventToGlobalObservers(event); } } /** * Posts the event to all applicable Named Observers. */ private void postEventToNamedObservers(Event event) { synchronized (mNamedEventObservers) { for (EventObserver observer : mNamedEventObservers.get(event.getName())) { Log.d(String.format("namedEventObserver %s received event %s", observer, event.getName())); observer.onEventReceived(event); } } } /** * Posts the event to the Global Observers list. */ private void postEventToGlobalObservers(Event event) { synchronized (mGlobalEventObservers) { for (EventObserver observer : mGlobalEventObservers) { Log.d(String.format("globalEventObserver %s received event %s", observer, event.getName())); observer.onEventReceived(event); } } } @RpcDeprecated(value = "eventPost", release = "r4") @Rpc(description = "Post an event to the event queue.") @RpcName(name = "postEvent") public void rpcPostEvent( @RpcParameter(name = "name") String name, @RpcParameter(name = "data") String data) { postEvent(name, data); } @RpcDeprecated(value = "eventPoll", release = "r4") @Rpc(description = "Returns and removes the oldest event (i.e. location or sensor update, etc.) from the event buffer.", returns = "Map of event properties.") public Event receiveEvent() { return mEventQueue.poll(); } @RpcDeprecated(value = "eventWaitFor", release = "r4") @Rpc(description = "Blocks until an event with the supplied name occurs. Event is removed from the buffer if removeEvent is True.", returns = "Map of event properties.") public Event waitForEvent( @RpcParameter(name = "eventName") final String eventName, @RpcOptional final Boolean removeEvent, @RpcParameter(name = "timeout", description = "the maximum time to wait") @RpcOptional Integer timeout) throws InterruptedException { return eventWaitFor(eventName, removeEvent, timeout); } /** * Closes this SL4A session, and sends a terminating signal to the event observers. */ @Rpc(description = "sl4a session is shutting down, send terminate event to client.") public void closeSl4aSession() { eventClearBuffer(); postEvent("EventDispatcherShutdown", null); } /** * Shuts down the RPC server. */ @Override public void shutdown() { mGlobalEventObservers.clear(); mEventQueue.clear(); } /** * Adds a named observer to the event listening queue. * @param eventName the name of the event to listen to * @param observer the observer object */ public void addNamedEventObserver(String eventName, EventObserver observer) { mNamedEventObservers.put(eventName, observer); } /** * Adds a global event listener ot the listening queue. * @param observer the observer object */ public void addGlobalEventObserver(EventObserver observer) { mGlobalEventObservers.add(observer); } /** * Removes an observer from the event listening queue. * @param observer the observer to remove */ public void removeEventObserver(EventObserver observer) { mGlobalEventObservers.remove(observer); } /** * Removes a named observer from the event listening queue. * @param eventName the name of the event being listened to. * @param observer the observer to remove */ public void removeNamedEventObserver(String eventName, EventObserver observer) { mNamedEventObservers.removeAll(eventName); removeEventObserver(observer); } public class BroadcastListener extends android.content.BroadcastReceiver { private EventFacade mParent; private boolean mEnQueue; public BroadcastListener(EventFacade parent, boolean enqueue) { mParent = parent; mEnQueue = enqueue; } @Override public void onReceive(Context context, Intent intent) { Bundle data; if (intent.getExtras() != null) { data = (Bundle) intent.getExtras().clone(); } else { data = new Bundle(); } data.putString("action", intent.getAction()); try { mParent.eventPost("sl4a", JsonBuilder.build(data).toString(), mEnQueue); } catch (JSONException e) { e.printStackTrace(); } } } }