Permalink
Browse files

Add realtime update for the Gauge list fragment

This approach works by introducing RealtimeTrafficService,
which manages the Pusher previously associated only with the
AirTraffic activity. Interested activities and fragments
bind to the service, and register themselves as listeners for
Hit events.

This has the advantage that the same pusher can be initialised
and then shared amongst multiple activities - so start up of
the AirTraffic activity can now be immediate, rather than the
10-second-ish delay we currently have.

Only the view count is incremented at the moment, but the
Gauges web app has code which shows us how we can use the
'uniques' data to increment the 'people' counter too.
  • Loading branch information...
1 parent 408d7cb commit 7eabc03db48562c7eeafb49349bd81835239d889 @rtyley rtyley committed Mar 13, 2012
View
@@ -45,6 +45,9 @@
</intent-filter>
</activity>
+
+ <service android:name=".realtime.RealtimeTrafficService"/>
+
<service android:name=".authenticator.AccountAuthenticatorService" android:process=":auth">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.github.mobile.gauges.ui.airtraffic;
+package com.github.mobile.gauges.realtime;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.FROYO;
@@ -23,6 +23,7 @@
import com.emorym.android_pusher.Pusher;
import com.github.mobile.gauges.GaugesServiceProvider;
import com.github.mobile.gauges.core.GaugesService;
+import com.google.inject.Provider;
import java.io.IOException;
@@ -37,29 +38,21 @@
private static final int AUTH_TIMEOUT = 30 * 1000;
- private final GaugesServiceProvider serviceProvider;
-
- private GaugesService service;
+ private final Provider<GaugesService> serviceProvider;
/**
* Create pusher
*
- * @param provider
+ * @param serviceProvider
*/
- public GaugesPusher(GaugesServiceProvider provider) {
+ public GaugesPusher(Provider<GaugesService> serviceProvider) {
// Skip certificate validation on Froyo or below
super(PUSHER_APP_KEY, true, SDK_INT <= FROYO);
- serviceProvider = provider;
+ this.serviceProvider = serviceProvider;
}
@Override
protected String authenticate(String channelName) throws IOException {
- if (service == null)
- try {
- service = serviceProvider.getService();
- } catch (AccountsException e) {
- throw new IOException(e.getMessage());
- }
// Socket id is required before authentication can begin so wait
// until it comes back on the connection_established event
int slept = 0;
@@ -76,6 +69,6 @@ protected String authenticate(String channelName) throws IOException {
if (slept >= AUTH_TIMEOUT)
break;
}
- return mSocketId != null ? service.getPusherAuth(mSocketId, channelName) : null;
+ return mSocketId != null ? serviceProvider.get().getPusherAuth(mSocketId, channelName) : null;
}
}
@@ -14,26 +14,26 @@
* limitations under the License.
*/
-package com.github.mobile.gauges.ui.airtraffic;
+package com.github.mobile.gauges.realtime;
/**
* Class to model a hit of traffic to a specific site
*/
public class Hit {
- final String title;
+ public final String title;
- final String siteId;
+ public final String siteId;
- final float lon;
+ public final float lon;
- final float lat;
+ public final float lat;
- final String city;
+ public final String city;
- final String region;
+ public final String region;
- final String country;
+ public final String country;
/**
* Create a hit for the given site
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012 GitHub Inc.
+ *
+ * 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.github.mobile.gauges.realtime;
+
+import com.github.mobile.gauges.core.Gauge;
+
+public interface HitListener {
+ public void observe(Hit hit, Gauge gauge);
+}
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012 GitHub Inc.
+ *
+ * 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.github.mobile.gauges.realtime;
+
+
+import static java.lang.Thread.sleep;
+import static java.util.Collections.emptyList;
+import android.util.Log;
+
+import com.github.mobile.gauges.core.DatedViewSummary;
+import com.github.mobile.gauges.core.Gauge;
+import com.github.mobile.gauges.core.GaugesService;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Activities and Fragments should not retain references to
+ * the traffic monitor, as fresh instances will be created
+ * when user credentials change.
+ */
+class RealtimeTrafficMonitor implements Runnable {
+
+ public static final String TAG = "RTM";
+
+ private static final String CHANNEL_PREFIX = "private-";
+
+ private final Provider<GaugesService> apiService;
+ private final RealtimeTrafficService realtimeTrafficService;
+ private final GaugesPusher pusher;
+ private final Map<String, Gauge> idGauges = new HashMap<String, Gauge>();
+ private final TrafficPusherCallback callback = new TrafficPusherCallback() {
+ protected void onHit(final Hit hit) {
+ realtimeTrafficService.broadcast(hit, updatedGaugeFor(hit));
+ }
+ };
+
+ private boolean cancelled = false;
+ private List<Gauge> gauges = emptyList();
+
+ public RealtimeTrafficMonitor(RealtimeTrafficService realtimeTrafficService, Provider<GaugesService> apiService) {
+ this.apiService = apiService;
+ this.realtimeTrafficService = realtimeTrafficService;
+ pusher = new GaugesPusher(apiService);
+ }
+
+ void cancel() {
+ Log.d(TAG,"monitor cancel() called");
+ cancelled = true;
+ }
+
+ public void run() {
+ while (!cancelled) {
+ try {
+ gauges = apiService.get().getGauges();
+ } catch (IOException e) {
+ Log.e(TAG, "Problem getting gauges ", e);
+ }
+
+ Log.d(TAG,"current gauge count = "+gauges.size());
+ for (Gauge gauge : gauges) {
+ if (idGauges.put(gauge.getId(), gauge) == null) {
+ Log.d(TAG,"subscribing to new Gauge "+gauge.getId());
+ pusher.subscribe(CHANNEL_PREFIX + gauge.getId()).bind("hit", callback);
+ }
+ }
+
+ try { sleep(120*1000L); } catch (InterruptedException e) { }
+ }
+
+ Log.d(TAG,"cancelled, unsubscribing");
+ for (String gaugeId : idGauges.keySet()) {
+ pusher.unsubscribe(CHANNEL_PREFIX + gaugeId);
+ }
+ pusher.disconnect();
+ }
+
+ private Gauge updatedGaugeFor(Hit hit) {
+ Gauge gauge = idGauges.get(hit.siteId);
+ if (gauge != null) {
+ DatedViewSummary today = gauge.getToday();
+ if (today != null) {
+ today.setViews(today.getViews() + 1);
+ }
+ }
+ return gauge;
+ }
+
+ public List<Gauge> getGauges() {
+ return gauges;
+ }
+
+}
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2012 GitHub Inc.
+ *
+ * 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.github.mobile.gauges.realtime;
+
+
+import static com.github.mobile.gauges.authenticator.AuthConstants.AUTHTOKEN_TYPE;
+import static com.github.mobile.gauges.authenticator.AuthConstants.GAUGES_ACCOUNT_TYPE;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OnAccountsUpdateListener;
+import android.accounts.OperationCanceledException;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.github.mobile.gauges.core.Gauge;
+import com.github.mobile.gauges.core.GaugesService;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+
+import roboguice.service.RoboService;
+
+/**
+ * Android Service that interested Activities/Fragments can bind to in order to obtain realtime traffic data.
+ * The lifetime of the service is dictated by bound clients - when there are no more bound clients, the Android OS
+ * will automatically destroy the service.
+ * <p/>
+ * On pausing/resuming your activity/fragment, you should add/remove yourself as listener on the localBinder.
+ * This stops the service from holding references to your activity after it's finished, which would be
+ * a memory leak.
+ * <p/>
+ * On starting/stopping your activity/fragment, you should bind/unbind the service. This allows Android to shutdown
+ * the service if no-one is currently using it.
+ */
+public class RealtimeTrafficService extends RoboService implements OnAccountsUpdateListener {
+
+ public static final String TAG = "RTS";
+
+ @Inject
+ private AccountManager accountManager;
+
+ private final ConcurrentHashMap<HitListener, Boolean> hitListeners = new ConcurrentHashMap<HitListener, Boolean>();
+
+ private final ExecutorService backgroundThread = newFixedThreadPool(1);
+
+ private final IBinder mBinder = new LocalBinder();
+
+ private RealtimeTrafficMonitor gaugeMonitor;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "Creating realtime service");
+ accountManager.addOnAccountsUpdatedListener(this, null, true);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "Destroying realtime service");
+ accountManager.removeOnAccountsUpdatedListener(this);
+ cancelMonitor();
+ backgroundThread.shutdownNow();
+ }
+
+ private void cancelMonitor() {
+ if (gaugeMonitor != null)
+ gaugeMonitor.cancel();
+ }
+
+ @Override
+ public void onAccountsUpdated(Account[] accounts) {
+ Log.d(TAG, "onAccountsUpdated() invoked");
+ for (final Account account : accounts) {
+ Log.d(TAG, "Checking " + account.name);
+ if (account.type.equals(GAUGES_ACCOUNT_TYPE)) {
+ Log.d(TAG, "Correct account type for " + account.name);
+ cancelMonitor();
+ gaugeMonitor = new RealtimeTrafficMonitor(this, new Provider<GaugesService>() {
+ @Override
+ public GaugesService get() {
+ String apiKey = null;
+ try {
+ apiKey = accountManager.blockingGetAuthToken(account, AUTHTOKEN_TYPE, true);
+ Log.d(TAG, "Got apiKey : "+apiKey);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return new GaugesService(apiKey);
+ }
+ });
+ backgroundThread.execute(gaugeMonitor);
+ }
+ }
+ }
+
+ void broadcast(Hit hit, Gauge gauge) {
+ long views = gauge.getToday().getViews();
+ Log.d(TAG, "Broadcasting '" + gauge.getTitle() + "' views=" + views + " to " + hitListeners.size());
+ for (HitListener hitListener : hitListeners.keySet())
+ hitListener.observe(hit, gauge);
+ }
+
+ class LocalBinder extends Binder {
+
+ public void addHitListener(HitListener hitListener) {
+ hitListeners.put(hitListener, true);
+ }
+
+ /**
+ * We don't want to retain references to hitListeners longer than we have to,
+ * they are probably activities or fragments that reference activities, and
+ * keeping a reference to those is a memory leak.
+ *
+ * @param hitListener
+ */
+ public void removeHitListener(HitListener hitListener) {
+ hitListeners.remove(hitListener);
+ }
+
+ public List<Gauge> getGauges() {
+ return gaugeMonitor == null ? null : gaugeMonitor.getGauges();
+ }
+ }
+
+}
Oops, something went wrong.

0 comments on commit 7eabc03

Please sign in to comment.