package com.nbs.client.assassins.services; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import com.google.android.gcm.GCMRegistrar; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks; import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener; import com.google.android.gms.location.ActivityRecognitionClient; import com.google.android.gms.location.LocationClient; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationListener; import com.googlecode.androidannotations.annotations.AfterInject; import com.googlecode.androidannotations.annotations.Background; import com.googlecode.androidannotations.annotations.EService; import com.googlecode.androidannotations.annotations.SystemService; import com.googlecode.androidannotations.annotations.UiThread; import com.googlecode.androidannotations.annotations.rest.RestService; import com.nbs.client.assassins.models.App; import com.nbs.client.assassins.models.Player; import com.nbs.client.assassins.models.PlayerModel; import com.nbs.client.assassins.models.Repository; import com.nbs.client.assassins.models.User; import com.nbs.client.assassins.network.HuntedRestClient; import com.nbs.client.assassins.network.UpdateLocationRequest; import com.nbs.client.assassins.network.LocationResponse; import com.nbs.client.assassins.utils.Bus; import com.nbs.client.assassins.utils.LocationUtils; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.os.IBinder; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.widget.Toast; @EService public class LocationService extends Service { private static final String TAG = "LocationService"; public static final String LOCATION_UPDATED = "com.nbs.android.client.LOCATION_UPDATED"; public static final String STOP_UPDATES = "com.nbs.android.client.STOP_LOCATION_UPDATES"; public static final String START_UPDATES = "com.nbs.android.client.START_LOCATION_UPDATES"; public static final String SEND_LOCATION_NOW = "com.nbs.android.client.SEND_LOCATION_NOW"; public static final float SEARCH_MIN_DISPLACEMENT = 100.0f; public static final float HUNT_MIN_DISPLACEMENT = 10.0f; public static final float ATTACK_MIN_DISPLACEMENT = 0.5f; public static final int SEARCH_INTERVAL = 10000; public static final int HUNT_INTERVAL = 2000; public static final int ATTACK_INTERVAL = 500; @RestService HuntedRestClient restClient; @SystemService LocationManager locationManager; LocationClient locationClient; LocationListener locationListener; ActivityRecognitionClient userActivityRecognitionClient; private IntentFilter intentFilter; private BroadcastReceiver intentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Log.d(TAG, "received intent [" + action + "]"); if(action.equals(PushNotifications.MATCH_COUNTDOWN) || action.equals(User.LOGIN_COMPLETE) || action.equals(PushNotifications.MATCH_START)) { sendLocationToServer(locationClient.getLastLocation()); } else if(action.equals(PlayerModel.TARGET_RANGE_CHANGED) || action.equals(PlayerModel.ENEMY_RANGE_CHANGED)) { //TODO if the user is not moving, do not request location updates //use activity recognition (in comments below) //throttle location updates based on the nearest of these two ranges String tRange = PlayerModel.getTargetProximity(LocationService.this); String eRange = PlayerModel.getEnemyProximity(LocationService.this); int interval = SEARCH_INTERVAL; float dist = SEARCH_MIN_DISPLACEMENT; int priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; if(tRange != null && eRange != null) { if(tRange.equals(PlayerModel.ATTACK_RANGE) || eRange.equals(PlayerModel.ATTACK_RANGE)) { interval = ATTACK_INTERVAL; dist = ATTACK_MIN_DISPLACEMENT; priority = LocationRequest.PRIORITY_HIGH_ACCURACY; } else if(tRange.equals(PlayerModel.HUNT_RANGE) || eRange.equals(PlayerModel.HUNT_RANGE)) { interval = HUNT_INTERVAL; dist = HUNT_MIN_DISPLACEMENT; priority = LocationRequest.PRIORITY_HIGH_ACCURACY; } } requestLocationUpdates(interval, dist, priority); } else if(action.equals(PushNotifications.MATCH_END)) { requestLocationUpdates(10000, 5.0f, LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY); } } }; private void requestLocationUpdates(int intervalMillis, float minDistanceMeters, int priority) { //replaces previous requests for the same listener locationClient.requestLocationUpdates( new LocationRequest() .setInterval(intervalMillis) .setSmallestDisplacement(minDistanceMeters) .setPriority(priority), locationListener); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand("+intent+")"); String action = intent != null ? intent.getAction() : null; if(action != null) { if(locationClient.isConnected()) { if(action.equals(LocationService.STOP_UPDATES)) { locationClient.removeLocationUpdates(locationListener); } else if(action.equals(LocationService.START_UPDATES)) { requestLocationUpdates(10000, 5.0f, LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY); } else if(action.equals(PushNotifications.MATCH_COUNTDOWN)) { sendLocationToServer(locationClient.getLastLocation()); } Bus.register(this,intentReceiver, intentFilter); } else if (locationClient.isConnecting() && action.equals(LocationService.STOP_UPDATES)) { try { locationClient.disconnect(); } catch(Exception e) { Log.d(TAG, e.getMessage()); } } } return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { locationClient.disconnect(); Bus.unregister(this,intentReceiver); } @Override public void onCreate() { Log.d(TAG, "onCreate"); intentFilter = new IntentFilter(); intentFilter.addAction(PushNotifications.MATCH_COUNTDOWN); intentFilter.addAction(PlayerModel.TARGET_RANGE_CHANGED); intentFilter.addAction(PlayerModel.ENEMY_RANGE_CHANGED); intentFilter.addAction(PushNotifications.MATCH_END); intentFilter.addAction(PushNotifications.MATCH_START); intentFilter.addAction(User.LOGIN_COMPLETE); locationListener = new LocationListener() { public void onLocationChanged(Location location) { sendLocationToServer(location); } }; locationClient = new LocationClient(this, new ConnectionCallbacks(){ @Override public void onConnected(Bundle arg0) { Location lastLocation = locationClient.getLastLocation(); Log.d(TAG, "LocationClient connected"); if(lastLocation != null) { Log.d(TAG, "LocationClient connected, location ["+lastLocation.toString()+"]"); sendLocationToServer(lastLocation); } requestLocationUpdates(1000, 3.0f, LocationRequest.PRIORITY_HIGH_ACCURACY); } @Override public void onDisconnected() { Log.d(TAG, "LocationClient disconnected"); } }, new OnConnectionFailedListener(){ @Override public void onConnectionFailed(ConnectionResult arg0) { Log.d(TAG, "LocationClient connection failed"); locationClient.connect(); } }); locationClient.connect(); /* TODO: implement activity recognition PendingIntent that would broadcast * to stop requesting location updates when a user is still for instance * userActivityRecognitionClient = new ActivityRecognitionClient(this, new ConnectionCallbacks(){ @Override public void onConnected(Bundle arg0) { userActivityRecognitionClient.requestActivityUpdates(5000, PendingIntent); } @Override public void onDisconnected() { // TODO Auto-generated method stub } }, new OnConnectionFailedListener(){ @Override public void onConnectionFailed(ConnectionResult arg0) { Log.d(TAG, "LocationClient connection failed"); locationClient.connect(); } });*/ } @Background public void sendLocationToServer(Location l) { Log.d(TAG, "location [" + (l != null ? l.toString() : null) + "]"); if(l == null) return; Repository model = App.getRepo(); User user = model.getUser(); if(user.hasToken()) { final String regId = GCMRegistrar.getRegistrationId(this); if (regId.equals("")) { GCMRegistrar.register(this, GCMUtilities.SENDER_ID); } else { Log.v(TAG, "GCM already registered. Sending location to server."); UpdateLocationRequest msg = new UpdateLocationRequest(LocationUtils.locationToLatLng(l), user.getInstallId()); Log.v(TAG, msg.toString()); LocationResponse response = restClient.updateLocation(user.getToken(), msg); Log.i(TAG, response.toString()); if(response != null) { if(response.ok()) { Log.i(TAG,"location successfully sent to server."); user.setLocation(response.latitude, response.longitude); } else { showToastOnUiThread(response.message); } Player[] players = response.players; //even if the location response is not 'ok' (i.e. out of bounds) //there will be a player state if in an active match if(players != null) { for(Player p : players) { model.updatePlayer(p); } } } } } else { //no token yet, but still broadcast for provisional-user functionality user.setLocation(l.getLatitude(), l.getLongitude()); } } @UiThread public void showToastOnUiThread(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } @AfterInject public void afterInjection() { //subvert a bug in HttpUrlConnection //see: http://www.sapandiwakar.in/technical/eofexception-with-spring-rest-template-android/ restClient.getRestTemplate().setRequestFactory( new HttpComponentsClientHttpRequestFactory()); } @Override public IBinder onBind(Intent intent) { // TODO Auto-generated method stub return null; } }