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.utexas.quietplaces.mocklocations; import android.annotation.TargetApi; import android.os.*; import android.os.Process; import android.util.Log; 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.LocationClient; import android.app.NotificationManager; import android.app.Service; import android.content.Intent; import android.location.Location; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; /** * 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 Service 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 LocationClient mLocationClient; // 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) { 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; // 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) { // Flag that a test has started mTestStarted = true; // Start mock location mode in Location Services mLocationClient.setMockMode(true); // Remove the notification that testing is started removeNotification(); // Add a notification that testing is in progress postNotification(getString(R.string.notification_content_test_running)); /* * 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 if (Build.VERSION.SDK_INT >= 17) { elapsedTimeNanos = SystemClock.elapsedRealtimeNanos(); } else { elapsedTimeNanos = SystemClock.elapsedRealtime() * 1000000; } 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. */ 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. */ if (Build.VERSION.SDK_INT >= 17) { 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); // Inject the test location into Location Services mLocationClient.setMockLocation(mockLocation); // 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 mLocationClient.setMockMode(false); // Flag that testing has stopped mTestStarted = false; // Clear the testing notification removeNotification(); // Disconnect from Location Services mLocationClient.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() { /* * Load the mock location data from MockLocationConstants.java */ LocationArray testData = getTestDataFromFile(); if (testData != null) { mLocationArray = buildTestLocationArray(testData.lat_list, testData.lng_list, testData.accuracy_list); } /* * 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(this); /* * 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); // Indicate that testing has not yet started mTestStarted = false; } /** * Post a notification to the notification bar. The title of the notification is fixed, but * the content comes from the input argument contentText. The notification does not contain * a content Intent, because the only destination it could have would be the main activity. * It's better to use the Recents button to go to the existing main activity. * @param contentText Text to use for the notification content (main line of expanded * notification). */ private void postNotification(String contentText) { // An instance of NotificationManager is needed to issue a notification NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); /* * Instantiate a new notification builder, using the API version that's backwards * compatible to platform version 4. */ NotificationCompat.Builder builder; // Get the notification title String contentTitle = this.getString(R.string.notification_title_test_start); // Add values to the builder builder = new NotificationCompat.Builder(this).setAutoCancel(false).setSmallIcon(R.drawable.ic_notify) .setContentTitle(contentTitle).setContentText(contentText); /* * Post the notification. All notifications from InjectMockLocationService have the same * ID, so posting a new notification overwrites the old one. */ notificationManager.notify(0, builder.build()); } /** * Remove all notifications from the notification bar. */ private void removeNotification() { // An instance of NotificationManager is needed to remove notifications NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // Remove all notifications notificationManager.cancelAll(); } /* * 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(Intent startIntent, int flags, int startId) { // 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); // Post a notification in the notification bar that a test is starting postNotification(getString(R.string.notification_content_test_start)); // Create a location client mLocationClient = new LocationClient(this, this, this); // Start connecting to Location Services mLocationClient.connect(); } else if (TextUtils.equals(mTestRequest, LocationUtils.ACTION_STOP_TEST)) { // Remove any existing notifications removeNotification(); // 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; } private static class LocationArray { public List<Double> lat_list; public List<Double> lng_list; public List<Float> accuracy_list; } private LocationArray getTestDataFromFile() { // Collect the data into lists List<Double> lat_list = new ArrayList<Double>(); List<Double> lng_list = new ArrayList<Double>(); List<Float> accuracy_list = new ArrayList<Float>(); BufferedReader reader = new BufferedReader( new InputStreamReader(getResources().openRawResource(R.raw.testdata))); String line; try { while ((line = reader.readLine()) != null) { String[] parts = line.trim().split("\\s?+,\\s?+"); Double lat = Double.parseDouble(parts[0]); Double lng = Double.parseDouble(parts[1]); Float accuracy = Float.parseFloat(parts[2]); lat_list.add(lat); lng_list.add(lng); accuracy_list.add(accuracy); } } catch (IOException e) { Log.e("getTestDataFromFile", "Unable to read test data resource.", e); return null; } // Marshal the results into an array LocationArray result = new LocationArray(); result.lat_list = lat_list; result.lng_list = lng_list; result.accuracy_list = accuracy_list; return result; } /** * Build an array of test location data for later use. * * @param lat_list An array of latitude values * @param lng_list An array of longitude values * @param accuracy_list An array of accuracy values * * @return An array of test location data */ private TestLocation[] buildTestLocationArray(List<Double> lat_list, List<Double> lng_list, List<Float> accuracy_list) { // Temporary array of location data TestLocation[] location_array = new TestLocation[lat_list.size()]; /* * Iterate through all the arrays of data. This loop assumes that the arrays * all have the same length. */ for (int index = 0; index < lat_list.size(); 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_list.get(index), lng_list.get(index), accuracy_list.get(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 arg0) { // 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); // Get a message object from the global pool Message msg = mUpdateHandler.obtainMessage(); TestParam testParams = new TestParam(mTestRequest, mPauseInterval, mInjectionInterval); msg.obj = testParams; // 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 onDisconnected() { // If testing didn't finish, send an error message if (mTestStarted) { sendBroadcastMessage(LocationUtils.CODE_DISCONNECTED, LocationUtils.CODE_TEST_STOPPED); } } }