/* * 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 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; import org.json.JSONException; 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.event.EventServer; 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; /** * Manage the event queue. <br> * <br> * <b>Usage Notes:</b><br> * EventFacade APIs interact with the Event Queue (a data buffer containing up to 1024 event * entries).<br> * Events are automatically entered into the Event Queue following API calls such as startSensing() * and startLocating().<br> * The Event Facade provides control over how events are entered into (and removed from) the Event * Queue.<br> * 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<Event> mEventQueue = new ConcurrentLinkedQueue<Event>(); private final CopyOnWriteArrayList<EventObserver> mGlobalEventObservers = new CopyOnWriteArrayList<EventObserver>(); private final Multimap<String, EventObserver> mNamedEventObservers = Multimaps .synchronizedListMultimap(ArrayListMultimap.<String, EventObserver> create()); private EventServer mEventServer = null; private final HashMap<String, BroadcastListener> mBroadcastListeners = new HashMap<String, BroadcastListener>(); private final Context mContext; private boolean bEventServerRunning; public EventFacade(FacadeManager manager) { super(manager); mContext = manager.getService().getApplicationContext(); Log.v("Creating new EventFacade Instance()"); bEventServerRunning = false; } /** * 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<String> eventGetBrodcastCategories() { return mBroadcastListeners.keySet(); } /** * Actual data returned in the map will depend on the type of event. * * <pre> * 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'] * </pre> * * e has the format:<br> * [{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}]<br> * x has the string value of the x force data (0.42223078000000003) at the time of the event * entry. </pre> */ @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<Event> eventPoll( @RpcParameter(name = "number_of_events") @RpcDefault("1") Integer number_of_events) { List<Event> 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<Event> 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<Event>(); 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)); removeEventObserver(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<Event> futureEvent = new FutureResult<Event>(); 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; } /** * <pre> * Example: * import android * from datetime import datetime * droid = android.Android() * t = datetime.now() * droid.eventPost('Some Event', t) * </pre> */ @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 != false) { synchronized (mEventQueue) { while (mEventQueue.size() >= MAX_QUEUE_SIZE) { mEventQueue.remove(); } mEventQueue.add(event); } Log.v(String.format("postEvent(%s)", name)); } synchronized (mNamedEventObservers) { for (EventObserver observer : mNamedEventObservers.get(name)) { observer.onEventReceived(event); } } synchronized (mGlobalEventObservers) { // TODO: Remove log. Log.v(String.format("mGlobalEventObservers size (%s)", mGlobalEventObservers.size())); for (EventObserver observer : mGlobalEventObservers) { 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); } @Rpc(description = "Opens up a socket where you can read for events posted") public int startEventDispatcher( @RpcParameter(name = "port", description = "Port to use") @RpcDefault("0") @RpcOptional() Integer port) { if (mEventServer == null) { if (port == null) { port = 0; } mEventServer = new EventServer(port); addGlobalEventObserver(mEventServer); bEventServerRunning = true; } return mEventServer.getAddress().getPort(); } @Rpc(description = "sl4a session is shutting down, send terminate event to client.") public void closeSl4aSession() { eventClearBuffer(); postEvent("EventDispatcherShutdown", null); } @Rpc(description = "Stops the event server, you can't read in the port anymore") public void stopEventDispatcher() throws RuntimeException { if (bEventServerRunning == true) { if (mEventServer == null) { throw new RuntimeException("Not running"); } bEventServerRunning = false; mEventServer.shutdown(); Log.v(String.format("stopEventDispatcher (%s)", mEventServer)); removeEventObserver(mEventServer); mEventServer = null; } return; } @Override public void shutdown() { try { stopEventDispatcher(); } catch (Exception e) { Log.e("Exception tearing down event dispatcher", e); } mGlobalEventObservers.clear(); mEventQueue.clear(); } public void addNamedEventObserver(String eventName, EventObserver observer) { mNamedEventObservers.put(eventName, observer); } public void addGlobalEventObserver(EventObserver observer) { mGlobalEventObservers.add(observer); } public void removeEventObserver(EventObserver observer) { mNamedEventObservers.removeAll(observer); mGlobalEventObservers.remove(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(); } } } }