Java tutorial
/* * Copyright (C) 2013 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 edu.berkeley.eecs.cfc_tracker.test.location; import android.app.Service; import android.content.Context; import android.content.Intent; import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.location.LocationServices; import edu.berkeley.eecs.cfc_tracker.location.TripDiaryStateMachineReceiver; import edu.berkeley.eecs.cfc_tracker.location.actions.GeofenceActions; import edu.berkeley.eecs.cfc_tracker.location.actions.LocationTrackingActions; /** * A Service that injects test Location objects into the Location Services back-end. All other * apps that are connected to Location Services will see the test location values instead of * real values, until the test is over. * * To use this service, define the mock location values you want to use in the class * MockLocationConstants.java, then call this Service with startService(). */ public class SendMockLocationService extends Object implements ConnectionCallbacks, OnConnectionFailedListener { /** * Convenience class for passing test parameters from the Intent received in onStartCommand() * via a Message to the Handler. The object makes it possible to pass the parameters through the * predefined Message field Message.obj. */ private class TestParam { public final String TestAction; public final int TestPause; public final int InjectionPause; public TestParam(String action, int testPause, int injectionPause) { TestAction = action; TestPause = testPause; InjectionPause = injectionPause; } } // Object that connects the app to Location Services GoogleApiClient mGoogleApiClient; Context mCachedContext; // A background thread for the work tasks HandlerThread mWorkThread; // Indicates if the test run has started private boolean mTestStarted; /* * Stores an instance of the local broadcast manager. A local * broadcast manager ensures security, because broadcast intents are * limited to the current app. */ private LocalBroadcastManager mLocalBroadcastManager; // Stores an instance of the object that dispatches work requests to the worker thread private Looper mUpdateLooper; // The Handler instance that does the actual work private UpdateHandler mUpdateHandler; // An array of test location data private TestLocation[] mLocationArray; // The time to wait before starting to inject the test locations private int mPauseInterval; // The time to wait between each test injection private int mInjectionInterval; // The type of test requested, either ACTION_START_ONCE or ACTION_START_CONTINUOUS private String mTestRequest; /** * Define a class that manages the work of injecting test locations, using the Android * Handler API. A Handler facilitates running the work on a separate thread, so that the test * loop doesn't block the UI thread. * * A Handler is an object that can run code on a thread. Handler methods allow you to associate * the object with a Looper, which dispatches Message objects to the Handler code. In turn, * Message objects contain data and instructions for the Handler's code. A Handler is * created with a default thread and default Looper, but you can inject the Looper from another * thread if you want. This is often done to associate a Handler with a HandlerThread thread * that runs in the background. */ public class UpdateHandler extends Handler { /** * Create a new Handler that uses the thread of the HandlerThread that contains the * provided Looper object. * * @param inputLooper The Looper object of a HandlerThread. */ public UpdateHandler(Looper inputLooper) { // Instantiate the Handler with a Looper connected to a background thread super(inputLooper); } /* * Do the work. The Handler's Looper dispatches a Message to handleMessage(), which then * runs the code it contains on the thread associated with the Looper. The Message object * allows external callers to pass data to handleMessage(). * * handleMessage() assumes that the location client already has a connection to Location * Services. */ @Override public void handleMessage(Message msg) { System.out.println("Handling message in injection loop"); boolean testOnce = false; // Create a new Location to inject into Location Services Location mockLocation = new Location(LocationUtils.LOCATION_PROVIDER); // Time values to put into the mock Location long elapsedTimeNanos; long currentTime; System.out.println("Getting message parameters"); // Get the parameters from the Message TestParam params = (TestParam) msg.obj; String action = params.TestAction; int pauseInterval = params.TestPause; int injectionInterval = params.InjectionPause; /* * Determine if this is a one-time run or a continuous run */ if (TextUtils.equals(action, LocationUtils.ACTION_START_ONCE)) { testOnce = true; } // If a test run is not already in progress if (!mTestStarted) { System.out.println("Test has not yet started, run it now"); // Flag that a test has started mTestStarted = true; // Start mock location mode in Location Services LocationServices.FusedLocationApi.setMockMode(mGoogleApiClient, true); /* * Wait to allow the test to switch to the app under test, by putting the thread * to sleep. try { Thread.sleep((long) (pauseInterval * 1000)); } catch (InterruptedException e) { return; } */ // Get the device uptime and the current clock time elapsedTimeNanos = SystemClock.elapsedRealtime(); currentTime = System.currentTimeMillis(); /* * Run the test loop, iterating through the array of test locations. * Each test location is injected into Location Services, after which the * thread is put to sleep for the requested interval. * * Uses a "do" loop so that one-time test and continuous test can share code. */ System.out.println("Starting test loop now..."); do { for (int index = 0; index < mLocationArray.length; index++) { /* * Set the time values for the test location. Both an elapsed system uptime * and the current clock time in UTC timezone must be specified. */ mockLocation.setElapsedRealtimeNanos(elapsedTimeNanos); mockLocation.setTime(currentTime); // Set the location accuracy, latitude, and longitude mockLocation.setAccuracy(mLocationArray[index].Accuracy); mockLocation.setLatitude(mLocationArray[index].Latitude); mockLocation.setLongitude(mLocationArray[index].Longitude); System.out.println("Sending location " + mockLocation + " to location services"); // Inject the test location into Location Services LocationServices.FusedLocationApi.setMockLocation(mGoogleApiClient, mockLocation); if (index == 0) { sendBroadcastMessage(LocationUtils.FIRST_POINT_SET, 0); } // Wait for the requested update interval, by putting the thread to sleep try { Thread.sleep((long) (injectionInterval * 1000)); } catch (InterruptedException e) { return; } /* * Change the elapsed uptime and clock time by the amount of time * requested. */ elapsedTimeNanos += (long) injectionInterval * LocationUtils.NANOSECONDS_PER_SECOND; currentTime += injectionInterval * LocationUtils.MILLISECONDS_PER_SECOND; } /* * Run the "do" while "testOnce" is false. For a one-time test, testOnce is true, * so the "do" loop runs only once. For a continuous test, testOnce is false, so the * "do" loop runs indefinitely. */ } while (!testOnce); /* * Testing is finished. */ // Turn mock mode off LocationServices.FusedLocationApi.setMockMode(mGoogleApiClient, false); // Flag that testing has stopped mTestStarted = false; // Disconnect from Location Services mGoogleApiClient.disconnect(); // Send a message back to the main activity sendBroadcastMessage(LocationUtils.CODE_TEST_FINISHED, 0); // Stop the service // stopSelf(); // If a test run is already in progress } else { /* * The Service received a request to start testing, but a test was already in * progress. Send a message back to the main Activity, and ignore the request. */ sendBroadcastMessage(LocationUtils.CODE_IN_TEST, 0); } } } /* * At startup, load the static mock location data from MockLocationConstants.java, then * create a HandlerThread to inject the locations and start it. */ // @Override public void onCreate(Context ctxt) { /* * Load the mock location data from MockLocationConstants.java */ mLocationArray = buildTestLocationArray(LocationUtils.WAYPOINTS_LAT, LocationUtils.WAYPOINTS_LNG, LocationUtils.WAYPOINTS_ACCURACY); /* * Prepare to send status updates back to the main activity. * Get a local broadcast manager instance; broadcast intents sent via this * manager are only available within the this app. */ mLocalBroadcastManager = LocalBroadcastManager.getInstance(ctxt); /* * Create a new background thread with an associated Looper that processes Message objects * from a MessageQueue. The Looper allows test Activities to send repeated requests to * inject mock locations from this Service. */ mWorkThread = new HandlerThread("UpdateThread", Process.THREAD_PRIORITY_BACKGROUND); /* * Start the thread. Nothing actually runs until the Looper for this thread dispatches a * Message to the Handler. */ mWorkThread.start(); // Get the Looper for the thread mUpdateLooper = mWorkThread.getLooper(); /* * Create a Handler object and pass in the Looper for the thread. * The Looper can now dispatch Message objects to the Handler's handleMessage() method. */ mUpdateHandler = new UpdateHandler(mUpdateLooper); // Create a location client mGoogleApiClient = new GoogleApiClient.Builder(ctxt).addConnectionCallbacks(this) .addOnConnectionFailedListener(this).addApi(LocationServices.API).build(); // Indicate that testing has not yet started mTestStarted = false; } /* * Since onBind is a static method, any subclass of Service must override it. * However, since this Service is not designed to be a bound Service, it returns null. @Override public IBinder onBind(Intent inputIntent) { return null; } */ /* * Respond to an Intent sent by startService. onCreate() is called before this method, * to take care of initialization. * * This method responds to requests from the main activity to start testing. */ // @Override public int onStartCommand(Context ctxt, Intent startIntent, int flags, int startId) { mCachedContext = ctxt; // Get the type of test to run mTestRequest = startIntent.getAction(); /* * If the incoming Intent was a request to run a one-time or continuous test */ if ((TextUtils.equals(mTestRequest, LocationUtils.ACTION_START_ONCE)) || (TextUtils.equals(mTestRequest, LocationUtils.ACTION_START_CONTINUOUS))) { // Get the pause interval and injection interval mPauseInterval = startIntent.getIntExtra(LocationUtils.EXTRA_PAUSE_VALUE, 2); mInjectionInterval = startIntent.getIntExtra(LocationUtils.EXTRA_SEND_INTERVAL, 1); // Start connecting to Location Services mGoogleApiClient.connect(); } else if (TextUtils.equals(mTestRequest, LocationUtils.ACTION_STOP_TEST)) { // Send a message back to the main activity that the test is stopping sendBroadcastMessage(LocationUtils.CODE_TEST_STOPPED, 0); // Stop this Service stopSelf(); } /* * Tell the system to keep the Service alive, but to discard the Intent that * started the Service */ return Service.START_STICKY; } /** * Build an array of test location data for later use. * * @param lat_array An array of latitude values * @param lng_array An array of longitude values * @param accuracy_array An array of accuracy values * * @return An array of test location data */ private TestLocation[] buildTestLocationArray(double[] lat_array, double[] lng_array, float[] accuracy_array) { // Temporary array of location data TestLocation[] location_array = new TestLocation[lat_array.length]; /* * Iterate through all the arrays of data. This loop assumes that the arrays * all have the same length. */ for (int index = 0; index < lat_array.length; index++) { /* * For each location, create a new location storage object. Set the "provider" * value to a number that identifies the location. This allows the tester or the * app under test to identify a particular location */ location_array[index] = new TestLocation(Integer.toString(index), lat_array[index], lng_array[index], accuracy_array[index]); } // Return the temporary array return location_array; } /* * Invoked by Location Services if a connection could not be established. */ @Override public void onConnectionFailed(ConnectionResult result) { // Send connection failure broadcast to main activity sendBroadcastMessage(LocationUtils.CODE_CONNECTION_FAILED, result.getErrorCode()); // Shut down. Testing can't continue until the problem is fixed. // stopSelf(); } /** * Send a broadcast message back to the main Activity, indicating a change in status. * * @param code1 The main status code to return * @param code2 A subcode for the status code, or 0. */ private void sendBroadcastMessage(int code1, int code2) { // Create a new Intent to send back to the main Activity Intent sendIntent = new Intent(LocationUtils.ACTION_SERVICE_MESSAGE); // Put the status codes into the Intent sendIntent.putExtra(LocationUtils.KEY_EXTRA_CODE1, code1); sendIntent.putExtra(LocationUtils.KEY_EXTRA_CODE2, code2); // Send the Intent mLocalBroadcastManager.sendBroadcast(sendIntent); } /* * When the client is connected, Location Services calls this method, which in turn * starts the testing cycle by sending a message to the Handler that injects the test locations. */ @Override public void onConnected(Bundle connectionHint) { final Context finalContext = mCachedContext; System.out.println("SendMockLocations connected"); // First, turn off any location tracking /* */ // Send message to main activity sendBroadcastMessage(LocationUtils.CODE_CONNECTED, 0); // Start injecting mock locations into Location Services // Get the HandlerThread's Looper and use it for our Handler mUpdateLooper = mWorkThread.getLooper(); mUpdateHandler = new UpdateHandler(mUpdateLooper); System.out.println("Getting a message from the global pool"); // Get a message object from the global pool Message msg = mUpdateHandler.obtainMessage(); TestParam testParams = new TestParam(mTestRequest, mPauseInterval, mInjectionInterval); msg.obj = testParams; System.out.println("Firing off injection loop"); // Fire off the injection loop mUpdateHandler.sendMessage(msg); } /* * If the client becomes disconnected without a call to LocationClient.disconnect(), Location * Services calls this method. If the test didn't finish, send a message to the main Activity. */ @Override public void onConnectionSuspended(int cause) { // It is unclear that we need to do anything here. GooglePlayServices will automatically // attempt to reconnect. Let's just notify that it is currently disconnected. // TODO: Should we also pause the thread? If so, how? // If testing didn't finish, send an error message if (mTestStarted) { sendBroadcastMessage(LocationUtils.CODE_DISCONNECTED, LocationUtils.CODE_TEST_STOPPED); } } public void stopSelf() { System.out.println("stopSelf called, but we aren't doing anything"); mUpdateLooper.quitSafely(); } public void unsetMock() { System.out.println("unsetMock called"); LocationServices.FusedLocationApi.setMockMode(mGoogleApiClient, false); } }