/** * Copyright (c) 2013, Sana * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Sana nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL Sana BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.sana.android.service.impl; import java.util.List; import java.util.Locale; import org.sana.BuildConfig; import org.sana.android.activity.ProcedureRunner; import org.sana.android.content.Uris; import org.sana.android.content.core.ObservationWrapper; import org.sana.android.provider.Observations; import android.annotation.SuppressLint; import android.app.Activity; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.app.Service; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.Process; import android.support.v4.util.SparseArrayCompat; import android.text.TextUtils; import android.util.Log; /** * @author Sana Development * */ public class InstrumentationService extends Service { public static final String TAG = InstrumentationService.class.getSimpleName(); public static final String ACTION_RECORD_GPS = "org.sana.android.intent.ACTION_RECORD_GPS"; public static final int MSG_GET_LOCATION = 0; public static final int MSG_STATUS_SEND = -1; public static final int MSG_STATUS_REPLY = 0; public static final int MSG_STATUS_UNAVAILABLE = 1; /* Handler for the incoming messages */ private final class IncomingHandler extends Handler{ public IncomingHandler(Looper looper){ super(looper); } @SuppressLint("NewApi") public void handleMessage(Message msg) { Log.i(TAG, "msg obj = " + String.valueOf(msg.obj)); switch(msg.what){ case MSG_GET_LOCATION: Uri uri = Uri.parse(msg.obj.toString()); if(msg.getData() != null){ //Intent reply = msg.getData().getParcelable(Intent.EXTRA_INTENT); PendingIntent replyTo = msg.getData().getParcelable(Intent.EXTRA_INTENT); //Log.d(TAG, "replyTo: " + String.valueOf(replyTo)); LocationListener listener = getListener(null,uri,msg.arg1, msg.getData()); try{ Criteria criteria = new Criteria(); if(android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.FROYO){ criteria.setPowerRequirement(Criteria.POWER_HIGH); criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH); } else { criteria.setAccuracy(Criteria.ACCURACY_FINE); } List<String> providers = locationManager.getProviders(criteria, true); for(String provider: providers){ Log.d(TAG, "Using location provider: " + provider); locationManager.requestLocationUpdates(provider, 0, 0, listener); } //if(TextUtils.isEmpty(provider)) // throw new IllegalArgumentException("No location providers available"); // add to our listeners so that we can clean up later if necessary //replies.put(msg.arg1, replyTo); if(providers.size() == 0){ Location nullLocation = new Location(LocationManager.GPS_PROVIDER); nullLocation.setAccuracy(0); nullLocation.setLatitude(0); nullLocation.setLongitude(0); listener.onLocationChanged(nullLocation); } else { listeners.put(msg.arg1, listener); } } catch (Exception e){ Log.e(TAG, "Error getting location updates: " + e.getMessage()); e.printStackTrace(); removeListener(msg.arg1); } } else { Log.w(TAG, "no replyTo in original intent sent to InstrumentationService"); removeListener(msg.arg1); } break; default: Log.w(TAG, "Unknown message! Message = " + msg.what); removeListener(msg.arg1); } } } private LocationManager locationManager = null; private Looper mLooper; Messenger mMessenger; IncomingHandler mHandler; private SparseArrayCompat<LocationListener> listeners = new SparseArrayCompat<LocationListener>(); private SparseArrayCompat<PendingIntent> replies = new SparseArrayCompat<PendingIntent>(); /* (non-Javadoc) * @see android.app.Service#onBind(android.content.Intent) */ @Override public IBinder onBind(Intent intent) { return mMessenger.getBinder(); } /* * (non-Javadoc) * @see android.app.Service#onCreate() */ @Override public void onCreate(){ super.onCreate(); HandlerThread thread = new HandlerThread("InstrumentationService", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); // Get the HandlerThread's Looper and use it for our Handler mLooper = thread.getLooper(); mHandler = new IncomingHandler(mLooper); mMessenger = new Messenger(mHandler); locationManager = (LocationManager) getBaseContext().getSystemService( Context.LOCATION_SERVICE); } /* * (non-Javadoc) * @see android.app.Service#onDestroy() */ @Override public void onDestroy(){ int key = 0; for(int index =0; index < listeners.size(); index++){ try{ locationManager.removeUpdates(listeners.get(key)); } catch(Exception e){} } listeners.clear(); for(int index =0; index < replies.size(); index++){ try{ locationManager.removeUpdates(replies.get(index)); } catch(Exception e){} } replies.clear(); super.onDestroy(); } /* * (non-Javadoc) * @see android.app.Service#onStartCommand(android.content.Intent, int, int) */ @Override public int onStartCommand(Intent intent, int flags, int startId) { if(intent != null){ String action = intent.getAction(); Uri uri = intent.getData(); Message msg = mHandler.obtainMessage(); msg.obj = uri.toString(); msg.arg1 = startId; //Bundle data = new Bundle(); //data.putAll(intent.getExtras()); msg.setData(intent.getExtras()); if(action.equals(ACTION_RECORD_GPS)){ msg.what = MSG_GET_LOCATION; } mHandler.sendMessage(msg); } return START_STICKY; } /* returns a Location Listener which will write the data if successful */ private final LocationListener getListener(final PendingIntent replyTo, int id){ LocationListener listener = new LocationInstrumentationListener(replyTo,id); return listener; } private final LocationListener getListener(final PendingIntent replyTo, Uri uri,int id, Bundle data){ LocationListener listener = new LocationInstrumentationListener(replyTo,uri,id, data); return listener; } private final void addListener(int key, LocationListener listener){ listeners.append(key, listener); } private final void addListener(int key, PendingIntent replyTo, String provider){ LocationListener listener = new LocationInstrumentationListener(replyTo, key); listeners.put(key, listener); locationManager.requestLocationUpdates(provider, 0,0, listener); } private final void removeListener(int key){ try{ LocationListener listener = listeners.get(key); if(listener != null){ try{ locationManager.removeUpdates(listener); //mHandler.removeMessages(key, null); } catch (Exception e){ Log.e(TAG, "removeListener(): " + e); } } listeners.remove(key); PendingIntent replyTo = replies.get(key); replies.delete(key); if(replyTo != null){ try{ locationManager.removeUpdates(replyTo); } catch (Exception e){ Log.e(TAG, "removeListener(): " + e); } } } catch(Exception e) { } if(listeners.size() == 0){ stopSelf(); } } final void updateObservation(Uri uri, Object value){ Log.i(TAG, "Updating: " + uri + ", value: " + value); ContentValues vals = new ContentValues(); vals.put(Observations.Contract.VALUE, String.valueOf(value)); getContentResolver().update(uri, vals, null, null); } class LocationInstrumentationListener implements LocationListener{ final int id; final Uri uri; final PendingIntent replyTo; final Bundle bundle = new Bundle(); Location currentLocation = null; final int minAccuracy; final static int DEFAULT_MIN_ACCURACY = 16; //final Context mContext; public LocationInstrumentationListener(PendingIntent replyTo, int id){ this(replyTo,Uri.EMPTY, id,null, DEFAULT_MIN_ACCURACY); } public LocationInstrumentationListener(PendingIntent replyTo, Uri uri, int id, Bundle data){ this(replyTo, uri,id,data,DEFAULT_MIN_ACCURACY); } public LocationInstrumentationListener(PendingIntent replyTo, Uri uri, int id, Bundle data, int minAccuracy){ this.id = id; this.replyTo = replyTo; this.uri = uri; bundle.putAll(data.getBundle("extra_data")); this.minAccuracy = minAccuracy; currentLocation = null; } @Override public void onLocationChanged(Location location) { Log.i(TAG, "onLocationChanged()"); if(currentLocation == null){ currentLocation = location; } if (currentLocation.getAccuracy() > location.getAccuracy()){ Log.d(TAG, "...Location update more accurate. Swapping"); currentLocation = location; } else { Log.d(TAG, "...Location update less accurate. Not swapping"); location = currentLocation; } Log.i(TAG, "Location update." + location); float accuracy = location.getAccuracy(); Log.d(TAG, "Accuracy: " + accuracy); String locStr = "( "+location.getLatitude() +", "+ location.getLongitude()+", " + accuracy+" )"; if(!Uris.isEmpty(uri)) updateObservation(uri,locStr); if(replyTo != null){ try { Intent reply = new Intent(); reply.putExtra(Observations.Contract.VALUE,locStr); reply.putExtras(bundle); reply.putExtra(LocationManager.KEY_LOCATION_CHANGED, location); replyTo.send(InstrumentationService.this, Activity.RESULT_OK, reply); //PendingIntent.getActivity(getBaseContext(), Activity.RESULT_OK, reply, PendingIntent.FLAG_UPDATE_CURRENT).send(); //replyTo.send(); } catch (Exception e) { Log.e(TAG, "Error sending replyTo: " + e.getMessage()); e.printStackTrace(); } } else { Log.w(TAG, "No Pending Intent replyTo. Did you provide extra" + "Intent.EXTRA_INTENT in Intent sent to service?"); } // Keep listening if accuracy is > minimum acceptable if(accuracy > minAccuracy) return; removeListener(id); stopSelf(id); } @Override public void onProviderDisabled(String provider) { Log.d(TAG, "LocationListener.onProviderDisabled(String)"); removeListener(id); stopSelf(id); } @Override public void onProviderEnabled(String provider) { Log.d(TAG, "LocationListener.onProviderEnabled(String)"); removeListener(id); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { Log.d(TAG, "LocationListener.onStatusChanged(...)"); if (status == LocationProvider.AVAILABLE) { // Do nothing, we should get a location update soon which will // disable the listener. } else if (status == LocationProvider.OUT_OF_SERVICE || status == LocationProvider.TEMPORARILY_UNAVAILABLE) { removeListener(id); stopSelf(id); } } } }