Chapter 10: Accessing Android Hardware 3. Create a new updateVelocity method that calculates the velocity change since the last update based on the current acceleration. private void updateVelocity() { // Calculate how long this acceleration has been applied. Date timeNow = new Date(System.currentTimeMillis()); long timeDelta = timeNow.getTime()-lastUpdate.getTime(); lastUpdate.setTime(timeNow.getTime()); // Calculate the change in velocity at the // current acceleration since the last update. float deltaVelocity = appliedAcceleration * (timeDelta/1000); appliedAcceleration = currentAcceleration; // Add the velocity change to the current velocity. velocity += deltaVelocity; // Convert from meters per second to miles per hour. double mph = (Math.round(velocity / 1.6 * 3.6)); myTextView.setText(String.valueOf(mph) + “mph”); } 4. Create a new SensorListener implementation that updates the current acceleration (and derived velocity) whenever a change in acceleration is detected. Because a speedometer will most likely be used while the device is mounted vertically, with the screen face perpendicular to the ground, measure negative acceleration along the Z-axis. private final SensorListener sensorListener = new SensorListener() { double calibration - Double.NAN; public void onSensorChanged(int sensor, float[] values) { double x = values[SensorManager.DATA_X]; double y = values[SensorManager.DATA_Y]; double z = values[SensorManager.DATA_Z]; double a = -1*Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)); if (calibration == Double.NaN) calibration = a; else { updateVelocity(); currentAcceleration = (float)a; } } public void onAccuracyChanged(int sensor, int accuracy) {} }; 5. Update the onCreate method to register your new Listener for accelerometer updates using the SensorManager. Take the opportunity to get a reference to the Text View. @Override public void onCreate(Bundle icicle) { 327 10/20/08 4:10:17 PM 44712c10.indd 327 44712c10.indd 327 10/20/08 4:10:17 PM
Chapter 10: Accessing Android Hardware super.onCreate(icicle); setContentView(R.layout.main); myTextView = (TextView)findViewById(R.id.myTextView); lastUpdate = new Date(System.currentTimeMillis()); sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); sensorManager.registerListener(sensorListener, SensorManager.SENSOR_ACCELEROMETER, SensorManager.SENSOR_DELAY_FASTEST); } 6. Create a new Timer that updates the speed based on the current acceleration every second. Because this will update a GUI element, you’ll need to create a new updateGUI method that synchronizes with the GUI thread using a Handler before updating the Text View. Handler handler = new Handler(); @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); myTextView = (TextView)findViewById(R.id.myTextView); lastUpdate = new Date(System.currentTimeMillis()); sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); sensorManager.registerListener(sensorListener, SensorManager.SENSOR_ACCELEROMETER, SensorManager.SENSOR_DELAY_FASTEST); Timer updateTimer = new Timer(“velocityUpdate”); updateTimer.scheduleAtFixedRate(new TimerTask() { public void run() { updateGUI(); } }, 0, 1000); } private void updateGUI()} //Convert from m/s to mph final double mph = (Math.round(100*velocity / 1.6*3.6)) / 100; //Update the GUI handler.post(new Runnable() { public void run() { myTextView.setText(String.valueOf(mph) + “mph”); } }); } Once you’re fi nished, you’ll want to test this out. Given that keeping constant watch on your handset while, driving, cycling, or running is a Bad Idea, you should consider some further enhancements to the speedometer before you take it on the road. 328 10/20/08 4:10:17 PM 44712c10.indd 328 44712c10.indd 328 10/20/08 4:10:17 PM
Chapter 10: Accessing Android Hardware Consider incorporating vibration or media player functionality to shake or beep with intensity propor- tional to your current speed, or simply log speed changes as they happen for later review. If you’re feeling particularly adventurous, consider integrating your speedometer into a map to track your speed along a journey using different colored lines to represent your speed along the way. Determining Your Orientation The orientation sensors are a combination of a built-in compass that provides the yaw (heading) and the accelerometers that help determine pitch and roll. If you’ve done a bit of trigonometry, you’ve got the skills required to calculate the device orientation based on the accelerometer values along the three axes. If you enjoyed trig as much as I did, you’ll be happy to learn that Android does these calculations for you. The device orientation is reported along all three dimensions, as illustrated in Figure 10-2: ❑ Heading The heading (also bearing or yaw) is the direction the device is facing around the Z-axis, where 0/360 degrees is North, 90 degrees is East, 180 degrees is South, and 270 degrees is West. ❑ Pitch Pitch represents the angle of the device around the Y-axis. The tilt angle returned shows 0 degrees when the device is fl at on its back, –90 degrees when standing upright (top of device pointing at the ceiling), 90 degrees when the device is upside down, and 180/–180 degrees when the device is face down. ❑ Roll The roll represents the device’s sideways tilt between –90 and 90 degrees on the X-axis. The tilt is 0 degrees when the device is fl at on its back, –90 degrees when the screen faces left, and 90 degrees when the screen faces right. Z Heading X Roll Y Pitch Figure 10-2 329 10/20/08 4:10:17 PM 44712c10.indd 329 10/20/08 4:10:17 PM 44712c10.indd 329
Chapter 10: Accessing Android Hardware As implied by the preceding list, the Sensor Manager considers the device at rest (heading, pitch, roll at 0 degrees) when it is fl at on its back. To monitor device orientation, register a Sensor Listener with the Sensor Manager, specifying the SENSOR_ORIENTATION fl ag, as shown in the following code snippet: SensorManager sm = (SensorManager)getSystemService(Context.SENSOR_SERVICE); sm.registerListener(myOrientationListener, SensorManager.SENSOR_ORIENTATION, SensorManager.SENSOR_DELAY_NORMAL); The onSensorChanged method in your SensorListener implementation will receive a float array containing the current orientation, along the three axes described above, whenever the device’s orienta- tion changes. Within this float array, use the Sensor Manager constants DATA_X, DATA_Y, and DATA_Z to fi nd the roll, pitch, and heading (yaw) respectively. Use the corresponding RAW_DATA_* constants to fi nd the unsmoothed / unfi ltered values as shown in the following code snippet: SensorListener myOrientationListener = new SensorListener() { public void onSensorChanged(int sensor, float[] values) { if (sensor == SensorManager.SENSOR_ORIENTATION) { float rollAngle = values[SensorManager.DATA_X]; float pitchAngle = values[SensorManager.DATA_Y]; float headingAngle = values[SensorManager.DATA_Z]; float raw_rollAngle = values[SensorManager.RAW_DATA_X]; float raw_pitchAngle= values[SensorManager.RAW_DATA_Y]; float raw_headingAngle = values[SensorManager.RAW_DATA_Z]; // TODO apply the orientation changes to your application. } } public void onAccuracyChanged(int sensor, int accuracy) { } }; Creating a Compass and Artifi cial Horizon In Chapter 4, you created a simple CompassView to experiment with owner-drawn controls. In this example, you’ll extend the functionality of the CompassView to display the device pitch and roll, before using it to display the current device orientation. 1. Open the Compass project you created in Chapter 4. You will be making changes to the CompassView as well as the Compass Activity used to display it. To ensure that the View and controller remain as decoupled as possible, the CompassView won’t be linked to the sensors directly; instead, it will be updated by the Activity. Start by adding fi eld variables and get/set methods for pitch and roll to the CompassView. float pitch = 0; float roll = 0; public float getPitch() { return pitch; } 330 10/20/08 4:10:17 PM 44712c10.indd 330 44712c10.indd 330 10/20/08 4:10:17 PM
Chapter 10: Accessing Android Hardware public void setPitch(float pitch) { this.pitch = pitch; } public float getRoll() { return roll; } public void setRoll(float roll) { this.roll = roll; } 2. Update the onDraw method to include two circles that will be used to indicate the pitch and roll values. @Override protected void onDraw(Canvas canvas) { [ … Existing onDraw method … ] 2.1. Create a new circle that’s half-fi lled and rotates in line with the sideways tilt. RectF rollOval = new RectF((getMeasuredWidth()/3)-getMeasuredWidth()/7, (getMeasuredHeight()/2)-getMeasuredWidth()/7, (getMeasuredWidth()/3)+getMeasuredWidth()/7, (getMeasuredHeight()/2)+getMeasuredWidth()/7 ); markerPaint.setStyle(Paint.Style.STROKE); canvas.drawOval(rollOval, markerPaint); markerPaint.setStyle(Paint.Style.FILL); canvas.save(); canvas.rotate(roll, getMeasuredWidth()/3, getMeasuredHeight()/2); canvas.drawArc(rollOval, 0, 180, false, markerPaint); canvas.restore(); 2.2. Create a new circle that starts half-fi lled and varies between full and empty based on the current pitch. RectF pitchOval = new RectF((2*mMeasuredWidth/3)-mMeasuredWidth/7, (getMeasuredHeight()/2)-getMeasuredWidth()/7, (2*getMeasuredWidth()/3)+getMeasuredWidth()/7, (getMeasuredHeight()/2)+getMeasuredWidth()/7 ); markerPaint.setStyle(Paint.Style.STROKE); canvas.drawOval(pitchOval, markerPaint); markerPaint.setStyle(Paint.Style.FILL); canvas.drawArc(pitchOval, 0-pitch/2, 180+(pitch), false, markerPaint); markerPaint.setStyle(Paint.Style.STROKE); } That completes the changes to the CompassView. If you run the application now, it should appear as shown in Figure 10-3. This screen capture was taken while the device was facing due East, with a 45-degree roll and 10-degree backward pitch. 331 10/20/08 4:10:18 PM 44712c10.indd 331 44712c10.indd 331 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Figure 10-3 1. Now you’ll be updating the Compass Activity to use the Sensor Manager to listen for orienta- tion changes and pass them through to the CompassView. Start by adding local fi eld variables to store the current roll, pitch, and heading as well as references to the CompassView and SensorManager. float pitch = 0; float roll = 0; float heading = 0; CompassView compassView; SensorManager sensorManager; 2. Create a new updateOrientation method that takes new heading, pitch, and roll values to update the fi eld variables and apply them to the CompassView. private void updateOrientation(float _roll, float _pitch, float _heading) { heading = _heading; pitch = _pitch; roll = _roll; if (compassView!= null) { compassView.setBearing(heading); compassView.setPitch(pitch); compassView.setRoll(roll); compassView.invalidate(); } } 3. Update the onCreate method to get references to the CompassView and SensorManager, as well as initializing the heading, pitch, and roll values. @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); 332 10/20/08 4:10:18 PM 44712c10.indd 332 10/20/08 4:10:18 PM 44712c10.indd 332
Chapter 10: Accessing Android Hardware setContentView(R.layout.main); compassView = (CompassView)this.findViewById(R.id.compassView); sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); updateOrientation(0, 0, 0); } 4. Create a new fi eld variable that instantiates a new SensorListener implementation that calls the updateOrientation method. private final SensorListener sensorListener = new SensorListener() { public void onSensorChanged(int sensor, float[] values) { updateOrientation(values[SensorManager.DATA_X], values[SensorManager.DATA_Y], values[SensorManager.DATA_Z]); } public void onAccuracyChanged(int sensor, int accuracy) {} }; 5. Then override the onResume method to register the SensorListener to listen for orientation changes when the Activity is visible. Also override onStop to prevent updates when the Activ- ity has been suspended. @Override protected void onResume() { super.onResume(); sensorManager.registerListener(sensorListener, SensorManager.SENSOR_ORIENTATION, SensorManager.SENSOR_DELAY_FASTEST); } @Override protected void onStop() { sensorManager.unregisterListener(sensorListener); super.onStop(); } If you run the application now, you should see the three face dials update dynamically when the orien- tation of the device changes. Unfortunately, it’s not currently possible to emulate the sensor hardware, so this application will only update when running on supported hardware. Android Telephony The telephony APIs let your applications access the underlying telephone hardware, making it pos- sible to create your own dialer — or integrate call handling and phone state monitoring into your applications. 333 10/20/08 4:10:18 PM 44712c10.indd 333 44712c10.indd 333 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Due to security concerns, the current Android SDK does not allow you to create your own “in call” application — the screen that is displayed when an incoming call is received or an outgoing call has been initiated. Rather than creating a new dialer implementation, the following sections focus on how to monitor and control phone, service, and cell events in your applications to augment and manage the native phone- handling functionality. Making Phone Calls The best practice is to use Intents to launch a dialer application to initiate new phone calls. There are two Intent actions you can use to dial a number; in both cases, you should specify the number to dial using the tel: schema as the data component of the Intent: ❑ Intent.ACTION_CALL Automatically initiates the call, displaying the in-call application. You should only use this action if you are replacing the native dialer, otherwise you should use the ACTION_DIAL action as described below. Your application must have the CALL_PHONE permis- sion granted to broadcast this action. ❑ Intent.ACTION_DIAL Rather than dial the number immediately, this action starts a dialer application, passing in the specifi ed number but allowing the dialer application to manage the call initialization (the default dialer asks the user to explicitly initiate the call). This action doesn’t require any permissions and is the standard way applications should initiate calls. The following skeleton code shows the basic technique for dialing a number: Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(“tel:1234567”)); startActivity(intent); By using Intents to announce your intention to dial a number, your application can remain fully decou- pled from a particular hardware implementation used to initiate the call. For example, should you replace the existing dialer with a hybrid that allows IP-based telephony, using Intents to dial a number from other applications will let you leverage that new functionality without needing to change each application. Monitoring Phone State and Phone Activity The Android telephony API lets you monitor phone state, retrieve incoming phone numbers, and man- age calls. Access to the telephony APIs is managed by the Telephony Manager, accessible using the getSystemService method as shown below: String srvcName = Context.TELEPHONY_SERVICE; TelephonyManager telephonyManager = (TelephonyManager)getSystemService(srvcName); In order to monitor and manage phone state, your application will need a READ_PHONE_STATE permis- sion, as shown in the following XML code snippet: <uses-permission android:name=”android.permission.READ_PHONE_STATE”/> 334 10/20/08 4:10:18 PM 44712c10.indd 334 44712c10.indd 334 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Changes to the phone state are announced to other components using the PhoneStateListener class. Extend the PhoneStateListener to listen for, and respond to, phone state change events including call state (ringing, off hook, etc.), cell location changes, voice-mail and call-forwarding status, phone service changes, and changes in mobile signal strength. To react to phone state change events, create a new Phone State Listener implementation, and override the event handlers of the events you want to react to. Each handler receives parameters that indicate the new phone state, such as the current cell location, call state, or signal strength. The following code highlights the available state change handlers in a skeleton Phone State Listener implementation: PhoneStateListener phoneStateListener = new PhoneStateListener() { public void onCallForwardingIndicatorChanged(boolean cfi) {} public void onCallStateChanged(int state, String incomingNumber) {} public void onCellLocationChanged(CellLocation location) {} public void onDataActivity(int direction) {} public void onDataConnectionStateChanged(int state) {} public void onMessageWaitingIndicatorChanged(boolean mwi) {} public void onServiceStateChanged(ServiceState serviceState) {} public void onSignalStrengthChanged(int asu) {} }; Once you’ve created your own Phone State Listener, register it with the Telephony Manager using a bit- mask to indicate the events you want to listen for, as shown in the following code snippet: telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR | PhoneStateListener.LISTEN_CALL_STATE | PhoneStateListener.LISTEN_CELL_LOCATION | PhoneStateListener.LISTEN_DATA_ACTIVITY | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR | PhoneStateListener.LISTEN_SERVICE_STATE | PhoneStateListener.LISTEN_SIGNAL_STRENGTH); To unregister a listener, call listen and pass in PhoneStateListener.LISTEN_NONE as the bit fi eld parameter, as shown below: telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); In the following sections, you’ll learn how to use the Phone State Listener to monitor incoming calls, track cell location changes, and monitor service changes. Monitoring Phone Calls One of the most popular reasons for monitoring phone state changes is to detect, and react to, incoming and outgoing phone calls. 335 10/20/08 4:10:18 PM 44712c10.indd 335 44712c10.indd 335 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Calls can be detected through changes in the phone’s call state. Override the onCallStateChanged method in a Phone State Listener implementation, and register it as shown below to receive notifi ca- tions when the call state changes: PhoneStateListener callStateListener = new PhoneStateListener() { public void onCallStateChanged(int state, String incomingNumber) { // TODO React to incoming call. } }; telephonyManager.listen(callStateListener, PhoneStateListener.LISTEN_CALL_STATE); The onCallStateChanged handler receives the phone number associated with incoming calls, and the state parameter represents the current call state as one of the following three values: ❑ TelephonyManager.CALL_STATE_IDLE When the phone is neither ringing nor in a call ❑ TelephonyManager.CALL_STATE_RINGING When the phone is ringing ❑ TelephonyManager.CALL_STATE_OFFHOOK If the phone is currently on a call Tracking Cell Location Changes You can get notifi cations whenever the current cell location changes by overriding onCellLocationChanged on a Phone State Listener implementation. Before you can register to listen for cell location changes, you need to add the ACCESS_COARSE_LOCATION permission to your application manifest. <uses-permission android:name=”android.permission.ACCESS_COARSE_LOCATION”/> The onCellLocationChanged handler receives a CellLocation object that includes methods for extracting the cell ID (getCid) and the current LAC (getLac). The following code snippet shows how to implement a Phone State Listener to monitor cell location changes, displaying a Toast that includes the new location’s cell ID: PhoneStateListener cellLocationListener = new PhoneStateListener() { public void onCellLocationChanged(CellLocation location) { GsmCellLocation gsmLocation = (GsmCellLocation)location; Toast.makeText(getApplicationContext(), String.valueOf(gsmLocation.getCid()), Toast.LENGTH_LONG).show(); } }; telephonyManager.listen(cellLocationListener, PhoneStateListener.LISTEN_CELL_LOCATION); Tracking Service Changes The onServiceStateChanged handler tracks the service details for the device’s cell service. Use the ServiceState parameter to fi nd details of the current service state. 336 10/20/08 4:10:18 PM 44712c10.indd 336 44712c10.indd 336 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware The getState method on the Service State object returns the current service state as one of: ❑ ServiceState.STATE_IN_SERVICE Normal phone service is available. ❑ ServiceState.STATE_EMERGENCY_ONLY Phone service is available but only for emergency calls. ❑ ServiceState.STATE_OUT_OF_SERVICE No cell phone service is currently available. ❑ ServiceState.STATE_POWER_OFF The phone radio is turned off (usually when airplane mode is enabled). A series of getOperator* methods is available to retrieve details on the operator supplying the cell phone service, while getRoaming tells you if the device is currently using a roaming profi le. The following example shows how to register for service state changes and displays a Toast showing the operator name of the current phone service: PhoneStateListener serviceStateListener = new PhoneStateListener() { public void onServiceStateChanged(ServiceState serviceState) { if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { String toastText = serviceState.getOperatorAlphaLong(); Toast.makeText(getApplicationContext(), toastText, Toast.LENGTH_SHORT); } } }; telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_SERVICE_STATE); Monitoring Data Connectivity and Activity As well as voice and service details, you can monitor changes in mobile data connectivity and mobile data transfer by implementing a PhoneStateListener. The Phone State Listener includes two event handlers for monitoring the device data connection. Over- ride onDataActivity to track data transfer activity, and onDataConnectionStateChanged to request notifi cations for data connection state changes. The following skeleton code shows both handlers overridden, with switch statements demonstrating each of the possible values for the state and direction parameters passed in to each event: PhoneStateListener dataStateListener = new PhoneStateListener() { public void onDataActivity(int direction) { switch (direction) { case TelephonyManager.DATA_ACTIVITY_IN : break; case TelephonyManager.DATA_ACTIVITY_OUT : break; case TelephonyManager.DATA_ACTIVITY_INOUT : break; case TelephonyManager.DATA_ACTIVITY_NONE : break; } } public void onDataConnectionStateChanged(int state) { 337 10/20/08 4:10:18 PM 44712c10.indd 337 44712c10.indd 337 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware switch (state) { case TelephonyManager.DATA_CONNECTED : break; case TelephonyManager.DATA_CONNECTING : break; case TelephonyManager.DATA_DISCONNECTED : break; case TelephonyManager.DATA_SUSPENDED : break; } } }; telephonyManager.listen(dataStateListener, PhoneStateListener.LISTEN_DATA_ACTIVITY | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE); Accessing Phone Properties and Status The Telephony Manager also provides access to several static phone properties. You can obtain the cur- rent value of any of the phone state details described previously. The following code snippet shows how to extract the current incoming call number if the phone is ringing: String incomingCall = null; if (telephonyManager.getCallState() == TelephonyManager.CALL_STATE_RINGING) incomingCall = telephonyManager.getLine1Number(); You can also access SIM and network operator details, network information, and voice-mail details. The following code snippet shows the framework used to access the current network details: String srvcName = Context.TELEPHONY_SERVICE; TelephonyManager telephonyManager = (TelephonyManager)getSystemService(srvcName); String networkCountry = telephonyManager.getNetworkCountryIso(); String networkOperatorId = telephonyManager.getNetworkOperator(); String networkName = telephonyManager.getNetworkOperatorName(); int networkType = telephonyManager.getNetworkType(); Controlling the Phone There are times when you need access to the underlying phone hardware to effect changes rather than simply monitoring them. Access to the underlying Phone hardware was removed shortly before the release of Android SDK version 1. It is expected that basic phone interaction including answering and hanging up the phone will be available in a subsequent API release. The following section is based on an earlier API release that included phone hard- ware interaction support. It has been included to serve as a guide for likely future implementations. The Phone class in Android provides this interface, letting you control hardware settings, handle incoming calls, initiate new outgoing calls, hang up calls in progress, handle conference calls, and a variety of other Phone functionalities. 338 10/20/08 4:10:18 PM 44712c10.indd 338 44712c10.indd 338 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Replacing the basic phone functionality is a complex process. Rather than delve into this in detail, this section focuses on some of the more useful functions that can be particularly powerful when used within applications that augment rather than replace the existing functionality. To access the phone hardware, use the Phone class, available through the Telephony Manager using the getPhone method as shown in the following code snippet: Phone phone = telephonyManager.getPhone(); Once you have a reference to the Phone object, you can initiate calls using the call or dial method or end them by calling endCall. Answering, Dismissing, and Ending Calls The following code snippet shows how to use the Phone State Listener to listen for incoming calls and reject them if they’re from a particular place: final String badPrefix = “+234”; PhoneStateListener callBlockListener = new PhoneStateListener() { public void onCallStateChanged(int state, String incomingNumber) { if (state == TelephonyManager.CALL_STATE_RINGING) { Phone phone = telephonyManager.getPhone(); if (incomingNumber.startsWith(badPrefix)) { phone.endCall(); } } } }; telephonyManager.listen(callBlockListener, PhoneStateListener.LISTEN_CALL_STATE); Using Bluetooth In this section, you’ll learn how to interact directly with Bluetooth devices including other phones and Bluetooth headsets. Using Bluetooth, you can pair with other devices within range, initiate an RFCOMMSocket, and transmit and receive streams of data from or for your applications. The Bluetooth libraries have been removed for the Android version 1.0 release. The following sections are based on earlier SDK releases and are included as a guide to functionality that is expected to be made available in subsequent releases. Introducing the Bluetooth Service The Android Bluetooth service is represented by the BluetoothDevice class. 339 10/20/08 4:10:18 PM 44712c10.indd 339 44712c10.indd 339 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Bluetooth is a system service accessed using the getSystemService method. Get a reference to the BluetoothDevice by passing in the Context.BLUETOOTH constant as the service name parameter, as shown in the following code snippet: String context = Context.BLUETOOTH_SERVICE; final BluetoothDevice bluetooth = (BluetoothDevice)getSystemService(context); To use the Bluetooth Service, your application needs to have the BLUETOOTH permission as shown here: <uses-permission android:name=”android.permission.BLUETOOTH”/> Controlling the Local Bluetooth Device The Bluetooth Device offers several methods that let you control the Bluetooth hardware. The enable and disable methods let you enable or disable the Bluetooth adapter. The getName and setName methods let you modify the local device name, and getAddress can be used to determine the local device address. You can fi nd and change the discovery mode and discovery time-out settings using the getMode and getDiscoverableTimeout methods and their setter equivalents. The following code snippet enables the Bluetooth adapter and waits until it has connected before changing the device name and setting the mode to “discoverable”: bluetooth.enable(new IBluetoothDeviceCallback.Stub() { public void onCreateBondingResult(String _address, int _result) throws RemoteException { String friendlyName = bluetooth.getRemoteName(_address); } public void onEnableResult(int _result) throws RemoteException { if (_result == BluetoothDevice.RESULT_SUCCESS) { bluetooth.setName(“BLACKFANG”); bluetooth.setMode(BluetoothDevice.MODE_DISCOVERABLE); } } }); Discovering and Bonding with Bluetooth Devices Before you can establish a data communications socket, the local Bluetooth device must fi rst discover, connect, and bond with the remote device. Discovery Looking for remote devices to connect to is called discovery. For other devices to discover your handset, you need to set the mode to “discoverable” or “connectable” using the setMode method as shown previously. 340 10/20/08 4:10:18 PM 44712c10.indd 340 10/20/08 4:10:18 PM 44712c10.indd 340
Chapter 10: Accessing Android Hardware To discover other devices, initiate a discovery session using the startDiscovery or startPeriodicDiscovery methods, as shown below: if (discoverPeriodically) bluetooth.startPeriodicDiscovery(); else bluetooth.startDiscovery(true); Both of these calls are asynchronous, broadcasting a REMOTE_DEVICE_FOUND_ACTION whenever a new remote Bluetooth device is discovered. To get a list of the remote devices that have been discovered, call listRemoteDevices on the Bluetooth device object. The returned String array contains the address of each remote device found. You can fi nd their “friendly” names by passing in the device address to getRemoteName. Bonding Bonding, also known as pairing, lets you create an authenticated connection between two Bluetooth devices using a four-digit PIN. This ensures that your Bluetooth connections aren’t hijacked. Android requires you to bond with remote devices before you can establish application-layer communi- cation sessions such as RFCOMM. To bond with a remote device, call the createBonding method on the Bluetooth Device after using setPin to set the unique identifi er PIN to use for this pairing request. To abort the bonding attempt, call cancelBonding and use cancelPin to re-set the PIN if required. Once a remote device has been bonded, it will be added to the native database and will automatically bond with the local device if it is discovered in the future. In the following code snippet, the Bluetooth Service is queried for a list of all the available remote devices. The list is then checked to see if any of these devices are not yet bonded with the local Bluetooth service, in which case, pairing is initiated. String[] devices = bluetooth.listRemoteDevices(); for (String device : devices) { if (!bluetooth.hasBonding(device)) { // Set the pairing PIN. In real life it’s probably a smart // move to make this user enterable and dynamic. bluetooth.setPin(device, new byte[] {1,2,1,2}); bluetooth.createBonding(device, new IBluetoothDeviceCallback.Stub() { public void onCreateBondingResult(String _address, int _result) throws RemoteException { if (_result == BluetoothDevice.RESULT_SUCCESS) { String connectText = “Connected to “ + bluetooth.getRemoteName(_address); Toast.makeText(getApplicationContext(), connectText, Toast.LENGTH_SHORT); } 341 10/20/08 4:10:18 PM 44712c10.indd 341 44712c10.indd 341 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware } public void onEnableResult(int _result) throws RemoteException {} }); } } To listen for remote-device bonding requests, implement and register a BroadcastListener that fi lters for the ACTION_PAIRING_REQUEST Intent. Managing Bluetooth Connections Calling listRemoteDevices returns a list of the currently discovered devices, while listBondings returns the address of each remote device currently bonded to the local device. As a shortcut, hasBonding lets you specify a device address and returns true if you have bonded with it. Further details on each device can be found using the lastSeen and lastUsed methods. These meth- ods return the last time a device was seen (through discovery) or accessed. Use removeBonding to sever a bond with a remote device. This will also close any application layer communications sockets you’ve established. Communication with Bluetooth The most likely reason for bonding to a remote Bluetooth device is to communicate with it. Bluetooth data transfer is handled using the RfcommSocket class, which provides a wrapper for the Bluetooth radiofrequency communications (RFCOMM) protocol that supports RS232 serial communica- tion over an underlying Logical Link Control and Adaptation Protocol (L2CAP) layer. In practice, this alphabet soup provides a mechanism for opening communication sockets between two paired Bluetooth devices. In order for an RFCOMM communication channel to be established, a listening port on one device must be connected to an outgoing port on the other. As a result, for bidirectional communication, two socket connections must be established. Opening a Socket Connection Before you can transfer data between Bluetooth devices, you need to open a new RFCommSocket. Start by creating a new RFCommSocket object and calling its create method. This constructs a new socket for you to use on your Bluetooth device, returning a FileDescriptor for transferring data. The FileDescriptor is the lowest-level representation of an I/O source. You can create any of the I/O class objects (FileStream, DataOutputStream, etc.) using a FileDescriptor as a constructor param- eter. Later you’ll use one of these I/O classes to transfer data with a remote device. 342 10/20/08 4:10:18 PM 44712c10.indd 342 10/20/08 4:10:18 PM 44712c10.indd 342
Chapter 10: Accessing Android Hardware The following skeleton code shows the basic RFCommSocket implementation that creates a new socket connection ready to either initiate or respond to communications requests: FileDescriptor localFile; RfcommSocket localSocket = new RfcommSocket(); try { localFile = localSocket.create(); } catch (IOException e) { } Once the socket has been created, you need to either bind it to the local device to listen for connection requests or initiate a connection with a remote device. Listening for Data To listen for incoming data, use the bind method to create a socket to use as a listening port for the local device. If this is successful, use the listen method to open the socket to start listening for incom- ing data transfer requests. Once you’ve initialized your listener socket, use the accept method to check for, and respond to, any incoming socket connection requests. The accept method takes a new RfcommSocket object that will be used to represent the remote socket connection, and a time-out for a connection request to be received. If a successful connection is made, accept returns a FileDescriptor that represents the input stream. Use this File Descriptor to process the incoming data stream from the remote device. The following skeleton code shows how to confi gure a new socket that listens for, and accepts, an incoming socket connection: FileDescriptor localFile; FileDescriptor remoteFile; RfcommSocket localSocket = new RfcommSocket(); try { localFile = localSocket.create(); localSocket.bind(null); localSocket.listen(1); RfcommSocket remotesocket = new RfcommSocket(); remoteFile = localSocket.accept(remotesocket, 10000); } catch (IOException e) { } If no connection request is made within the time-out period, accept returns null. Transmitting Data To transmit data using an RfcommSocket, use the connect method to specify a bonded remote device address and port number to transmit data to. The connection request can also be made asynchronously using the connectAsync method to initiate the connection; waitForAsyncConnect can then be used to block on a separate thread until a response is received. 343 10/20/08 4:10:18 PM 44712c10.indd 343 44712c10.indd 343 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Once a connection has been established, you can transmit data with any of the I/O output classes using the local socket’s FileDescriptor as a constructor parameter, as shown in the following code snippet: FileDescriptor localFile; String remoteAddress = bluetooth.listBondings()[0]; RfcommSocket localSocket = new RfcommSocket(); try { localFile = localSocket.create(); // Select an unused port if (localSocket.connect(remoteAddress, 0)) { FileWriter output = new FileWriter(localFile); output.write(“Hello, Android”); output.close(); } } catch (IOException e) { } Using a Bluetooth Headset Wireless headsets are one of the most common uses of Bluetooth on mobile phones. The BluetoothHeadset class provides specialized support for interacting with Bluetooth headsets. In this context, a headset includes any headset or hands-free device. To use the Bluetooth headset API, create a new BluetoothHeadset object on your application context, as shown in the code snippet below: BluetoothHeadset headset = new BluetoothHeadset(this); This object will act as a proxy to the Bluetooth Headset Service that services any Bluetooth headsets bonded with the system. Android only supports a single headset connection at a time, but you can change the connected headset using this API. Call connectHeadset, passing in the address of the headset to connect to, as shown in the code snippet below: headset.connectHeadset(address, new IBluetoothHeadsetCallback.Stub() { public void onConnectHeadsetResult(String _address, int _resultCode) throws RemoteException { if (_resultCode == BluetoothHeadset.RESULT_SUCCESS) { // Connected to a new headset device. } } }); 344 10/20/08 4:10:18 PM 44712c10.indd 344 44712c10.indd 344 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware The Headset Service is not guaranteed to be connected to a headset at all times, so it’s good practice to use the getState method to confi rm a valid connection before performing any actions, as shown in the code snippet below: if (headset.getState() == BluetoothHeadset.STATE_CONNECTED) { // TODO Perform actions on headset. } When you’ve fi nished interacting with the headset, you should always call close on the BluetoothHeadset object to let the proxy unbind from the underlying service: BluetoothHeadset headset = new BluetoothHeadset(this); // [ … Perform headset actions … ] headset.close(); Managing Network and Wi-Fi Connections The incredible growth of Internet services and the ubiquity of mobile devices has made mobile Internet access an increasingly prevalent feature on mobile phones. With the speed, reliability, and cost of Internet connectivity dependent on the network technology being used (Wi-Fi, GPRS, 3G), letting your applications know and manage these connections can help to ensure that they run effi ciently and responsively. Android provides access to the underlying network state, broadcasting Intents to notify applica- tion components of changes in network connectivity and offering control over network settings and connections. Android networking is principally handled using the ConnectivityManager, a Service that lets you monitor the connectivity state, set your preferred network connection, and manage connectivity failover. Later you’ll learn how to use the WifiManager to monitor and control the device’s Wi-Fi connectivity specifi cally. The Wi-Fi Manager lets you see and modify the confi gured Wi-Fi networks, manage the active connection, and perform access point scans. Monitoring and Managing Your Internet Connectivity The ConnectivityManager represents the Network Connectivity Service. It’s used to monitor the state of network connections, confi gure failover settings, and control the network radios. To access the Connectivity Manager, call getSystemService, passing in Context.CONNECTIVITY_SERVICE as the service name, as shown in the code snippet below: String service = Context.CONNECTIVITY_SERVICE; ConnectivityManager connectivity = (ConnectivityManager)getSystemService(service); 345 10/20/08 4:10:18 PM 44712c10.indd 345 44712c10.indd 345 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Before it can use the Connectivity Manager, your application will need Read and Write network state access permissions to be added to the manifest, as shown below: <uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE”/> <uses-permission android:name=”android.permission.CHANGE_NETWORK_STATE”/> Managing Active Connections The Connectivity Manager provides a high-level view of the available network connections. Using the getActiveNetworkInfo or getNetworkInfo methods, you query NetworkInfo objects that include details on the currently active network or on an inactive network of the type specifi ed. In both cases, the NetworkInfo returned includes methods that indicate the connection status and net- work type of the specifi ed network. Confi guring Network Preferences and Controlling Hardware The Connectivity Manager can also be used to control network hardware and confi gure failover preferences. Android will attempt to connect to the preferred network whenever an authorized application requests an Internet connection. You can set the preferred network using the setNetworkPreference method and specifying the network you would prefer to connect to, as shown in the code snippet below: connectivity.setNetworkPreference(NetworkPreference.PREFER_WIFI); If the preferred connection is unavailable, or connectivity on this network is lost, Android will auto- matically attempt to connect to the secondary network. You can control the availability of the network types, using the setRadio method. This method lets you set the state of the radio associated with a particular network (Wi-Fi, mobile, etc.). For example, in the following code snippet, the Wi-Fi radio is turned off and the mobile radio is turned on: connectivity.setRadio(NetworkType.WIFI, false); connectivity.setRadio(NetworkType.MOBILE, true); Monitoring Network Connectivity One of the most useful functions of the Connectivity Manager is to notify applications of changes in network connectivity. To monitor network connectivity, create your own Broadcast Receiver implementation that listens for ConnectivityManager.CONNECTIVITY_ACTION Intents. Such Intents include several extras that pro- vide additional details on the change to the connectivity state: ❑ ConnectivityManager.EXTRA_IS_FAILOVER Is a Boolean that returns true if the current connection is the result of a failover from a preferred network. ❑ ConnectivityManager.EXTRA_NO_CONNECTIVITY Is a Boolean that returns true if the device is not connected to any network. 346 10/20/08 4:10:18 PM 44712c10.indd 346 10/20/08 4:10:18 PM 44712c10.indd 346
Chapter 10: Accessing Android Hardware ❑ ConnectivityManager.EXTRA_REASON If this broadcast represents a connection failure, this string value includes a description of why the connection attempt failed. ❑ ConnectivityManager.EXTRA_NETWORK_INFO This returns a NetworkInfo object contain- ing more fi ne-grained details on the network associated with the current connectivity event. ❑ ConnectivityManager.EXTRA_OTHER_NETWORK_INFO After a network disconnection, this value will return a NetworkInfo object populated with the details for the possible failover net- work connection. ❑ ConnectivityManager.EXTRA_EXTRA_INFO Contains additional network-specifi c extra con- nection details. Android SDK beta 0.9 included a NetworkConnectivityListener that encapsulated this functionality. This class has been removed for version 1.0. Managing Your Wi-Fi The WifiManager represents the Android Wi-Fi Connectivity Service. It can be used to confi gure Wi-Fi network connections, manage the current Wi-Fi connection, scan for access points, and monitor changes in Wi-Fi connectivity. As with the Connectivity Manager, access the Wi-Fi Manager using the getSystemService method, passing in the Context.WIFI_SERVICE constant, as shown in the following code snippet: String service = Context.WIFI_SERVICE; final WifiManager wifi = (WifiManager)getSystemService(service); To use the Wi-Fi Manager, your application must have uses-permissions for Read/Write Wi-Fi state access included in its manifest. <uses-permission android:name=”android.permission.ACCESS_WIFI_STATE”/> <uses-permission android:name=”android.permission.CHANGE_WIFI_STATE”/> You can use the Wi-Fi Manager to enable or disable your Wi-Fi hardware using the setWifiEnabled method, or request the current Wi-Fi state using the getWifiState or isWifiEnabled methods as shown in the code snippet below: if (!wifi.isWifiEnabled()) if (wifi.getWifiState() != WifiManager.WIFI_STATE_ENABLING) wifi.setWifiEnabled(true); The following sections begin with tracking the current Wi-Fi connection status and monitoring changes in signal strength. Later you’ll also learn how to scan for and connect to specifi c access points. While these functions may be suffi cient for most developers, the WifiManager also provides low-level access to the Wi-Fi network confi gurations, giving you full control over each confi guration setting and allowing you to completely replace the native Wi-Fi management application. Later in the section, you’ll be introduced to the API used to create, delete, and modify network confi gurations. 347 10/20/08 4:10:18 PM 44712c10.indd 347 44712c10.indd 347 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Monitoring Wi-Fi Connectivity The Wi-Fi Manager broadcasts Intents whenever the connectivity status of the Wi-Fi network changes, using the following actions: ❑ WifiManager.WIFI_STATE_CHANGED_ACTION Indicates that the Wi-Fi hardware status has changed, moving between enabling, enabled, disabling, disabled, and unknown. It includes two extra values keyed on EXTRA_WIFI_STATE and EXTRA_PREVIOUS_STATE that provide the pre- vious and new Wi-Fi states. ❑ WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION This Intent is broadcast when- ever the connection state with the active supplicant (access point) changes. It is fi red when a new connection is established or an existing connection is lost using the EXTRA_NEW_STATE Boolean extra that returns true in the former case. ❑ WifiManager.NETWORK_STATE_CHANGED_ACTION The network state change broadcast is fi red whenever the Wi-Fi connectivity state changes. This Intent includes two extras — the fi rst EXTRA_NETWORK_INFO includes a NetworkInfo object that details the current network state, while the second EXTRA_BSSID includes the BSSID of the access point you’re connected to. ❑ WifiManager.RSSI_CHANGED_ACTION You can monitor the current signal strength of the connected Wi-Fi network by listening for the RSSI_CHANGED_ACTION Intent. This Broadcast Intent includes an integer extra, EXTRA_NEW_RSSI, that holds the current signal strength. To use this signal strength, you should use the calculateSignalLevel static method on the Wi-Fi Manager to convert it to an integer value on a scale you specify. Creating and Managing Wi-Fi Connections and Confi gurations You can use the Wi-Fi Manager to manage the confi gured network settings and control which networks to connect to. Once connected, you can interrogate the active network connection to get additional details of its confi guration and settings. Get a list of the current network confi gurations using getConfiguredNetworks. The list of Wifi Configuration objects returned includes the network ID, SSID, and other details for each confi guration. To use a particular network confi guration, use the enableNetwork method, passing in the network ID to use and specifying true for the disableAllOthers parameter as shown below: // Get a list of available configurations List<WifiConfiguration> configurations = wifi.getConfiguredNetworks(); // Get the network ID for the first one. if (configurations.size() > 0) { int netID = configurations.get(0).networkId; // Enable that network. boolean disableAllOthers = true; wifi.enableNetwork(netID, disableAllOtherstrue); } Once an active network connection has been established, use the getConnectionInfo method to return information on the active connection’s status. The returned WifiInfo object includes the BSSID, Mac address, and IP address of the current access point, as well as the current link speed and signal strength. 348 10/20/08 4:10:18 PM 44712c10.indd 348 10/20/08 4:10:18 PM 44712c10.indd 348
Chapter 10: Accessing Android Hardware The following code snippet queries the currently active Wi-Fi connection and displays a Toast showing the connection speed and signal strength: WifiInfo info = wifi.getConnectionInfo(); if (info.getBSSID() != null) { int strength = WifiManager.calculateSignalLevel(info.getRssi(), 5); int speed = info.getLinkSpeed(); String units = WifiInfo.LINK_SPEED_UNITS; String ssid = info.getSSID(); String toastText = String.format(“Connected to {0} at {1}{2}. Strength {3}/5”, ssid, speed, units, strength); Toast.makeText(this, toastText, Toast.LENGTH_LONG); } Scanning for Hotspots You can use the Wi-Fi Manager to conduct access point scans using the startScan method. An Intent with the SCAN_RESULTS_AVAILABLE_ACTION action will be broadcast to asynchronously announce that the scan is complete and results are available. Call getScanResults to get those results as a list of ScanResult objects. Each ScanResult includes the details retrieved for each access point detected, including link speed, signal strength, SSID, and the authentication techniques supported. The following skeleton code shows how to initiate a scan for access points that displays a Toast indicat- ing the total number of access points found and the name of the Access Point with the strongest signal: // Register a broadcast receiver that listens for scan results. registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { List<ScanResult> results = wifi.getScanResults(); ScanResult bestSignal = null; for (ScanResult result : results) { if (bestSignal == null || WifiManager.compareSignalLevel(bestSignal.level, result.level) < 0) bestSignal = result; } String toastText = String.format(“{0} networks found. {1} is the strongest.”, results.size(), bestSignal.SSID); Toast.makeText(getApplicationContext(), toastText, Toast.LENGTH_LONG); } }, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); // Initiate a scan. wifi.startScan(); 349 10/20/08 4:10:18 PM 44712c10.indd 349 44712c10.indd 349 10/20/08 4:10:18 PM
Chapter 10: Accessing Android Hardware Managing Wi-Fi Network Confi gurations To connect to a Wi-Fi network, you need to create and register a confi guration. Normally your users would do this using the native Wi-Fi confi guration settings, but there’s no reason you can’t expose the same functionality within your own applications, or for that matter replace the native Wi-Fi confi gura- tion Activity entirely. Network confi gurations are stored as WifiConfiguration objects. The following is a non-exhaustive list of some of the public fi elds available for each Wi-Fi Confi guration: ❑ BSSID Specifi es the BSSID for an access point. ❑ SSID The SSID for a particular network ❑ networkId A unique identifi er used to identify this network confi guration on the current device ❑ priority The priority of each network confi guration when choosing which of several access points to connect to ❑ status The current status of this network connection, will be one of WifiConfiguration.Status.ENABLED, WifiConfiguration.Status.DISABLED, or WifiConfiguration.Status.CURRENT The confi guration object also contains the supported authentication technique, as well as the keys used previously to authenticate with this access point. The addNetwork method lets you specify a new confi guration to add to the current list; similarly, updateNetwork lets you update a network confi guration by passing in a WifiConfiguration that’s sparsely populated with a network ID and the values you want to change. You can also use removeNetwork, passing in a network ID, to remove a confi guration. To persist any changes made to the network confi gurations, you must call saveConfiguration. Controlling Device Vibration In Chapter 8, you learned how to create Notifi cations that can trigger vibration to provide additional user feedback when signaling events. In some circumstances, you may wish to vibrate the device inde- pendently of Notifi cations. Vibrating the device is an excellent way to provide haptic user feedback and is particularly popular as a feedback mechanism for games. To control device vibration, your applications need the VIBRATE permission. Add this to your applica- tion manifest using the code snippet below: <uses-permission android:name=”android.permission.VIBRATE”/> Device vibration is controlled through the Vibrator class, accessible using the getSystemService method, as shown in the following code snippet: String vibratorService = Context.VIBRATOR_SERVICE; Vibrator vibrator = (Vibrator)getSystemService(vibratorService); 350 10/20/08 4:10:18 PM 44712c10.indd 350 10/20/08 4:10:18 PM 44712c10.indd 350
Chapter 10: Accessing Android Hardware Call vibrate to start device vibration; you can pass in either a vibration duration or pattern of alter- nating vibration/pause sequences along with an optional index parameter that will repeat the pattern starting at the index specifi ed. Both techniques are demonstrated below: long[] pattern = {1000, 2000, 4000, 8000, 16000 }; vibrator.vibrate(pattern, 0); vibrator.vibrate(1000); // Vibrate for 1 second To cancel vibration, you can call cancel. Alternatively, exiting your application will automatically can- cel any vibration it has initiated. Summary In this chapter, you learned how to monitor and control some of the hardware services available on Android devices. Beginning with the media APIs, you learned about Android’s multimedia capabilities including play- back and recording using the Media Player and Media Recorder classes. You were introduced to the Camera APIs that can be used to change camera settings, initiate live cam- era previews, and take photos. With the Sensor Manager, you used the accelerometer and compass hardware to determine the device’s orientation and acceleration, as well as monitoring and interpreting changes to detect device movement. Finally, you examined the underlying communications hardware APIs available in Android. This included an introduction to the telephony APIs and an overview of the Bluetooth, network, and Wi-Fi managers for monitoring and controlling device connectivity. This chapter also included: ❑ Monitoring phone state information, including cell location, phone state, and service state. ❑ Controlling the Bluetooth device to discover, bond, and transmit information between local and remote Bluetooth devices. ❑ Managing Bluetooth headsets. ❑ Managing Wi-Fi confi gurations, searching for access points, and managing Wi-Fi connections. ❑ Controlling device vibration to provide haptic feedback. In the fi nal chapter, you’ll be introduced to some of the advanced Android features. You’ll learn more about security and how to use AIDL to facilitate interprocess communication. You’ll learn about Android’s User Interface and graphics capabilities by exploring animations and advanced Canvas drawing techniques. Finally, you’ll be introduced to the SurfaceView and touch-screen input functionality. 351 10/20/08 4:10:18 PM 44712c10.indd 351 44712c10.indd 351 10/20/08 4:10:18 PM
10/20/08 4:10:18 PM 44712c10.indd 352 44712c10.indd 352 10/20/08 4:10:18 PM
Advanced Android Development In this chapter, you’ll be returning to some of the possibilities touched on in previous chapters and explore some of the topics that deserve more attention. In the fi rst six chapters, you learned the fundamentals of creating mobile applications for Android devices. In Chapters 7 through 10, you were introduced to some of the more powerful optional APIs, including location-based services, maps, instant messaging, and hardware monitoring and control. You’ll start this chapter by taking a closer look at security, in particular, how permissions work and how to use them to secure your own applications. Next you’ll examine the Android Interface Defi nition Language (AIDL) and learn how to cre- ate rich application interfaces that support full object-based interprocess communication (IPC) between Android applications running in different processes. You’ll then take a closer look at the rich toolkit available for creating User Interfaces for your Activities. Starting with animations, you’ll learn how to apply tweened animations to Views and View Groups, and construct frame-by-frame cell-based animations. Next is an in-depth examination of the possibilities available with Android’s raster graphics engine. You’ll be introduced to the drawing primitives available before learning some of the more advanced possibilities available with Paint. You’ll learn how to use transparency and create gra- dient Shaders and bitmap brushes. You’ll be introduced to mask and color fi lters, as well as Path Effects and the possibilities of using different transfer modes. You’ll then delve a little deeper into the design and execution of more complex User Interface Views, learning how to create three-dimensional and high frame-rate interactive controls using the Surface View, and how to use the touch screen, trackball, and device keys to create intuitive input possibilities for your UIs. 10/20/08 4:09:56 PM 44712c11.indd 353 10/20/08 4:09:56 PM 44712c11.indd 353
Chapter 11: Advanced Android Development Paranoid Android Much of Android’s security is native to the underlying Linux kernel. Resources are sandboxed to their owner applications, making them inaccessible from other applications. Android provides broadcast Intents, Services, and Content Providers to let you relax these strict process boundaries, using the per- mission mechanism to maintain application-level security. You’ve already used the permission system to request access to native system services — notably the location-based services and contacts Content Provider — for your applications. The following sections provide a more detailed look at the security available. For a comprehensive view, the Android documentation provides an excellent resource that describes the security features in depth at code.google.com/android/devel/security.html. Linux Kernel Security Each Android package has a unique Linux userID assigned to it during installation. This has the effect of sandboxing the process and the resources it creates, so that it can’t affect (or be affected by) other applications. Because of this kernel-level security, you need to take additional steps to communicate between appli- cations. Enter Content Providers, broadcast Intents, and AIDL interfaces. Each of these mechanisms opens a tunnel for information to fl ow between applications. Android permissions act as border guards at either end to control the traffi c allowed through these tunnels. Introducing Permissions Permissions are an application-level security mechanism that lets you restrict access to application com- ponents. Permissions are used to prevent malicious applications from corrupting data, gaining access to sensitive information, or making excessive (or unauthorized) use of hardware resources or external communication channels. As you’ve learned in earlier chapters, many of Android’s native components have permission require- ments. The native permission strings used by native Android Activities and Services can be found as static constants in the android.Manifest.permission class. To use permission-protected components, you need to add uses-permission tags to application mani- fests, specifying the permission string that each application requires. When an application package is installed, the permissions requested in its manifest are analyzed and granted (or denied) by checks with trusted authorities and user feedback. Unlike many existing mobile platforms, all Android permission checks are done at installation. Once an application is installed, the user will not be prompted to reevaluate those permissions. There’s no guarantee that your application will be granted the permissions it requests, so it’s good prac- tice to write defensive code that ensures it fails gracefully in these circumstances. 354 10/20/08 4:09:57 PM 44712c11.indd 354 10/20/08 4:09:57 PM 44712c11.indd 354
Chapter 11: Advanced Android Development Declaring and Enforcing Permissions Before you can assign a permission to an application component, you need to defi ne it within your manifest using the permission tag as shown in the following code snippet: <permission android:name=”com.paad.DETONATE_DEVICE” android:protectionLevel=”dangerous” android:label=”Self Destruct” android:description=”@string/detonate_description”> </permission> Within the permission tag, you can specify the level of access that the permission will permit (normal, dangerous, signature, signatureOrSystem), a label, and an external resource containing the description that explains the risks of granting this permission. To include permission requirements for your own application components, use the permission attri- bute in the application manifest. Permission constraints can be enforced throughout your application, most usefully at application interface boundaries, for example: ❑ Activities Add a permission to limit the ability of other applications to launch an Activity. ❑ Broadcast Receivers Control which applications can send broadcast Intents to your receiver. ❑ Content Providers Limit Read access and Write operations on Content Providers. ❑ Services Limit the ability of other applications to start, or bind to, a Service. In each case, you can add a permission attribute to the application component in the manifest, specify- ing a required permission string to access each component, as shown below in a manifest excerpt that shows a permission requirement for an Activity: <activity android:name=”.MyActivity” android:label=”@string/app_name” android:permission=”com.paad.DETONATE_DEVICE”> </activity> Content Providers let you set readPermission and writePermission attributes to offer a more granu- lar control over Read/Write access. Enforcing Permissions with Broadcasting Intents As well as requiring permissions for Intents to be received by your Broadcast Receivers, you can also attach a permission string to each Intent you broadcast. When calling sendIntent, you can supply a permission string required by Broadcast Receivers before they can receive the Intent. This process is shown below: sendBroadcast(myIntent, REQUIRED_PERMISSION); 355 10/20/08 4:09:57 PM 44712c11.indd 355 44712c11.indd 355 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development Using AIDL to Support IPC for Services One of the more interesting possibilities of Services is the idea of running independent background processes to supply processing, data lookup, or other useful functionality to multiple independent applications. In Chapter 8, you learned how to create Services for your applications. Here, you’ll learn how to use the Android Interface Defi nition Language (AIDL) to support interprocess communication (IPC) between Services and application components. This will give your Services the ability to support multiple appli- cations across process boundaries. To pass objects between processes, you need to deconstruct them into OS-level primitives that the underlying operating system (OS) can then marshal across application boundaries. AIDL is used to simplify the code that lets your processes exchange objects. It’s similar to interfaces like COM or Corba in that it lets you create public methods within your Services that can accept and return object parameters and return values between processes. Implementing an AIDL Interface AIDL supports the following data types: ❑ Java language primitives (int, boolean, float, char, etc.) ❑ String and CharSequence values ❑ List (including generic) objects, where each element is a supported type. The receiving class will always receive the List object instantiated as an ArrayList. ❑ Map (not including generic) objects in which each key and element is a supported type. The receiving class will always receive the Map object instantiated as a HashMap. ❑ AIDL-generated interfaces (covered later). An import statement is always needed for these. ❑ Classes that implement the Parcelable interface (covered next). An import statement is always needed for these. The following sections demonstrate how to make your application classes AIDL-compatible by imple- menting the Parcelable interface, before creating an AIDL interface defi nition and implementing that interface within your Service. Passing Custom Class Objects To pass non-native objects between processes, they must implement the Parcelable interface. This lets you decompose your objects into primitive types stored within a Parcel that can be marshaled across process boundaries. Implement the writeToParcel method to decompose your class object, then implement the public static Creator fi eld (which implements a new Parcelable.Creator class), which will create a new object based on an incoming Parcel. 356 10/20/08 4:09:57 PM 44712c11.indd 356 10/20/08 4:09:57 PM 44712c11.indd 356
Chapter 11: Advanced Android Development The following code snippet shows a basic example of using the Parcelable interface for the Quake class you’ve been using in the ongoing Earthquake example: package com.paad.earthquake; import java.util.Date; import android.location.Location; import android.os.Parcel; import android.os.Parcelable; public class Quake implements Parcelable { private Date date; private String details; private Location location; private double magnitude; private String link; public Date getDate() { return date; } public String getDetails() { return details; } public Location getLocation() { return location; } public double getMagnitude() { return magnitude; } public String getLink() { return link; } public Quake(Date _d, String _det, Location _loc, double _mag, String _link) { date = _d; details = _det; location = _loc; magnitude = _mag; link = _link; } @Override public String toString(){ SimpleDateFormat sdf = new SimpleDateFormat(“HH.mm”); String dateString = sdf.format(date); return dateString + “:” + magnitude + “ “ + details; } private Quake(Parcel in) { date.setTime(in.readLong()); details = in.readString(); magnitude = in.readDouble(); Location location = new Location(“gps”); location.setLatitude(in.readDouble()); location.setLongitude(in.readDouble()); link= in.readString(); } public void writeToParcel(Parcel out, int flags) { out.writeLong(date.getTime()); out.writeString(details); out.writeDouble(magnitude); 357 10/20/08 4:09:57 PM 44712c11.indd 357 44712c11.indd 357 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development out.writeDouble(location.getLatitude()); out.writeDouble(location.getLongitude()); out.writeString(link); } public static final Parcelable.Creator<Quake> CREATOR = new Parcelable.Creator<Quake>() { public Quake createFromParcel(Parcel in) { return new Quake(in); } public Quake[] newArray(int size) { return new Quake[size]; } }; public int describeContents() { return 0; } } Now that you’ve got a Parcelable class, you need to create an AIDL defi nition to make it available when defi ning your Service’s AIDL interface. The following code snippet shows the contents of the Quake.aidl fi le you need to create for the Quake class defi ned above: package com.paad.earthquake; parcelable Quake; Remember that when you’re passing class objects between processes, the client process must understand the defi nition of the object being passed. Creating the AIDL Defi nition In this section, you will be defi ning a new AIDL interface defi nition for a Service you’d like to use across processes. Start by creating a new .aidl fi le within your project. This will defi ne the methods and fi elds to include in an Interface that your Service will implement. The syntax for creating AIDL defi nitions is similar to that used for standard Java interface defi nitions. Start by specifying a fully qualifi ed package name, then import all the packages required. Unlike normal Java interfaces, AIDL defi nitions need to import packages for any class or interface that isn’t a native Java type even if it’s defi ned in the same project. Defi ne a new interface, adding the properties and methods you want to make available. 358 10/20/08 4:09:57 PM 44712c11.indd 358 10/20/08 4:09:57 PM 44712c11.indd 358
Chapter 11: Advanced Android Development Methods can take zero or more parameters and return void or a supported type. If you defi ne a method that takes one or more parameters, you need to use a directional tag to indicate if the parameter is a value or reference type using the in, out, and inout keywords. Where possible, you should limit the direction of each parameter, as marshaling parameters is an expen- sive operation. The following sample shows a basic AIDL defi nition for the IEarthquakeService.aidl fi le: package com.paad.earthquake; import com.paad.earthquake.Quake; interface IEarthquakeService { List<Quake> getEarthquakes(); void refreshEarthquakes(); } Implementing and Exposing the IPC Interface If you’re using the ADT plug-in, saving the AIDL fi le will automatically code-generate a Java interface fi le. This interface will include an inner Stub class that implements the interface as an abstract class. Have your Service extend the Stub and implement the functionality required. Typically, this will be done using a private fi eld variable within the Service whose functionality you’ll be exposing. The following code snippet shows an implementation of the IEarthquakeService AIDL defi nition created above: IBinder myEarthquakeServiceStub = new IEarthquakeService.Stub() { public void refreshEarthquakes() throws RemoteException { EarthquakeService.this.refreshEarthquakes(); } public List<Quake> getEarthquakes() throws RemoteException { ArrayList<Quake> result = new ArrayList<Quake>(); ContentResolver cr = EarthquakeService.this.getContentResolver(); Cursor c = cr.query(EarthquakeProvider.CONTENT_URI, null, null, null, null); if (c.moveToFirst()) do { Double lat = c.getDouble(EarthquakeProvider.LATITUDE_COLUMN); Double lng = c.getDouble(EarthquakeProvider.LONGITUDE_COLUMN); Location location = new Location(“dummy”); location.setLatitude(lat); location.setLongitude(lng); String details = c.getString(EarthquakeProvider.DETAILS_COLUMN); 359 10/20/08 4:09:57 PM 44712c11.indd 359 44712c11.indd 359 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development String link = c.getString(EarthquakeProvider.LINK_COLUMN); double magnitude = c.getDouble(EarthquakeProvider.MAGNITUDE_COLUMN); long datems = c.getLong(EarthquakeProvider.DATE_COLUMN); Date date = new Date(datems); result.add(new Quake(date, details, location, magnitude, link)); } while(c.moveToNext()); return result; } }; There are several considerations when implementing these methods: ❑ All exceptions will remain local to the implementing process; they will not be propagated to the calling application. ❑ All IPC calls are synchronous. If you know that the process is likely to be time-consuming, you should consider wrapping the synchronous call in an asynchronous wrapper or moving the processing on the receiver side onto a background thread. With the functionality implemented, you need to expose this interface to client applications. Expose the IPC-enabled Service interface by overriding the onBind method within our service implementation to return an instance of the interface. The code snippet below demonstrates the onBind implementation for the EarthquakeService: @Override public IBinder onBind(Intent intent) { return myEarthquakeServiceStub; } To use the IPC Service from within an Activity, you must bind it as shown in the following code snippet taken from the Earthquake Activity: IEarthquakeService earthquakeService = null; private void bindService() { bindService(new Intent(IEarthquakeService.class.getName()), serviceConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { earthquakeService = IEarthquakeService.Stub.asInterface(service); } public void onServiceDisconnected(ComponentName className) { 360 10/20/08 4:09:57 PM 44712c11.indd 360 10/20/08 4:09:57 PM 44712c11.indd 360
Chapter 11: Advanced Android Development earthquakeService = null; } }; Using Internet Services Software as a service, or cloud computing, is becoming increasingly popular as companies try to reduce the cost overheads associated with installation, upgrades, and maintenance of deployed software. The result is a range of rich Internet services with which you can build thin mobile applications that enrich online services with the personalization available from your mobile. The idea of using a middle tier to reduce client-side load is not a novel one, and happily there are many Internet-based options to supply your applications with the level of service you need. The sheer volume of Internet services available makes it impossible to list them all here (let alone look at them in any detail), but the following list shows some of the more mature and interesting Internet services currently available: ❑ Google’s gData Services As well as the native Google applications, Google offers Web APIs for access to their calendar, spreadsheet, Blogger, and Picasaweb platforms. These APIs collec- tively make use of Google’s standardized gData framework, a form of Read/Write XML data communication. ❑ Yahoo! Pipes Yahoo! Pipes offers a graphical web-based approach to XML feed manipulation. Using pipes, you can fi lter, aggregate, analyze, and otherwise manipulate XML feeds and out- put them in a variety of formats to be consumed by your applications. ❑ The Google App Engine Using the Google App Engine, you can create cloud-hosted web ser- vices that shift complex processing away from your mobile client. Doing so reduces the load on your system resources but comes at the price of Internet-connection dependency. ❑ Amazon Web Services Amazon offers a range of cloud-based services, including a rich API for accessing its media database of books, CDs, and DVDs. Amazon also offers a distributed storage solution (S3) and an elastic compute cloud (EC2). Building Rich User Interfaces Mobile phone User Interfaces have improved dramatically in recent years, thanks not least of all to the iPhone’s innovative take on mobile UI. In this section, you’ll learn how to use more advanced UI visual effects like Shaders, translucency, ani- mations, touch screens, and OpenGL to add a level of polish to your Activities and Views. Working with Animations Back in Chapter 3, you learned how to defi ne animations as external resources. Now, eight chapters later, you get the opportunity to put them to use. 361 10/20/08 4:09:57 PM 44712c11.indd 361 44712c11.indd 361 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development Android offers two kinds of animation: ❑ Frame-by-Frame Animations Traditional cell-based animations in which a different Drawable is displayed in each frame. Frame-by-frame animations are displayed within a View, using its Canvas as a projection screen. ❑ Tweened Animations Tweened animations are applied to Views, letting you defi ne a series of changes in position, size, rotation, and opacity that animate the View contents. Both animation types are restricted to the original bounds of the View they’re applied to. Rotations, translations, and scaling transformations that extend beyond the original boundaries of the View will result in the contents being clipped. Introducing Tweened Animations Tweened Animations offer a simple way to provide depth, movement, or feedback to your users at a minimal resource cost. Using animations to apply a set of orientation, scale, position, and opacity changes is much less resource-intensive than manually redrawing the Canvas to achieve similar effects, not to mention far simpler to implement. Tweened animations are commonly used to: ❑ Transition between Activities. ❑ Transition between layouts within an Activity. ❑ Transition between different content displayed within the same View. ❑ Provide user feedback such as: ❑ A rotating hourglass View to indicate progress or ❑ “Shaking” an input box to indicate an incorrect or invalid data entry. Creating Tweened Animations Tweened animations are created using the Animation class. The following list explains the animation types available: ❑ AlphaAnimation Lets you animate a change in the Views transparency (opacity or alpha blending). ❑ RotateAnimation Lets you spin the selected View canvas in the XY plane. ❑ ScaleAnimation Allows you to zoom in to or out from the selected View. ❑ TranslateAnimation Lets you move the selected View around the screen (although it will only be drawn within its original bounds). Android offers the AnimationSet class to group and confi gure animations to be run as a set. You can defi ne the start time and duration of each animation used within a set to control the timing and order of the animation sequence. 362 10/20/08 4:09:57 PM 44712c11.indd 362 44712c11.indd 362 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development It’s important to set the start offset and duration for each child animation, or they will all start and complete at the same time. The following code and XML snippets demonstrate how to create the same animation sequence in code or as an external resource: // Create the AnimationSet AnimationSet myAnimation = new AnimationSet(true); // Create a rotate animation. RotateAnimation rotate = new RotateAnimation(0, 360, RotateAnimation.RELATIVE_TO_SELF,0.5f, RotateAnimation.RELATIVE_TO_SELF,0.5f ); rotate.setFillAfter(true); rotate.setDuration(1000); // Create a scale animation ScaleAnimation scale = new ScaleAnimation(1, 0, 1, 0, ScaleAnimation.RELATIVE_TO_SELF, 0.5f, ScaleAnimation.RELATIVE_TO_SELF, 0.5f ); scale.setFillAfter(true); scale.setDuration(500); scale.setStartOffset(500); // Create an alpha animation AlphaAnimation alpha = new AlphaAnimation(1, 0); scale.setFillAfter(true); scale.setDuration(500); scale.setStartOffset(500); // Add each animation to the set myAnimation.addAnimation(rotate); myAnimation.addAnimation(scale); myAnimation.addAnimation(alpha); The code snippet above implements the same animation sequence shown in the following XML snippet: <?xml version=”1.0” encoding=”utf-8”?> <set xmlns:android=”http://schemas.android.com/apk/res/android” android:shareInterpolator=”true”> <rotate android:fromDegrees=”0” android:toDegrees=”360” android:pivotX=”50%” android:pivotY=”50%” android:startOffset=”0” android:duration=”1000” /> <scale android:fromXScale=”1.0” android:toXScale=”0.0” 363 10/20/08 4:09:57 PM 44712c11.indd 363 44712c11.indd 363 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development android:fromYScale=”1.0” android:toYScale=”0.0” android:pivotX=”50%” android:pivotY=”50%” android:startOffset=”500” android:duration=”500” /> <alpha android:fromAlpha=”1.0” android:toAlpha=”0.0” android:startOffset=”500” android:duration=”500” /> </set> As you can see, it’s generally both easier and more intuitive to create your animation sequences using an external animation resource. Applying Tweened Animations Animations can be applied to any View by calling its startAnimation method and passing in the Ani- mation or Animation Set to apply. Animation sequences will run once and then stop, unless you modify this behavior using the setRepeatMode and setRepeatCount methods on the Animation or Animation Set. You can force an animation to loop or ping-pong by setting the repeat mode of RESTART or REVERSE. Setting the repeat count controls the number of times the animation will repeat. The following code snippet shows an Animation that repeats indefi nitely: myAnimation.setRepeatMode(Animation.RESTART); myAnimation.setRepeatCount(Animation.INFINITE); myView.startAnimation(myAnimation); Using Animation Listeners The AnimationListener lets you create an event handler that’s fi red when an animation begins or ends. This lets you perform actions before or after an animation has completed, such as changing the View contents or chaining multiple animations. Call setAnimationListener on an Animation object, and pass in a new implementation of AnimationListener, overriding onAnimationEnd, onAnimationStart, and onAnimationRepeat as required. The following skeleton code shows the basic implementation of an Animation Listener: myAnimation.setAnimationListener(new AnimationListener() { public void onAnimationEnd(Animation _animation) { // TODO Do something after animation is complete. } public void onAnimationRepeat(Animation _animation) { 364 10/20/08 4:09:57 PM 44712c11.indd 364 44712c11.indd 364 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development // TODO Do something when the animation repeats. } public void onAnimationStart(Animation _animation) { // TODO Do something when the animation starts. } }); Animated Sliding User Interface Example In this example, you’ll create a new Activity that uses an Animation to smoothly change the content of the User Interface based on the direction pressed on the D-pad. 1. Start by creating a new ContentSlider project featuring a ContentSlider Activity. package com.paad.contentslider; import android.app.Activity; import android.view.KeyEvent; import android.os.Bundle; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; import android.widget.TextView; public class ContentSlider extends Activity { @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); } } 2. Next, modify the main.xml layout resource. It should contain a single TextView with the text bold, centered, and relatively large. <?xml version=”1.0” encoding=”utf-8”?> <LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android” android:orientation=”vertical” android:layout_width=”fill_parent” android:layout_height=”fill_parent”> <TextView android:id=”@+id/myTextView” android:layout_width=”fill_parent” android:layout_height=”fill_parent” android:gravity=”center” android:textStyle=”bold” android:textSize=”30sp” android:text=”CENTER” android:editable=”false” android:singleLine=”true” android:layout_margin=”10px” /> </LinearLayout> 365 10/20/08 4:09:57 PM 44712c11.indd 365 44712c11.indd 365 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development 3. Then create a series of animations that slides the current View out of, and the next View into, the frame for each direction: left, right, up, and down. Each animation should have its own fi le. 3.1. Create slide_bottom_in.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromYDelta=”-100%p” android:toYDelta=”0” android:duration=”700” /> </set> 3.2. Create slide_bottom_out.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromYDelta=”0” android:toYDelta=”100%p” android:duration=”700” /> </set> 3.3. Create slide_top_in.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromYDelta=”100%p” android:toYDelta=”0” android:duration=”700” /> </set> 3.4. Create slide_top_out.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromYDelta=”0” android:toYDelta=”-100%p” android:duration=”700” /> </set> 3.5. Create slide_left_in.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromXDelta=”100%p” android:toXDelta=”0” android:duration=”700” /> </set> 366 10/20/08 4:09:57 PM 44712c11.indd 366 10/20/08 4:09:57 PM 44712c11.indd 366
Chapter 11: Advanced Android Development 3.6. Create slide_left_out.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromXDelta=”0” android:toXDelta=”-100%p” android:duration=”700” /> </set> 3.7. Create slide_right_in.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromXDelta=”-100%p” android:toXDelta=”0” android:duration=”700” /> </set> 3.8. Create slide_right_out.xml. <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <translate android:fromXDelta=”0” android:toXDelta=”100%p” android:duration=”700” /> </set> 4. Return to the ContentSlider Activity and get references to the TextView and each of the ani- mations you created in Step 3. Animation slideInLeft; Animation slideOutLeft; Animation slideInRight; Animation slideOutRight; Animation slideInTop; Animation slideOutTop; Animation slideInBottom; Animation slideOutBottom; TextView myTextView; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); slideInLeft = AnimationUtils.loadAnimation(this, R.anim.slide_left_in); slideOutLeft = AnimationUtils.loadAnimation(this, R.anim.slide_left_out); slideInRight = AnimationUtils.loadAnimation(this, R.anim.slide_right_in); slideOutRight = AnimationUtils.loadAnimation(this, R.anim.slide_right_out); slideInTop = AnimationUtils.loadAnimation(this, R.anim.slide_top_in); slideOutTop = AnimationUtils.loadAnimation(this, R.anim.slide_top_out); 367 10/20/08 4:09:57 PM 44712c11.indd 367 44712c11.indd 367 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development slideInBottom = AnimationUtils.loadAnimation(this, R.anim.slide_bottom_in); slideOutBottom = AnimationUtils.loadAnimation(this, R.anim.slide_bottom_out); myTextView = (TextView)findViewById(R.id.myTextView); } Each screen transition consists of two animations chained together: sliding out the old text before sliding in the new text. Rather than create multiple Views, you can change the value of the View once it’s “off screen” before sliding it back in from the opposite side. 5. Create a new method that applies a slide-out animation and waits for it to complete before modifying the text and initiating the slide-in animation. private void applyAnimation(Animation _out, Animation _in, String _newText) { final String text = _newText; final Animation in = _in; // Ensure the text stays out of screen when the slide-out // animation has completed. _out.setFillAfter(true); // Create a listener to wait for the slide-out animation to complete. _out.setAnimationListener(new AnimationListener() { public void onAnimationEnd(Animation _animation) { // Change the text myTextView.setText(text); // Slide it back in to view myTextView.startAnimation(in); } public void onAnimationRepeat(Animation _animation) {} public void onAnimationStart(Animation _animation) {} }); // Apply the slide-out animation myTextView.startAnimation(_out); } 6. The text displayed can represent nine positions. To keep track of the current location, create an enum for each position and an instance variable to track it. TextPosition textPosition = TextPosition.Center; enum TextPosition { UpperLeft, Top, UpperRight, Left, Center, Right, LowerLeft, Bottom, LowerRight }; 7. Create a new method movePosition that takes the current position, and the direction to move, and calculates the new position. It should then execute the appropriate animation sequence cre- ated in Step 5. private void movePosition(TextPosition _current, TextPosition _directionPressed) { 368 10/20/08 4:09:57 PM 44712c11.indd 368 10/20/08 4:09:57 PM 44712c11.indd 368
Chapter 11: Advanced Android Development Animation in; Animation out; TextPosition newPosition; if (_directionPressed == TextPosition.Left){ in = slideInLeft; out = slideOutLeft; } else if (_directionPressed == TextPosition.Right){ in = slideInRight; out = slideOutRight; } else if (_directionPressed == TextPosition.Top){ in = slideInTop; out = slideOutTop; } else { in = slideInBottom; out = slideOutBottom; } int newPosValue = _current.ordinal(); int currentValue = _current.ordinal(); // To simulate the effect of ‘tilting’ the device moving in one // direction should make text for the opposite direction appear. // Ie. Tilting right should make left appear. if (_directionPressed == TextPosition.Bottom) newPosValue = currentValue - 3; else if (_directionPressed == TextPosition.Top) newPosValue = currentValue + 3; else if (_directionPressed == TextPosition.Right) { if (currentValue % 3 != 0) newPosValue = currentValue - 1; } else if (_directionPressed == TextPosition.Left) { if ((currentValue+1) % 3 != 0) newPosValue = currentValue + 1; } if (newPosValue != currentValue && newPosValue > -1 && newPosValue < 9){ newPosition = TextPosition.values()[newPosValue]; applyAnimation(in, out, newPosition.toString()); textPosition = newPosition; } } 8. Wire up the D-pad by overriding the Activity’s onKeyDown handler to listen for key presses and trigger movePosition accordingly. @Override public boolean onKeyDown(int _keyCode, KeyEvent _event) { 369 10/20/08 4:09:57 PM 44712c11.indd 369 44712c11.indd 369 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development if (super.onKeyDown(_keyCode, _event)) return true; if (_event.getAction() == KeyEvent.ACTION_DOWN){ switch (_keyCode) { case (KeyEvent.KEYCODE_DPAD_LEFT): movePosition(textPosition, TextPosition.Left); return true; case (KeyEvent.KEYCODE_DPAD_RIGHT): movePosition(textPosition, TextPosition.Right); return true; case (KeyEvent.KEYCODE_DPAD_UP): movePosition(textPosition, TextPosition.Top); return true; case (KeyEvent.KEYCODE_DPAD_DOWN): movePosition(textPosition, TextPosition.Bottom); return true; } } return false; } Running the application now will show a screen displaying “Center”; pressing any of the four direc- tions will slide out this text and display the appropriate new position. As an extra step, you could wire up the accelerometer sensor rather than relying on pressing the D-pad. Animating Layouts and View Groups A LayoutAnimation is used to animate View Groups, applying a single Animation (or Animation Set) to each child View in a predetermined sequence. Use a LayoutAnimationController to specify an Animation (or Animation Set) that’s applied to each child View in a View Group. Each View it contains will have the same animation applied, but you can use the Layout Animation Controller to specify the order and start time for each View. Android includes two LayoutAnimationController classes. ❑ LayoutAnimationController Lets you select the start offset of each View (in milliseconds) and the order (forward, reverse, and random) to apply the animation to each child View. ❑ GridLayoutAnimationController Is a derived class that lets you assign the animation sequence of the child Views using grid row and column references. Creating Layout Animations To create a new Layout Animation, start by defi ning the Animation to apply to each child view. Then create a new LayoutAnimation, either in code or as an external animation resource, that references the animation to apply and defi nes the order and timing in which to apply it. The following XML snippets show the defi nition of a simple animation stored as popin.xml in the res/ anim folder, and a layout animation stored as popinlayout.xml. The Layout Animation applies a simple “pop-in” animation randomly to each child View of any View Group it’s assigned to. 370 10/20/08 4:09:57 PM 44712c11.indd 370 10/20/08 4:09:57 PM 44712c11.indd 370
Chapter 11: Advanced Android Development res/anim/popin.xml <set xmlns:android=”http://schemas.android.com/apk/res/android” android:interpolator=”@android:anim/accelerate_interpolator”> <scale android:fromXScale=”0.0” android:toXScale=”1.0” android:fromYScale=”0.0” android:toYScale=”1.0” android:pivotX=”50%” android:pivotY=”50%” android:duration=”400” /> </set> res/anim/popinlayout.xml <layoutAnimation xmlns:android=”http://schemas.android.com/apk/res/android” android:delay=”0.5” android:animationOrder=”random” android:animation=”@anim/popin” /> Using Layout Animations Once you’ve defi ned a Layout Animation, you can apply it to a ViewGroup either in code or in the lay- out XML resource. In XML this is done using the android:layoutAnimation tag in the layout defi ni- tion, as shown in the following XML snippet: android:layoutAnimation=”@anim/popinlayout” To set a Layout Animation in code, call setLayoutAnimation on the View Group, passing in a refer- ence to the LayoutAnimation object you want to apply. In both cases, the Layout Animation will execute once, when the View Group is fi rst laid out. You can force it to execute again by calling scheduleLayoutAnimation on the ViewGroup object. The anima- tion will be executed the next time the View Group is laid out. Layout Animations also support Animation Listeners. In the following code snippet, a ViewGroup’s animation is re-run with a listener attached to trigger additional actions once it’s complete: aViewGroup.setLayoutAnimationListener(new AnimationListener() { public void onAnimationEnd(Animation _animation) { // TODO: Actions on animation complete. } public void onAnimationRepeat(Animation _animation) {} public void onAnimationStart(Animation _animation) {} }); aViewGroup.scheduleLayoutAnimation(); 371 10/20/08 4:09:57 PM 44712c11.indd 371 44712c11.indd 371 10/20/08 4:09:57 PM
Chapter 11: Advanced Android Development Creating and Using Frame-by-Frame Animations Frame-by-frame animations are akin to traditional cell-based cartoons where an image is chosen for each frame. Where tweened animations use the target View to supply the content of the animation, frame-by- frame animations let you specify a series of Drawable objects that are used as the background to a View. The AnimationDrawable class is used to create a new frame-by-frame animation presented as a Drawable resource. You can defi ne your Animation Drawable resource as an external resource in your project’s drawable folder using XML. Use the animation-list tag to group a collection of item tags, each of which uses a drawable attribute to defi ne an image to display, and a duration attribute to specify the time (in mil- liseconds) to display it. The following XML snippet shows how to create a simple animation that displays a rocket taking off (rocket images not included). The fi le is stored as res/drawable/animated_rocket.xml: <animation-list xmlns:android=”http://schemas.android.com/apk/res/android” android:oneshot=”false”> <item android:drawable=”@drawable/rocket1” android:duration=”500” /> <item android:drawable=”@drawable/rocket2” android:duration=”500” /> <item android:drawable=”@drawable/rocket3” android:duration=”500” /> </animation-list> To display your animation, set it as the background to a View using the setBackgroundResource method, as shown in the following code snippet: ImageView image = (ImageView)findViewById(R.id.my_animation_frame); image.setBackgroundResource(R.drawable.animated_rocket); Alternatively, use the setBackgroundDrawable to use a Drawable instance instead of a resource refer- ence. Run the animation calling its start method, as shown in the code snippet below: AnimationDrawable animation = (AnimationDrawable)image.getBackground(); animation.start(); Using Themes to Skin Your Applications The multifunction nature of a mobile device means users will be running and switching between many applications created by a range of different developers. Themes are a way of ensuring that your applica- tions present a consistent look and feel. To apply a theme, set the android:theme attribute on either the application or an individual activity tag in the manifest, as shown in the code snippet below: <manifest xmlns:android=”http://schemas.android.com/apk/res/android” package=”com.google.android.home”> <application android:theme=”@android:style/Theme.Light” > <activity android:theme=”@android:style/Theme.Black” > 372 10/20/08 4:09:58 PM 44712c11.indd 372 44712c11.indd 372 10/20/08 4:09:58 PM
Chapter 11: Advanced Android Development </activity> </application> </manifest> Android includes several predefi ned themes as part of the base package, including: ❑ Theme.Black Features a black background with white foreground controls and text. ❑ Theme.Light Features a white background with dark borders and text. ❑ Theme.Translucent Features a partially transparent Form. You can set the theme of an Activity at run time, but it’s generally not recommended, as Android uses your Activity’s theme for intra-Activity animations, which happens before your application is loaded. If you do apply a theme programmatically, be sure to do so before you lay out the Activity as shown in the code snippet below: protected void onCreate(Bundle icicle) { super.onCreate(icicle); setTheme(android.R.style.Theme_Translucent); setContentView(R.layout.main); } If you don’t apply a theme to your application or any of its Activities, it will use the default theme android.R.style.Theme. Advanced Canvas Drawing You were introduced to the Canvas class in Chapter 4, where you learned how to create your own Views. The Canvas was also used in Chapter 7 to annotate Overlays for MapViews. The concept of the Canvas is a common metaphor used in graphics programming and generally con- sists of three basic drawing components: ❑ Canvas Supplies the draw methods that paint drawing primitives onto the underlying bitmap. ❑ Paint Also referred to as a “brush,” Paint lets you specify how a primitive is drawn on the bitmap. ❑ Bitmap Is the surface being drawn on. Most of the advanced techniques described in this chapter involve variations and modifi cations to the Paint object that let you add depth and texture to otherwise fl at raster drawings. The Android drawing API supports translucency, gradient fi lls, rounded rectangles, and anti-aliasing. Unfortunately, owing to resource limitations, it does not yet support vector graphics; instead, it uses traditional raster-style repaints. The result of this raster approach is improved effi ciency, but changing a Paint object will not affect primitives that have already been drawn; it will only affect new elements. If you’ve got a Windows development background, the two-dimensional (2D) drawing capabilities of Android are roughly equivalent to those available in GDI+. 373 10/20/08 4:09:58 PM 44712c11.indd 373 44712c11.indd 373 10/20/08 4:09:58 PM
Chapter 11: Advanced Android Development What Can You Draw? The Canvas class wraps up the bitmap that’s used as a surface for your artistic endeavors; it also exposes the draw* methods used to implement your designs. Without going into detail on each of the draw methods, the following list provides a taste of the primi- tives available: ❑ drawARGB / drawRGB / drawColor Fill the canvas with a single color. ❑ drawArc Draws an arc between two angles within an area bounded by a rectangle. ❑ drawBitmap Draws a bitmap on the Canvas. You can alter the appearance of the target bit- map by specifying a target size or using a matrix to transform it. ❑ drawBitmapMesh Draws a bitmap using a mesh that lets you manipulate the appearance of the target by moving points within it. ❑ drawCircle Draws a circle of a specifi ed radius centered on a given point. ❑ drawLine(s) Draws a line (or series of lines) between two points. ❑ drawOval Draws an oval bounded by the rectangle specifi ed. ❑ drawPaint Fills the entire Canvas with the specifi ed Paint. ❑ drawPath Draws the specifi ed Path. A Path object is often used to hold a collection of draw- ing primitives within a single object. ❑ drawPicture Draws a Picture object within the specifi ed rectangle. ❑ drawPosText Draws a text string specifying the offset of each character. ❑ drawRect Draws a rectangle. ❑ drawRoundRect Draws a rectangle with rounded edges. ❑ drawText Draws a text string on the Canvas. The text font, size, color, and rendering proper- ties are all set in the Paint object used to render the text. ❑ drawTextOnPath Draws text that follows along a specifi ed path. ❑ drawVertices Draws a series of tri-patches specifi ed as a series of vertex points. Each of these drawing methods lets you specify a Paint object to render it. In the following sections, you’ll learn how to create and modify Paint objects to get the most out of your drawing. Getting the Most from Your Paint The Paint class represents a paint brush and palette. It lets you choose how to render the primitives you draw onto the canvas using the draw methods described above. By modifying the Paint object, you can control the color, style, font, and special effects used when drawing. Most simply, setColor lets you select the color of a Paint while the style of a Paint object (controlled using setStyle) lets you decide if you want to draw only the outline of a drawing object (STROKE), just the fi lled portion (FILL), or both (STROKE_AND_FILL). 374 10/20/08 4:09:58 PM 44712c11.indd 374 44712c11.indd 374 10/20/08 4:09:58 PM
Chapter 11: Advanced Android Development Beyond these simple controls, the Paint class also supports transparency and can also be modifi ed using a variety of Shaders, fi lters, and effects to provide a rich palette of complex paints and brushes. The Android SDK includes several excellent projects that demonstrate most of the features available in the Paint class. They are available in the graphics subfolder of the API demos at [sdk root folder]\samples\ApiDemos\src\com\android\samples\graphics In the following sections, you’ll learn what some of these features are and how to use them. These sec- tions outline what can be achieved (such as gradients and edge embossing) without exhaustively listing all possible alternatives. Using Translucency All colors in Android include an opacity component (alpha channel). You defi ne an alpha value for a color when you create it using the argb or parseColor methods shown below: // Make color red and 50% transparent int opacity = 127; int intColor = Color.argb(opacity, 255, 0, 0); int parsedColor = Color.parseColor(“#7FFF0000”); Alternatively, you can set the opacity of an existing Paint object using the setAlpha method: // Make color 50% transparent int opacity = 127; myPaint.setAlpha(opacity); Creating a paint color that’s not 100 percent opaque means that any primitive drawn with it will be par- tially transparent — making whatever is drawn beneath it partially visible. You can use transparency effects in any class or method that uses colors including Paint colors, Shaders, and Mask Filters. Introducing Shaders Extensions of the Shader class let you create Paints that fi ll drawn objects with more than a single solid color. The most common use of Shaders is to defi ne gradient fi lls; gradients are an excellent way to add depth and texture to 2D drawings. Android includes three gradient Shaders as well as a Bitmap Shader and a Compose Shader. Trying to describe painting techniques seems inherently futile, so have a look at Figure 11-1 to get an idea of how each of the Shaders works. Represented from left to right are LinearGradient, RadialGradient, and SweepGradient. 375 10/20/08 4:09:58 PM 44712c11.indd 375 44712c11.indd 375 10/20/08 4:09:58 PM
Chapter 11: Advanced Android Development Not included in the image in Figure 11-1 is the ComposeShader, which lets you create a composite of multiple Shaders and the BitmapShader that lets you create a paint brush based on a bitmap image. To use a Shader when drawing, apply it to a Paint using the setShader method, as shown in the fol- lowing code snippet: Paint shaderPaint = new Paint(); shaderPaint.setShader(myLinearGradient); Anything you draw with this Paint will be fi lled with the Shader you specifi ed rather than the paint color. Figure 11-1 Defi ning Gradient Shaders As shown above, using gradient Shaders lets you fi ll drawings with an interpolated color range; you can defi ne the gradient as either a simple transition between two colors, as shown in the LinearGradientShader in the following code snippet: int colorFrom = Color.BLACK; int colorTo = Color.WHITE; LinearGradient linearGradientShader = new LinearGradient(x1, y1, x2, y2, colorFrom, colorTo, TileMode.CLAMP); or as a more complex series of colors distributed at set proportions, as shown in the following example of a RadialGradientShader: int[] gradientColors = new int[3]; gradientColors[0] = Color.GREEN; 376 10/20/08 4:09:58 PM 44712c11.indd 376 44712c11.indd 376 10/20/08 4:09:58 PM
Search
Read the Text Version
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436