Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Androidfp_16_voicerecorder.fm Page 1 Thursday, April 19, 2012 8:09 AM 16 Voice Recorder App Audio Recording with MediaRecorder, Audio Playback with MediaPlayer, Sending a File as an E-Mail Attachment Objectives In this chapter, you’ll: ■ Specify permissions for recording audio and writing to a device’s external storage. ■ Record audio files using a MediaRecorder. ■ Create a visual representation of the user’s audio input. ■ Use an ArrayAdapter to display the names of a directory’s files in ListView. ■ Use the File class to save files in a device’s external storage directory. ■ Use the File class to delete files from a device’s external storage directory. ■ Allow the user to send a recording as an e-mail attachment. Androidfp_16_voicerecorder.fm Page 2 Thursday, April 19, 2012 8:09 AM Outline 16-2 Chapter 16 Voice Recorder App 16.4.6 saved_recordings.xml: Layout for the SavedRecordings 16.1 Introduction 16.2 Test-Driving the Voice Recorder App 16.3 Technologies Overview 16.4 Building the App’s GUI and Resource Files 16.4.1 Creating the Project 16.4.2 Using Standard Android Icons in the App’s GUI 16.4.3 AndroidManifest.xml 16.4.4 main.xml: Layout for the ListActivity 16.4.7 saved_recordings_row.xml: Custom ListView Item Layout for the SavedRecordings ListActivity 16.4.8 play_pause_drawable.xml: Drawable for the Play/Pause Button 16.5 Building the App 16.5.1 VoiceRecorder Subclass of Activity 16.5.2 VisualizerView Subclass of View 16.5.3 SavedRecordings Subclass of VoiceRecorder Activity 16.4.5 name_edittext.xml: Layout for the Custom AlertDialog Used to Name a Recording Activity 16.6 Wrap-Up 16.1 Introduction The Voice Recorder app allows the user to record sounds using the phone’s microphone and save the audio files for playback later. The app’s main Activity (Fig. 16.1) shows a Record ToggleButton that allows the user to begin recording audio, Save and Delete buttons that become active after the user finishes a recording, and a View Saved Recordings button that allows the user to view a list of saved recordings. When the user touches the Record ToggleButton, it becomes a Stop ToggleButton and the top part of the screen becomes a visualizer that displays green bars which vary in size proportional to the intensity of the user’s voice (Fig. 16.2). When the user touches the Stop ToggleButton, the Save and Delete buttons are Visualization area Record Button The disabled Save and Delete buttons are enabled only when there is a new recording Touch to view saved recordings Fig. 16.1 | Voice Recorder app ready to record. Androidfp_16_voicerecorder.fm Page 3 Thursday, April 19, 2012 8:09 AM 16.1 Introduction 16-3 Visualization of the user’s recording Save and Delete Buttons are now enabled because the user pressed the Stop ToggleButton (which now displays Record) to stop recording Fig. 16.2 | Visualization of the user’s recording. enabled so the user can choose whether to save or delete the temporary recording file. If the user touches Save, the Name Your Recording dialog is displayed (Fig. 16.3). If the user touches Delete, a confirmation dialog is displayed before the recording is deleted. Fig. 16.3 | AlertDialog for naming a recording. The user can touch the View Saved Recordings Button to view the list of previously saved recordings (Fig. 16.4(a)). Touching a recording’s name plays that recording (Fig. 16.4(b)). The user can touch the Pause/Play ToggleButton to pause and play the recording, and can drag the Seekbar above to move forward or backward through the recording. Touching a recording’s icon allows you to send the recording via e-mail. Touching a recording’s icon allows you to delete the recording (after you confirm by touching Delete in the confirmation dialog that’s displayed).Touching the device’s back button returns the user to the app’s main Activity. [Note: This app’s recording capabilities require an actual Android device for testing purposes. At the time of this writing, the Android emulator does not provide microphone support.] Androidfp_16_voicerecorder.fm Page 4 Thursday, April 19, 2012 8:09 AM 16-4 Chapter 16 Voice Recorder App a) Selecting a previously saved recording to play b) Invitation list.3gp playing ToggleButton toggles between Play and Pause Touch to delete recording Touch to e-mail recording Touch the name of a clip to play it Fig. 16.4 | Playing a previously saved recording. 16.2 Test-Driving the Voice Recorder App Opening and Running the App Open Eclipse and import the Voice Recorder app project. To import the project: 1. Select File > Import… to display the Import dialog. 2. Expand the General node and select Existing Projects into Workspace, then click Next >. 3. To the right of the Select root directory: textfield, click Browse… then locate and select the VoiceRecorder folder. 4. Click Finish to import the project. Connect an Android device that’s set up for debugging to your computer, then right click the app’s project in the Package Explorer window and select Run As > Android Application from the menu that appears. Recording a New Audio File Touch the Record ToggleButton (Fig. 16.1) to begin recording. As you speak into your device’s microphone, the app’s visualizer reacts to the intensity of your voice. If you record long enough, the visualization bars will scroll off the left side of the screen as new visualization bars display at the right. When you’re done recording, press the Stop ToggleButton to enable the Save and Delete buttons. To save your recording, touch Save then enter a name in the Name Your Recording dialog and touch the dialog’s Save button. To delete your recording, touch Delete, then confirm that you wish to delete the recording. Androidfp_16_voicerecorder.fm Page 5 Thursday, April 19, 2012 8:09 AM 16.3 Technologies Overview 16-5 Playing a Recording Touch the View Saved Recordings Button to display a customized ListActivity containing a scrollable list of previous recordings. Touch the name of the recording you wish to play—it will load and begin playing immediately. Slide the SeekBar thumb to adjust the recording’s playback position. Touch the Pause ToggleButton to pause playback. The label and icon on the button change to indicate that you can touch the button again to play the recording—touch it to continue playback. To return to the app’s main Activity, touch the device’s back button. 16.3 Technologies Overview This section presents the new technologies that we use in the Voice Recorder app. Permissions for Writing Data to External Storage and Recording Audio As always, the AndroidManifest.xml file describes the app’s components. Recall from Chapter 11 that, by default, shared Android services are not accessible to an app. To access shared services, you must request permission to use them in the manifest file with <usespermission> elements nested in the root <manifest> element. This app’s <manifest> element contains <uses-permission> elements for recording audio (to use the microphone) and for writing to a device’s external storage (to save a recording). Clickable ImageViews In our custom ListView item layouts for the Slideshow apps (Chapters 12–13), we used Buttons to represent the tasks that could be performed for each list item. Using GUI components that can receive the focus (such as Buttons) in a custom ListView item layout prevents the ListView items themselves from being clickable. This was not a problem in the Slideshow and Enhanced Slideshow apps, because the tasks for a given ListView item were all defined by the Buttons’ event handlers. In this app, we want the user to touch a ListView item to play the corresponding recording. We also want to provide “buttons” that allow the user to send the recording as an e-mail attachment or to delete the recording. For this reason, the e-mail and delete “buttons” are defined as clickable ImageViews (Section 16.4.7). This allows the ListView items themselves to remain clickable as well. Using a State List Drawable to Change the Icons on a ToggleButton Based on Its State Android’s buttons can assume various states, depending on their type. For example, a ToggleButton can be checked or unchecked. Sometimes it’s desirable to specify different icons for different states. In Android, this is accomplished by defining a state list drawable in XML with a root <selector> element (Section 16.4.8) that contains <item> elements for each state. Each <item> element also specifies the Drawable (such as an icon) to display for the corresponding state. In this app, we specify a state list Drawable for a ToggleButton’s android:drawableTop attribute, and Android automatically displays the correct Drawable based on the ToggleButton’s state. Using a MediaRecorder to Record Audio The Voice Recorder app uses a MediaRecorder (package android.media) to record the user’s voice. A MediaRecorder records audio using the device’s microphone and saves it to an audio file on the device. Section 16.5.1 demonstrates how to configure and use a MediaRecorder. Androidfp_16_voicerecorder.fm Page 6 Thursday, April 19, 2012 8:09 AM 16-6 Chapter 16 Voice Recorder App Using the File Class to Create a Temporary File, Rename a File and Delete a File Initially, each recording is saved in a temporary file, which we create with class File’s createTempFile method. When the user chooses to save that file, we use class File’s renameTo method to give the file a permanent name. These features are shown in Section 16.5.1. Section 16.5.3 shows how to delete a file with File method delete. Sending a Recording as an E-Mail Attachment We use an Intent and an Activity chooser to allow the user to send a recording as an email attachment (Section 16.5.3) via any app on the device that supports this capability. 16.4 Building the App’s GUI and Resource Files In this section, we discuss the app’s resource files and layout files. 16.4.1 Creating the Project Begin by creating a new Android project named VoiceRecorder. Specify the following values in the New Android Project dialog, then press Finish : • Build Target: • Application name: Voice Recorder • Package name: com.deitel.voicerecorder • Create Activity: VoiceRecorder • Min SDK Version: 10. Ensure that Android 2.3.3 is checked 16.4.2 Using Standard Android Icons in the App’s GUI You learned in Chapter 10 that Android comes with standard icons that you can use in your own apps. Again, these are located in the SDK’s platforms folder under each platform version’s data/res/drawable-hdpi folder. We copied the icons that we use into this app’s res/drawable-hdpi folder. Expand that folder in Eclipse to see the specific icons we chose. 16.4.3 AndroidManifest.xml As in prior apps with multiple activities, this app’s AndroidManifest.xml file contains <activity> elements for each Activity—VoiceRecorder and SavedRecordings. Both use the portrait screen orientation. In addition, the <manifest> element contains the following <uses-permission> elements: <uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> which indicate that the app requires the ability to record audio and write data to external storage, respectively. 16.4.4 main.xml: Layout for the VoiceRecorder Activity No new features are presented in the main.xml layout for the VoiceRecorder Activity, so we do not show main.xml here. When you view the file in Eclipse, recall that custom Androidfp_16_voicerecorder.fm Page 7 Thursday, April 19, 2012 8:09 AM 16.4 Building the App’s GUI and Resource Files 16-7 Views like the VisualizerView (lines 5–7 in the file) must be declared with their package and class names. 16.4.5 name_edittext.xml: Layout for the Custom AlertDialog Used to Name a Recording As in the Slideshow app, this app uses a custom layout (name_edittext.xml) that’s attached to an AlertDialog so that the user can enter a recording name when saving a recording. The layout is identical to the one used in Section 12.4.6, so we don’t show it here. 16.4.6 saved_recordings.xml: Layout for the SavedRecordings ListActivity Figure 16.5 shows the SavedRecordings ListActivity’s layout. Recall that you must provide a ListView with its android:id set to "@android:id/list" (lines 26–28) when customizing a ListActivity’s layout. This layout introduces one new feature—the ToggleButton specifies for its android:drawableTop attribute (line 17) the custom Drawable play_pause_drawable (Section 16.4.8). This Drawable allows Android to toggle the icon between a play and a pause icon when the user changes the ToggleButton’s state. The items displayed in this layout’s ListView use the custom drawable defined in Section 16.4.8. 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 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingLeft="15dp" android:paddingRight="10dp"> <TextView android:id="@+id/nowPlayingTextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textSize="20sp" android:keepScreenOn="true"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="25dp" android:layout_marginTop="25dp"> <ToggleButton android:layout_width="75dp" android:layout_height="wrap_content" android:id="@+id/playPauseButton" android:drawableTop="@drawable/play_pause_drawable" android:textOff="@string/button_play" android:textOn="@string/button_pause" android:layout_marginRight="5dp"/> <SeekBar android:id="@+id/progressSeekBar" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_width="match_parent"/> </LinearLayout> <ListView android:id="@android:id/list" android:layout_margin="5dp" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1"/> </LinearLayout> Fig. 16.5 | Layout for the SavedRecordings ListActivity. Androidfp_16_voicerecorder.fm Page 8 Thursday, April 19, 2012 8:09 AM 16-8 Chapter 16 Voice Recorder App 16.4.7 saved_recordings_row.xml: Custom ListView Item Layout for the SavedRecordings ListActivity Figure 16.6 shows the layout for the items displayed in the SavedRecordings ListActivEach item consists of a horizontal LinearLayout and two clickable ImageViews—specified by setting the android:clickable attribute to true for each ListView (lines 16 and 22). ity’s ListView. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/editLinearLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="?android:attr/listPreferredItemHeight"> <TextView android:id="@+id/nameTextView" android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="20sp" android:gravity="center_vertical" android:layout_weight="1" /> <ImageView android:id="@+id/emailButton" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="right" android:src="@drawable/sym_action_email" android:clickable="true"/> <ImageView android:id="@+id/deleteButton" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="right" android:src="@drawable/ic_delete" android:clickable="true"/> </LinearLayout> Fig. 16.6 | Layout for the items in the SavedRecordings ListActivity’s ListView. 16.4.8 play_pause_drawable.xml: Drawable for the Play/Pause Button As you know, ToggleButtons (introduced in Chapter 11) provide android:textOff and android:textOn attributes that allow you to specify the text that’s displayed for ToggleButton’s two states. You can also specify different icons for the two states by defining a custom Drawable that specifies the two icons. To do so: 1. Create a new Android XML file for a Drawable—this will place the file into the project’s /res/drawable folder by default. 2. Name the file play_pause_drawable.xml. 3. Specify selector as the root element. 4. Define the two <item> elements shown in lines 3–6 of Fig. 16.7. Each <item> element specifies a Drawable for a given state. In this case, we specify Drawables for the two key states of a ToggleButton—that is, when it’s checked and when it’s un- Androidfp_16_voicerecorder.fm Page 9 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 16-9 checked. To specify which state the Drawable applies to, use the android:state_checked attribute with the value true (checked) or false (unchecked). For more information, see: developer.android.com/guide/topics/resources/ drawable-resource.html#StateList 1 2 3 4 5 6 7 <?xml version="1.0" encoding="utf-8"> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_checked="true" android:drawable="@drawable/ic_media_pause"/> <item android:state_checked="false" android:drawable="@drawable/ic_media_play"/> </selector> Fig. 16.7 | Custom drawable for the VoiceRecorder Activity’s Record ToggleButton. 16.5 Building the App This app consists of three classes—VoiceRecorder (an Activity subclass, Figs. 16.8– 16.14), VisualizerView (a View subclass, Figs. 16.15–16.18) and SavedRecordings (a ListActivity subclass, Figs. 16.19–16.27). 16.5.1 VoiceRecorder Subclass of Activity VoiceRecorder is the app’s main Activity class and is responsible for creating a recording and visualizing it. Class VoiceRecorder also enables the user to save or delete the new recording and to view a separate Activity for playing back previously saved recordings that were created by this app. Statement, import Statements and Fields The only new class used by class VoiceRecorder is MediaRecorder, which is highlighted in Fig. 16.8. Line 36 declares the VisualizerView, which is used to display the visual representation of the audio input while recording. The other instance variables are discussed as they’re used throughout the class. package 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // VoiceRecorder.java // Main Activity for the VoiceRecorder class. package com.deitel.voicerecorder; import java.io.File; import java.io.IOException; import import import import import import import Fig. 16.8 | android.app.Activity; android.app.AlertDialog; android.content.Context; android.content.DialogInterface; android.content.Intent; android.media.MediaRecorder; android.os.Bundle; package statement, import statements and fields. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 10 Thursday, April 19, 2012 8:09 AM 16-10 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 Chapter 16 import import import import import import import import import import import import Voice Recorder App android.os.Handler; android.util.Log; android.view.Gravity; android.view.LayoutInflater; android.view.View; android.view.View.OnClickListener; android.widget.Button; android.widget.CompoundButton; android.widget.CompoundButton.OnCheckedChangeListener; android.widget.EditText; android.widget.Toast; android.widget.ToggleButton; public class VoiceRecorder extends Activity { private static final String TAG = VoiceRecorder.class.getName(); private MediaRecorder recorder; // used to record audio private Handler handler; // Handler for updating the visualizer private boolean recording; // are we currently recording? // variables for GUI private VisualizerView visualizer; private ToggleButton recordButton; private Button saveButton; private Button deleteButton; private Button viewSavedRecordingsButton; Fig. 16.8 | package statement, import statements and fields. (Part 2 of 2.) Overriding Activity Methods onCreate, onResume and onPause After inflating main.xml (Fig. 16.9, line 47), lines 50–58 of method onCreate get references to the layout’s ToggleButton, Buttons and VisualizerView, and disable the saveButton and deleteButton. Lines 61–64 register the listeners for the ToggleButton and Buttons. Line 66 creates the Handler that will be used to update the VisualizerView. 42 43 44 45 46 47 48 49 50 51 52 53 54 55 // called when the activity is first created @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // set the Activity's layout // get the Activity's Buttons and VisualizerView recordButton = (ToggleButton) findViewById(R.id.recordButton); saveButton = (Button) findViewById(R.id.saveButton); saveButton.setEnabled(false); // disable saveButton initially deleteButton = (Button) findViewById(R.id.deleteButton); deleteButton.setEnabled(false); // disable deleteButton initially Fig. 16.9 | Overriding Activity methods onCreate, onResume and onPause. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 11 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 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 16-11 viewSavedRecordingsButton = (Button) findViewById(R.id.viewSavedRecordingsButton); visualizer = (VisualizerView) findViewById(R.id.visualizerView); // register listeners saveButton.setOnClickListener(saveButtonListener); deleteButton.setOnClickListener(deleteButtonListener); viewSavedRecordingsButton.setOnClickListener( viewSavedRecordingsListener); handler = new Handler(); // create the Handler for visualizer update } // end method onCreate // create the MediaRecorder @Override protected void onResume() { super.onResume(); // register recordButton's listener recordButton.setOnCheckedChangeListener(recordButtonListener); } // end method onResume // release the MediaRecorder @Override protected void onPause() { super.onPause(); recordButton.setOnCheckedChangeListener(null); // remove listener if (recorder != null) { handler.removeCallbacks(updateVisualizer); // stop updating GUI visualizer.clear(); // clear visualizer for next recording recordButton.setChecked(false); // reset recordButton viewSavedRecordingsButton.setEnabled(true); // enable recorder.release(); // release MediaRecorder resources recording = false; // we are no longer recording recorder = null; ((File) deleteButton.getTag()).delete(); // delete the temp file } // end if } // end method onPause Fig. 16.9 | Overriding Activity methods onCreate, onResume and onPause. (Part 2 of 2.) Method onResume (lines 70–77) sets recordButton’s listener. Method onPause (lines 80–97) removes recordButton’s listener and, if recorder is not null, performs cleanup tasks just in case the app is not brought back to the foreground. Line 88 removes the callbacks from the handler, so the visualizer stops updating and line 89 clears it for a possible new recording in the future. Lines 90–91 ensure that the recordButton and viewSavedRecordingsButton are in the proper state in case the app is brought back to the foreground. Line 92 calls MediaRecorder method release to release the resources used by the Androidfp_16_voicerecorder.fm Page 12 Thursday, April 19, 2012 8:09 AM 16-12 Chapter 16 Voice Recorder App MediaRecorder object. If the recorder is not null, then there was a recording in progress when the app was paused. For simplicity, line 95 deletes the temporary recording’s file. OnCheckedChangedListener recordButtonListener Starts and Stops a Recording The recordButtonListener OnCheckedChangedListener (Fig. 16.10) responds to events generated when the user clicks the Record ToggleButton. If the onCheckedChanged method’s isChecked parameter is true, lines 109–148 configure the app to begin a new recording. Line 109 clears the VisualizerView, then we disable the Buttons that should not be active when a recording is in progress. 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 // starts/stops a recording OnCheckedChangeListener recordButtonListener = new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { visualizer.clear(); // clear visualizer for next recording saveButton.setEnabled(false); // disable saveButton deleteButton.setEnabled(false); // disable deleteButton viewSavedRecordingsButton.setEnabled(false); // disable Fig. 16.10 | // create MediaRecorder and configure recording options if (recorder == null) recorder = new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat( MediaRecorder.OutputFormat.THREE_GPP); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); recorder.setAudioEncodingBitRate(16); recorder.setAudioSamplingRate(44100); try { // create temporary file to store recording File tempFile = File.createTempFile( "VoiceRecorder", ".3gp", getExternalFilesDir(null)); // store File as tag for saveButton and deleteButton saveButton.setTag(tempFile); deleteButton.setTag(tempFile); // set the MediaRecorder's output file recorder.setOutputFile(tempFile.getAbsolutePath()); recorder.prepare(); // prepare to record recorder.start(); // start recording recording = true; // we are currently recording OnCheckedChangedListener recordButtonListener starts and stops a record- ing. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 13 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 16-13 handler.post(updateVisualizer); // start updating view } // end try catch (IllegalStateException e) { Log.e(TAG, e.toString()); } // end catch catch (IOException e) { Log.e(TAG, e.toString()); } // end catch } // end if else { recorder.stop(); // stop recording recorder.reset(); // reset the MediaRecorder recording = false; // we are no longer recording saveButton.setEnabled(true); // enable saveButton deleteButton.setEnabled(true); // enable deleteButton recordButton.setEnabled(false); // disable recordButton } // end else } // end method onCheckedChanged }; // end OnCheckedChangedListener 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 Fig. 16.10 | OnCheckedChangedListener recordButtonListener starts and stops a record- ing. (Part 2 of 2.) Lines 115–122 create a new MediaRecorder, then prepare it for recording as follows: • Line 117 calls setAudioSource to indicate that the MediaRecorder should get its input from the device’s microphone. Several input sources are supported. For a complete list see the online documentation for class MediaRecorder.AudioSource. Method setAudioSource must be called before you configure other audio recording parameters. • Lines 118–119 call setOutputFormat to indicate that the audio should be saved in 3GP format, which is recommended for compatibility with desktop audio players. Other formats are specified in class MediaRecorder.OutputFormat. • Line 120 calls setAudioEncoder to specify that the AAC audio encoder should be used. An encoder compresses the audio—different encoders yield results of different quality. We chose AAC because it’s designed for better quality audio (which also consumes more storage). Android also provides an AMR encoder, which is geared to voice data that’s being transmitted over cellular networks. • Line 121 calls setAudioEncodingBitRate to specify the recording’s bit rate (bits/ sec). Lower bit rates yield lesser quality audio. If the device cannot support the specified bit rate, Android adjusts the bit rate lower automatically. • Line 122 calls setAudioSamplingRate to specify the sampling rate, which depends on the audio encoder being used. The AAC encoder supports sampling rates from 8 to 96 kHz, with higher sampling rates producing better-quality sound. Androidfp_16_voicerecorder.fm Page 14 Thursday, April 19, 2012 8:09 AM 16-14 Chapter 16 Voice Recorder App The bit rate and sampling rate we use in this example typically produce good-quality sound without requiring significant storage space. Once the MediaRecorder is configured, lines 127–139 create a temporary file to store the recording and begin the recording process. Line 127–128 create the file by calling class File’s static method createTempFile. The first two arguments are the temporary filename’s prefix and the extension. The last argument is the file’s location. Recall from Chapter 13 that method getExternalFilesDir returns a File representing an application-specific external storage directory that’s automatically managed by the system—if you delete this app, its files are deleted as well. Lines 131–132 set the tempFile object as the tag on the saveButton and deleteButton so the tempFile can be used in each Button’s onClick event handler. Line 135 calls MediaRecorder method setOutputFile to indicate that the recording should be saved into tempFile. Lines 136 and 137 call MediaRecorder methods prepare and start, respectively. Method prepare uses the settings in lines 117– 122 to ensure that the device is ready to record. This must be done before calling method start, which begins recording audio. We then indicate that the app is recording and start the updateVisualizer Runnable (Fig. 16.11) by passing it to the Handler’s post method. If the onCheckedChanged method’s isChecked parameter is false, lines 152–157 configure the activity to allow the user to save or delete the temporary recording file. Lines 152–153 call MediaRecorder’s stop and reset methods to end the recording and reset the MediaRecorder. We then indicate that the app is not recording, enable the Save and Delete Buttons, and disable the Record ToggleButton. Updates the VisualizerView (Fig. 16.11) updates the VisualizerView (Section 16.5.2) to reflect the current audio input. If the app is recording, line 171 calls class MediaRecorder’s getMaxAmplitude method to get the maximum recording amplitude since getMaxAmplitude was last called. We add that amplitude value to the VisualizerView (line 172), then call its invalidate method (line 173) to indicate that the View needs to be redrawn. Line 174 schedules updateVisualizer to run again after a 50-millisecond delay. Runnable updateVisualizer Runnable updateVisualizer 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 // updates the visualizer every 50 milliseconds Runnable updateVisualizer = new Runnable() { @Override public void run() { if (recording) // if we are already recording { // get the current amplitude int x = recorder.getMaxAmplitude(); visualizer.addAmplitude(x); // update the VisualizeView visualizer.invalidate(); // refresh the VisualizerView handler.postDelayed(this, 50); // update in 50 milliseconds } // end if } // end method run }; // end Runnable Fig. 16.11 | Runnable updateVisualizer updates the VisualizerView. Androidfp_16_voicerecorder.fm Page 15 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 16-15 Allows the User to Save a New Recording The saveButtonListener OnClickListener (Fig. 16.12) displays an AlertDialog with a custom layout (name_edittext.xml, which is inflated at line 190) to confirm whether the recording should be saved. If the user touches the dialog’s Save button and the name specified by the user is not an empty String, lines 210–215 give the file a permanent name specified by the user. Line 210 gets the File object that was set as the Button’s tag in line 131 of Fig. 16.10. Lines 211–214 create a File object that represents the filename specified by the user. Line 215 then calls File method renameTo on the tempFile object to rename it to the filename specified in the newFile object. If the name specified by the user is empty, lines 224–229 display a Toast indicating that a filename is required. OnClickListener saveButtonListener 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 // saves a recording OnClickListener saveButtonListener = new OnClickListener() { @Override public void onClick(final View v) { // get a reference to the LayoutInflater service LayoutInflater inflater = (LayoutInflater) getSystemService( Context.LAYOUT_INFLATER_SERVICE); Fig. 16.12 | // inflate name_edittext.xml to create an EditText View view = inflater.inflate(R.layout.name_edittext, null); final EditText nameEditText = (EditText) view.findViewById(R.id.nameEditText); // create an input dialog to get recording name from user AlertDialog.Builder inputDialog = new AlertDialog.Builder(VoiceRecorder.this); inputDialog.setView(view); // set the dialog's custom View inputDialog.setTitle(R.string.dialog_set_name_title); inputDialog.setPositiveButton(R.string.button_save, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // create a SlideshowInfo for a new slideshow String name = nameEditText.getText().toString().trim(); if (name.length() != 0) { // create Files for temp file and new filename File tempFile = (File) v.getTag(); File newFile = new File( getExternalFilesDir(null).getAbsolutePath() + File.separator + name + ".3gp"); tempFile.renameTo(newFile); // rename the file saveButton.setEnabled(false); // disable OnClickListener saveButtonListener ing. (Part 1 of 2.) allows the user to save a new record- Androidfp_16_voicerecorder.fm Page 16 Thursday, April 19, 2012 8:09 AM 16-16 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 Chapter 16 Voice Recorder App deleteButton.setEnabled(false); // disable recordButton.setEnabled(true); // enable viewSavedRecordingsButton.setEnabled(true); // enable } // end if else { // display message that slideshow must have a name Toast message = Toast.makeText(VoiceRecorder.this, R.string.message_name, Toast.LENGTH_SHORT); message.setGravity(Gravity.CENTER, message.getXOffset() / 2, message.getYOffset() / 2); message.show(); // display the Toast } // end else } // end method onClick } // end anonymous inner class ); // end call to setPositiveButton inputDialog.setNegativeButton(R.string.button_cancel, null); inputDialog.show(); } // end method onClick }; // end OnClickListener Fig. 16.12 | OnClickListener saveButtonListener allows the user to save a new record- ing. (Part 2 of 2.) Allows the User to Delete a New Recording The deleteButtonListener OnClickListener (Fig. 16.13) displays an AlertDialog to confirm whether the recording should be deleted. If the user touches the dialog’s Delete button, line 257 gets the File object that was set as the Button’s tag in line 132 of Fig. 16.10, then calls File method delete to delete the file from the device. OnClickListener deleteButtonListener 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 // deletes the temporary recording OnClickListener deleteButtonListener = new OnClickListener() { @Override public void onClick(final View v) { // create an input dialog to get recording name from user AlertDialog.Builder confirmDialog = new AlertDialog.Builder(VoiceRecorder.this); confirmDialog.setTitle(R.string.dialog_confirm_title); confirmDialog.setMessage(R.string.dialog_confirm_message); Fig. 16.13 | confirmDialog.setPositiveButton(R.string.button_delete, new DialogInterface.OnClickListener() { OnClickListener deleteButtonListener recording. (Part 1 of 2.) allows the user to delete a new Androidfp_16_voicerecorder.fm Page 17 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 16-17 public void onClick(DialogInterface dialog, int which) { ((File) v.getTag()).delete(); // delete the temp file saveButton.setEnabled(false); // disable deleteButton.setEnabled(false); // disable recordButton.setEnabled(true); // enable viewSavedRecordingsButton.setEnabled(true); // enable } // end method onClick } // end anonymous inner class ); // end call to setPositiveButton confirmDialog.setNegativeButton(R.string.button_cancel, null); confirmDialog.show(); recordButton.setEnabled(true); // enable recordButton } // end method onClick }; // end OnClickListener Fig. 16.13 | OnClickListener deleteButtonListener allows the user to delete a new recording. (Part 2 of 2.) OnClickListener viewSavedRecordingsListener ings ListActivity The Launches the SavedRecord- viewSavedRecordings OnClickListener ings Activity (Fig. 16.14) launches the (Section 16.5.3) using an explicit Intent. SavedRecord- 272 // launch Activity to view saved recordings 273 OnClickListener viewSavedRecordingsListener = new OnClickListener() 274 { 275 @Override 276 public void onClick(View v) 277 { 278 // launch the SaveRecordings Activity 279 Intent intent = 280 new Intent(VoiceRecorder.this, SavedRecordings.class); 281 startActivity(intent); 282 } // end method onClick 283 }; // end OnClickListener 284 } // end class VoiceRecorder Fig. 16.14 | OnClickListener viewSavedRecordingsListener launches the Saved- Recordings ListActivity. 16.5.2 VisualizerView Subclass of View This VisualizerView class (Figs. 16.15–16.18) is a custom View that displays a visual representation of a recording. As the user makes a recording, the View displays green lines with their height proportional to the amplitude of the audio input. Statement, import Statements, Fields and Constructor In class VisualizerView, the instance variable amplitudes (line 19) maintains a List of the most recent amplitude values in the current recording. These determine the height of package Androidfp_16_voicerecorder.fm Page 18 Thursday, April 19, 2012 8:09 AM 16-18 Chapter 16 Voice Recorder App each line. The class’s constructor (lines 25–31) configures a draw the visualization lines. 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 Paint object that’s used to // VisualizerView.java // Visualizer for the audio being recorded. package com.deitel.voicerecorder; import java.util.ArrayList; import java.util.List; import import import import import import android.content.Context; android.graphics.Canvas; android.graphics.Color; android.graphics.Paint; android.util.AttributeSet; android.view.View; public class VisualizerView extends View { private static final int LINE_WIDTH = 1; // width of visualizer lines private static final int LINE_SCALE = 75; // scales visualizer lines private List<Float> amplitudes; // amplitudes for line lengths private int width; // width of this View private int height; // height of this View private Paint linePaint; // specifies line drawing characteristics // constructor public VisualizerView(Context context, AttributeSet attrs) { super(context, attrs); // call superclass constructor linePaint = new Paint(); // create Paint for lines linePaint.setColor(Color.GREEN); // set color to green linePaint.setStrokeWidth(LINE_WIDTH); // set stroke width } // end VisualizerView constructor Fig. 16.15 | package statement, import statements, fields and constructor. Overriding View Method onSizeChanged Method onSizeChanged (Fig. 16.16) is called whenever the size of the VisualizerView changes—such as when it’s added to the VoiceRecorder layout’s view hierarchy. We store the VisualizerView’s width and height, which are used to determine the number of lines that can fit in the width of the screen and to scale the height of those lines, respectively. 33 34 35 36 37 38 // called when the dimensions of the View change @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { width = w; // new width of this View height = h; // new height of this View Fig. 16.16 | Overriding View method onSizeChanged. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 19 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 39 40 41 16-19 amplitudes = new ArrayList<Float>(width / LINE_WIDTH); } // end method onSizeChanged Fig. 16.16 | Overriding View method onSizeChanged. (Part 2 of 2.) Methods clear and addAmplitude Method clear (Fig. 16.17, lines 43–46) empties the amplitudes List to prepare for visualizing a new recording. Method addAmplitude (lines 49–58) is called by VoiceRecorder to add an amplitude reading from the current recording to the amplitudes List (line 51). If the amplitude lines for the recording fill the screen’s width (line 54), we remove the oldest value, which represents the leftmost line on the screen—this enables the visualization lines to scroll off the left side of the screen as new lines are added at the right. 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 // clear all amplitudes to prepare for a new visualization public void clear() { amplitudes.clear(); } // end method clear // add the given amplitude to the amplitudes ArrayList public void addAmplitude(float amplitude) { amplitudes.add(amplitude); // add newest to the amplitudes ArrayList // if the power lines completely fill the VisualizerView if (amplitudes.size() * LINE_WIDTH >= width) { amplitudes.remove(0); // remove oldest power value } // end if } // end method addAmplitude Fig. 16.17 | Methods clear and addAmplitude. Overriding View Method onDraw VisualizerView’s onDraw method (Fig. 16.18) displays a visual representation of the recording to the given Canvas. This method is called when the View is first displayed and when the VoiceRecorder activity calls the View’s invalidate method. Lines 68–76 process the amplitudes List. For each amplitude, we calculate a scaledHeight (line 70) that represents the amplitude’s line length on the screen, then determine the new x-coordinate of the line. Lines 74–75 then draw the line centered vertically on the View using Canvas’s drawLine method. 60 61 62 63 // draw the visualizer with scaled lines representing the amplitudes @Override public void onDraw(Canvas canvas) { Fig. 16.18 | Overriding View method onDraw. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 20 Thursday, April 19, 2012 8:09 AM 16-20 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 Chapter 16 Voice Recorder App int middle = height / 2; // get the middle of the View float curX = 0; // start curX at zero // for each item in the amplitudes ArrayList for (float power : amplitudes) { float scaledHeight = power / LINE_SCALE; // scale the power curX += LINE_WIDTH; // increase X by LINE_WIDTH // draw a line representing this item in the amplitudes ArrayList canvas.drawLine(curX, middle + scaledHeight / 2, curX, middle - scaledHeight / 2, linePaint); } // end for } // end method onDraw } // end class VisualizerView Fig. 16.18 | Overriding View method onDraw. (Part 2 of 2.) 16.5.3 SavedRecordings Subclass of Activity The SavedRecordings subclass of ListActivity (Figs. 16.19–16.27) displays the user’s saved recordings in a ListView. The user can touch a recording’s name to play it, touch the corresponding e-mail icon to send the recording as an e-mail attachment, or touch the delete icon to delete the recording from the device’s storage. The user can also toggle between pausing and playing the selected recording, and can adjust the playback position by using a SeekBar. Statement, import Statements and Fields Figure 16.19 shows the package statement, import statements and fields of class SavedRecordings. All of the import statements have been used previously in this app or earlier apps, so we do not highlight any of them here. We discuss the class’s fields as they’re encountered throughout this section. package 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // SavedRecordings.java // Activity displaying and playing saved recordings. package com.deitel.voicerecorder; import import import import import java.io.File; java.util.ArrayList; java.util.Arrays; java.util.Collections; java.util.List; import import import import import import android.app.AlertDialog; android.app.ListActivity; android.content.Context; android.content.DialogInterface; android.content.Intent; android.media.MediaPlayer; Fig. 16.19 | package statement, import statements and fields. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 21 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 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 import import import import import import import import import import import import import import import import import import 16-21 android.media.MediaPlayer.OnCompletionListener; android.net.Uri; android.os.Bundle; android.os.Handler; android.util.Log; android.view.LayoutInflater; android.view.View; android.view.View.OnClickListener; android.view.ViewGroup; android.widget.ArrayAdapter; android.widget.CompoundButton; android.widget.CompoundButton.OnCheckedChangeListener; android.widget.ImageView; android.widget.ListView; android.widget.SeekBar; android.widget.SeekBar.OnSeekBarChangeListener; android.widget.TextView; android.widget.ToggleButton; public class SavedRecordings extends ListActivity { private static final String TAG = SavedRecordings.class.getName(); // SavedRecordingsAdapter displays list of saved recordings in ListView private SavedRecordingsAdapter savedRecordingsAdapter; private private private private private Fig. 16.19 | MediaPlayer mediaPlayer; // plays saved recordings SeekBar progressSeekBar; // controls audio playback Handler handler; // updates the SeekBar thumb position TextView nowPlayingTextView; // displays audio name ToggleButton playPauseButton; // displays audio name package statement, import statements and fields. (Part 2 of 2.) Overriding Activity Methods onCreate, onResume and onPause Method onCreate (Fig. 16.20, lines 50–73) inflates this ListActivity’s custom layout (line 54), then configures the ListView’s ArrayAdapter (lines 57–61). The SavedRecordingsAdapter class (Fig. 16.21) receives as its second constructor argument a List<String> that contains the list of files in the app’s external files directory. Line 60 obtains the list of filenames as an array of Strings by calling File method list, which we use to initialize an ArrayList<String>. Line 63 creates a Handler for updating the SeekBar’s thumb position during playback. Lines 66–72 get references to the other GUI components in the layout and register the listeners for the progressSeekBar and playPauseButton. 49 50 51 52 53 // called when the activity is first created @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Fig. 16.20 | Overriding Activity methods onCreate, onResume and onPause. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 22 Thursday, April 19, 2012 8:09 AM 16-22 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 Chapter 16 Voice Recorder App setContentView(R.layout.saved_recordings); // get ListView and set its listeners and adapter ListView listView = getListView(); savedRecordingsAdapter = new SavedRecordingsAdapter(this, new ArrayList<String>( Arrays.asList(getExternalFilesDir(null).list()))); listView.setAdapter(savedRecordingsAdapter); handler = new Handler(); // updates SeekBar thumb position // get other GUI components and register listeners progressSeekBar = (SeekBar) findViewById(R.id.progressSeekBar); progressSeekBar.setOnSeekBarChangeListener( progressChangeListener); playPauseButton = (ToggleButton) findViewById(R.id.playPauseButton); playPauseButton.setOnCheckedChangeListener(playPauseButtonListener); nowPlayingTextView = (TextView) findViewById(R.id.nowPlayingTextView); } // end method onCreate // create the MediaPlayer object @Override protected void onResume() { super.onResume(); mediaPlayer = new MediaPlayer(); // plays recordings } // end method onResume // release the MediaPlayer object @Override protected void onPause() { super.onPause(); if (mediaPlayer != null) { handler.removeCallbacks(updater); // stop updating GUI mediaPlayer.stop(); // stop audio playback mediaPlayer.release(); // release MediaPlayer resources mediaPlayer = null; } // end if } // end method onPause Fig. 16.20 | Overriding Activity methods onCreate, onResume and onPause. (Part 2 of 2.) Method onResume (lines 76–81)—which is called when after onCreate when the Activity first loads and when the Activity resumes after being paused—creates a MediaPlayer (introduced in Chapter 12) to play a selected recording. Method onPause (lines 84–96) ensures that audio playback stops if the app is paused. We do not know when or whether the app will resume, so we remove the updater Runnable from the handler, call Androidfp_16_voicerecorder.fm Page 23 Thursday, April 19, 2012 8:09 AM 16.5 Building the App MediaPlayer release method stop to terminate audio playback and call to release the resources used by the MediaPlayer. MediaPlayer 16-23 method Subclass of ArrayAdapter Class ViewHolder (Fig. 16.21, lines 100–105) and class SavedRecordingsAdapter (lines 108–159) use the view-holder pattern (introduced in Chapter 12) to populate the SavedRecordings ListActivity’s ListView, reusing ListView items for better performance. Class ViewHolder contains variables that reference the TextView and two ImageViews in a ListView item (defined in the layout saved_recordings_row.xml). The SavedRecordingsAdapter constructor (lines 113–120) sorts the List of filenames, then stores it in instance variable items. Then we store a reference to the LayoutInflater for later use. SavedRecordingsAdapter 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 // Class for implementing the view-holder pattern // for better ListView performance private static class ViewHolder { TextView nameTextView; ImageView emailButton; ImageView deleteButton; } // end class ViewHolder // ArrayAdapter displaying recording names and delete buttons private class SavedRecordingsAdapter extends ArrayAdapter<String> { private List<String> items; // list of filenames private LayoutInflater inflater; public SavedRecordingsAdapter(Context context, List<String> items) { super(context, -1, items); // -1 indicates we're customizing view Collections.sort(items, String.CASE_INSENSITIVE_ORDER); this.items = items; inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); } // end SavedRecordingsAdapter constructor @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; // holds references to current item's GUI Fig. 16.21 | // if convertView is null, inflate GUI and create ViewHolder; // otherwise, get existing ViewHolder if (convertView == null) { convertView = inflater.inflate(R.layout.saved_recordings_row, null); // set up ViewHolder for this ListView item viewHolder = new ViewHolder(); SavedRecordingsAdapter subclass of ArrayAdapter. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 24 Thursday, April 19, 2012 8:09 AM 16-24 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 Chapter 16 Voice Recorder App viewHolder.nameTextView = (TextView) convertView.findViewById(R.id.nameTextView); viewHolder.emailButton = (ImageView) convertView.findViewById(R.id.emailButton); viewHolder.deleteButton = (ImageView) convertView.findViewById(R.id.deleteButton); convertView.setTag(viewHolder); // store as View's tag } // end if else // get the ViewHolder from the convertView's tag viewHolder = (ViewHolder) convertView.getTag(); // get and display name of recording file String item = items.get(position); viewHolder.nameTextView.setText(item); // configure listeners for email and delete "buttons" viewHolder.emailButton.setTag(item); viewHolder.emailButton.setOnClickListener(emailButtonListener); viewHolder.deleteButton.setTag(item); viewHolder.deleteButton.setOnClickListener(deleteButtonListener); return convertView; } // end method getView } // end class SavedRecordingsAdapter Fig. 16.21 | SavedRecordingsAdapter Overridden ArrayAdapter subclass of ArrayAdapter. (Part 2 of 2.) method getView (lines 122–158) determines whether a ListView item needs to be inflated (line 129). If so, lines 131–142 inflate the layout for an item, configure a new ViewHolder object and set that as the ListView item’s tag. Otherwise, we simply get the existing ViewHolder from the ListView item’s tag (line 145). Lines 148–149 get the corresponding filename from the items List, assign it to item and use it to set the TextView’s text. Then lines 152–155 set item as the tag for the “buttons” emailButton and deleteButton, and register their event listeners. Recall that these “buttons” are actually ImageViews so that the ListView items can also be clickable (see Section 16.4.7). OnClickListener emailButtonListener When the user touches the e-mail icon ( ) for a given recording, the emailButtonListener (Fig. 16.22) lets the user attach the recording to an e-mail. Method onClick gets a Uri from a File created using the selected recording’s path in the app’s external files directory (lines 168–169). Lines 172–174 create and configure an Intent using Intent's ACTION_SEND and set the Intent’s MIME type to text/plain. This can be handled by any apps capable of sending plain text messages, such as e-mail apps. We include the recording’s Uri as an extra with Intent’s EXTRA_STREAM constant. Most e-mail clients (including Android’s) will attach the file at the given Uri to an e-mail draft in response to this Intent. We pass the Intent and a String title to Intent’s createChooser method (line 175) to create an Activity chooser for the new Intent. It’s important to set the title of the Activity chooser to remind the user to select an e-mail app to receive the expected behavior—you cannot control the apps installed on a user’s phone and the Intent filters that Androidfp_16_voicerecorder.fm Page 25 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 16-25 can launch those apps, so it’s possible that incompatible activities could appear in the chooser. Lines 175–176 launch the Intent. 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 // sends specified recording as e-mail attachment OnClickListener emailButtonListener = new OnClickListener() { @Override public void onClick(final View v) { // get Uri to the recording's location on disk Uri data = Uri.fromFile( new File(getExternalFilesDir(null), (String) v.getTag())); // create Intent to send Email Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_STREAM, data); startActivity(Intent.createChooser(intent, getResources().getString(R.string.emailPickerTitle))); } // end method onClick }; // end OnClickListener Fig. 16.22 | OnClickListener emailButtonListener. OnClickListener deleteButtonListener The deleteButtonListener OnClickListener (Fig. 16.23) displays an AlertDialog to confirm whether the recording should be deleted. If the user touches the dialog’s Delete Button, lines 197–198 create a File object that represents the recording to delete, then line 199 calls File method delete to delete the file from the device. Line 200 removes the corresponding item from the savedRecordingsAdapter. 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 // deletes the specified recording OnClickListener deleteButtonListener = new OnClickListener() { @Override public void onClick(final View v) { // create an input dialog to get recording name from user AlertDialog.Builder confirmDialog = new AlertDialog.Builder(SavedRecordings.this); confirmDialog.setTitle(R.string.dialog_confirm_title); confirmDialog.setMessage(R.string.dialog_confirm_message); Fig. 16.23 | confirmDialog.setPositiveButton(R.string.button_delete, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { OnClickListener deleteButtonListener. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 26 Thursday, April 19, 2012 8:09 AM 16-26 197 198 199 200 201 202 203 204 205 206 207 208 209 Chapter 16 Voice Recorder App File fileToDelete = new File(getExternalFilesDir(null) + File.separator + (String) v.getTag()); fileToDelete.delete(); savedRecordingsAdapter.remove((String) v.getTag()); } // end method onClick } // end anonymous inner class ); // end call to setPositiveButton confirmDialog.setNegativeButton(R.string.button_cancel, null); confirmDialog.show(); } // end method onClick }; // end OnClickListener Fig. 16.23 | OnClickListener deleteButtonListener. (Part 2 of 2.) Overriding ListActivity Method onListItemClick Overridden ListActivity method onListItemClick (Fig. 16.24) uses a MediaPlayer to play the selected recording when the user touches a filename in the ListView. Line 221 gets the selected filename. Lines 224–225 create a String containing the recording’s absolute path. Lines 234–236 reset the mediaPlayer, set its data source to the selected recording’s path and prepare the mediaPlayer to play the recording. Line 237 uses MediaPlayer’s getDuration method to get the recording’s total duration, then sets that as the progressSeekBar’s maximum value using SeekBar’s setMax method. This allows the SeekBar’s thumb position to represent the recording’s playback position, since the thumb position will have a one-to-one correlation with the recording’s duration. Lines 239–249 configure the MediaPlayer’s OnCompletionListener to reset the playPauseButton and progressSeekBar. Line 250 starts playing the recording and line 251 calls the updater Runnable’s run method to begin updating the progressSeekBar based on the current playback position. 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); playPauseButton.setChecked(true); // checked state handler.removeCallbacks(updater); // stop updating progressSeekBar // get the item that was clicked TextView nameTextView = ((TextView) v.findViewById(R.id.nameTextView)); String name = nameTextView.getText().toString(); // get path to file String filePath = getExternalFilesDir(null).getAbsolutePath() + File.separator + name; Fig. 16.24 | Overriding ListActivity method onListItemClick. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 27 Thursday, April 19, 2012 8:09 AM 16.5 Building the App 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 16-27 // set nowPlayingTextView's text nowPlayingTextView.setText(getResources().getString( R.string.now_playing_prefix) + " " + name); try { // set the MediaPlayer to play the file at filePath mediaPlayer.reset(); // reset the MediaPlayer mediaPlayer.setDataSource(filePath); mediaPlayer.prepare(); // prepare the MediaPlayer progressSeekBar.setMax(mediaPlayer.getDuration()); progressSeekBar.setProgress(0); mediaPlayer.setOnCompletionListener( new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { playPauseButton.setChecked(false); // unchecked state mp.seekTo(0); } // end method onCompletion } // end OnCompletionListener ); // end call to setOnCompletionListener mediaPlayer.start(); updater.run(); // start updating progressSeekBar } // end try catch (Exception e) { Log.e(TAG, e.toString()); // log exceptions } // end catch } // end method onListItemClick Fig. 16.24 | Overriding ListActivity method onListItemClick. (Part 2 of 2.) OnSeekBarChangeListener progressChangeListener The OnSeekBarChangeListener (Fig. 16.25) responds to the progressSeekBar’s events. When the user drags the SeekBar thumb, method onProgressChanged (lines 263–268) calls SeekBar’s getProgress method to get the current thumb position and—if the user initiated the event (i.e., parameter fromUser is true)—passes this to MediaPlayer’s seekTo method to continue playback from the corresponding point in the recording. OnSeekBarChangeListener’s other methods are not used in this app, so they’re defined with empty bodies. , 259 260 261 262 263 264 265 266 // reacts to events created when the Seekbar's thumb is moved OnSeekBarChangeListener progressChangeListener = new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { Fig. 16.25 | OnSeekBarChangeListener progressChangeListener. (Part 1 of 2.) Androidfp_16_voicerecorder.fm Page 28 Thursday, April 19, 2012 8:09 AM 16-28 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 Chapter 16 Voice Recorder App if (fromUser) mediaPlayer.seekTo(seekBar.getProgress()); } // end method onProgressChanged @Override public void onStartTrackingTouch(SeekBar seekBar) { } // end method onStartTrackingTouch @Override public void onStopTrackingTouch(SeekBar seekBar) { } // end method onStopTrackingTouch }; // end OnSeekBarChangeListener Fig. 16.25 | OnSeekBarChangeListener progressChangeListener. (Part 2 of 2.) Runnable updater The updater Runnable (Fig. 16.26) moves the SeekBar’s thumb as a recording plays. If the MediaPlayer is playing, we call MediaPlayer’s getCurrentPosition method to get the current playback position and use that value to set the SeekBar’s thumb position by calling setProgress. Line 291 calls the handler’s postDelayed method to post this Runnable once every 100 milliseconds. 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 // updates the SeekBar every second Runnable updater = new Runnable() { @Override public void run() { if (mediaPlayer.isPlaying()) { // update the SeekBar's position progressSeekBar.setProgress(mediaPlayer.getCurrentPosition()); handler.postDelayed(this, 100); } // end if } // end method run }; // end Runnable Fig. 16.26 | Runnable updater. OnCheckedChangeListener playPauseButtonListener The playPauseButtonListener OnClickListener’s onClick method (Fig. 16.27) toggles between pausing and playing the selected recording when the user touches the playPauseToggleButton. If the ToggleButton is checked, we start the audio playback using MediaPlayer’s start method, then call the updater Runnable’s run method; otherwise, we call MediaPlayer’s pause method to pause the audio playback. Androidfp_16_voicerecorder.fm Page 29 Thursday, April 19, 2012 8:09 AM 16.6 Wrap-Up 16-29 297 // called when the user touches the "Play" Button 298 OnCheckedChangeListener playPauseButtonListener = 299 new OnCheckedChangeListener() 300 { 301 // toggle play/pause 302 @Override 303 public void onCheckedChanged(CompoundButton buttonView, 304 boolean isChecked) 305 { 306 if (isChecked) 307 { 308 mediaPlayer.start(); // start the MediaPlayer 309 updater.run(); // start updating progress SeekBar 310 } 311 else 312 mediaPlayer.pause(); // pause the MediaPlayer 313 } // end method onCheckedChanged 314 }; // end OnCheckedChangedListener 315 } // end class SavedRecordings Fig. 16.27 | OnCheckedChangeListener playPauseButtonListener. 16.6 Wrap-Up The Voice Recorder app allowed the user to record sounds using the device’s microphone, save the recordings for playback later, delete the recordings and send the recordings as email attachments. To enable recording and saving files, this app’s manifest specified permissions for recording audio and for writing to a device’s external storage. To provide clickable areas in the SavedRecordings ListActivity’s ListView, we used clickable ImageViews rather than Buttons. This allowed the ListView items themselves to remain clickable as well. To display different icons based on a ToggleButton’s state, we defined a state list drawable in XML with a root <selector> element that contained <item> elements for each state. Each <item> element specified the drawable (such as an icon) to display for the corresponding state. We then set the state list drawable as the button’s drawable and Android automatically displayed the correct drawable based on the button’s state. We used a MediaRecorder (package android.media) to record the user’s voice via the device’s built-in microphone and saved the recordings to audio files on the device. We managed the recordings by initially saving each new recording in a temporary file, which we created with class File’s createTempFile method. When the user chose to save a temporary file, we used class File’s renameTo method to give the file a permanent name. When the user chose to delete a file, we removed it by calling File method delete. Finally, we used an Intent and an Activity chooser to allow the user to send a recording as an e-mail attachment via any app on the device that supported this capability. In the next chapter, we present the Enhanced Address Book app, which allows the user to transfer contacts between devices via a Bluetooth connection.