Beruflich Dokumente
Kultur Dokumente
16
Voice Recorder App
Audio Recording with MediaRecorder, Audio Playback
with MediaPlayer, Sending a File as an E-Mail Attachment
Objectives
In this chapter, youll:
Outline
16-2
16.1 Introduction
16.2 Test-Driving the Voice Recorder
App
16.3 Technologies Overview
16.4 Building the Apps GUI and Resource
Files
16.4.1 Creating the Project
16.4.2 Using Standard Android Icons in the
Apps 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
VoiceRecorder Activity
Activity
16.6 Wrap-Up
16.1 Introduction
The Voice Recorder app allows the user to record sounds using the phones microphone and
save the audio files for playback later. The apps 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 users voice
(Fig. 16.2). When the user touches the Stop ToggleButton, the Save and Delete buttons are
Visualization area
Record Button
16.1 Introduction
16-3
Visualization of the
users recording
Fig. 16.3 |
AlertDialog
The user can touch the View Saved Recordings Button to view the list of previously
saved recordings (Fig. 16.4(a)). Touching a recordings 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 recordings
icon allows you to send the recording via e-mail.
Touching a recordings
icon allows you to delete the recording (after you confirm by
touching Delete in the confirmation dialog thats displayed).Touching the devices back
button returns the user to the apps main Activity. [Note: This apps recording capabilities
require an actual Android device for testing purposes. At the time of this writing, the Android
emulator does not provide microphone support.]
16-4
ToggleButton
toggles between
Play and Pause
Touch
to
delete recording
Touch
to
e-mail recording
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
playit will load and begin playing immediately. Slide the SeekBar thumb to adjust the
recordings 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 recordingtouch it to continue playback. To return to the apps main Activity,
touch the devices back button.
16-6
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 Files createTempFile method. When the user chooses to save that file, we use class Files 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.
Build Target:
16.4.3 AndroidManifest.xml
As in prior apps with multiple activities, this apps AndroidManifest.xml file contains
<activity> elements for each ActivityVoiceRecorder 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-7
Views like the VisualizerView (lines 57 in the file) must be declared with their package
and class names.
16-8
itys ListView.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Fig. 16.6 | Layout for the items in the SavedRecordings ListActivitys ListView.
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
Fig. 16.7 | Custom drawable for the VoiceRecorder Activitys Record ToggleButton.
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.
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
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
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;
Fig. 16.8 |
package
Fig. 16.9 | Overriding Activity methods onCreate, onResume and onPause. (Part 1 of 2.)
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 7077) sets recordButtons listener. Method onPause (lines
8097) removes recordButtons 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 9091 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
16-12
Chapter 16
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 recordings file.
// 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 |
16-13
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 |
Lines 115122 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 devices 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 118119 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 audiodifferent encoders yield results of
different quality. We chose AAC because its designed for better quality audio
(which also consumes more storage). Android also provides an AMR encoder,
which is geared to voice data thats being transmitted over cellular networks.
Line 121 calls setAudioEncodingBitRate to specify the recordings 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.
16-14
Chapter 16
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 127139 create a temporary file to store
the recording and begin the recording process. Line 127128 create the file by calling class
Files static method createTempFile. The first two arguments are the temporary filenames prefix and the extension. The last argument is the files location. Recall from
Chapter 13 that method getExternalFilesDir returns a File representing an application-specific external storage directory thats automatically managed by the systemif you
delete this app, its files are deleted as well. Lines 131132 set the tempFile object as the
tag on the saveButton and deleteButton so the tempFile can be used in each Buttons
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 Handlers post method.
If the onCheckedChanged methods isChecked parameter is false, lines 152157
configure the activity to allow the user to save or delete the temporary recording file. Lines
152153 call MediaRecorders 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.
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
Fig. 16.11 |
Runnable updateVisualizer
16-15
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 |
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
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
Fig. 16.13 |
confirmDialog.setPositiveButton(R.string.button_delete,
new DialogInterface.OnClickListener()
{
OnClickListener deleteButtonListener
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
16-17
Fig. 16.13 |
OnClickListener deleteButtonListener
The
viewSavedRecordings OnClickListener
ings Activity
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
Recordings ListActivity.
16-18
Chapter 16
Paint
// 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;
Fig. 16.15 |
package
39
40
41
16-19
16-20
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Chapter 16
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
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;
Fig. 16.19 |
package
Fig. 16.20 | Overriding Activity methods onCreate, onResume and onPause. (Part 1 of 2.)
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
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
onCreate
when the
Activity first loads and when the Activity resumes after being pausedcreates a MediaPlayer (introduced in Chapter 12) to play a selected recording. Method onPause (lines
8496) 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
MediaPlayer
16-23
method
Subclass of ArrayAdapter
Class ViewHolder (Fig. 16.21, lines 100105) and class SavedRecordingsAdapter (lines
108159) use the view-holder pattern (introduced in Chapter 12) to populate the SavedRecordings ListActivitys 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 113120) 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
Fig. 16.21 |
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
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
method
getView
ListView item needs to be inflated (line 129). If so, lines 131142 inflate the layout for an
item, configure a new ViewHolder object and set that as the ListView items tag. Otherwise, we simply get the existing ViewHolder from the ListView items tag (line 145). Lines
148149 get the corresponding filename from the items List, assign it to item and use it
to set the TextViews text. Then lines 152155 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 recordings path in the apps external files directory (lines 168169). Lines 172174 create and configure an Intent using Intent's
ACTION_SEND and set the Intents 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 recordings Uri as an extra with Intents EXTRA_STREAM constant. Most e-mail clients (including
Androids) 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 Intents createChooser method (line 175) to
create an Activity chooser for the new Intent. Its important to set the title of the Activity chooser to remind the user to select an e-mail app to receive the expected behavioryou cannot control the apps installed on a users phone and the Intent filters that
16-25
can launch those apps, so its possible that incompatible activities could appear in the
chooser. Lines 175176 launch the Intent.
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
Fig. 16.22 |
OnClickListener emailButtonListener.
OnClickListener deleteButtonListener
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.)
16-26
197
198
199
200
201
202
203
204
205
206
207
208
209
Chapter 16
Fig. 16.23 |
OnClickListener deleteButtonListener.
(Part 2 of 2.)
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;
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
Fig. 16.25 |
OnSeekBarChangeListener progressChangeListener.
(Part 1 of 2.)
16-28
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
Chapter 16
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 SeekBars thumb as a recording plays. If
the MediaPlayer is playing, we call MediaPlayers getCurrentPosition method to get
the current playback position and use that value to set the SeekBars thumb position by
calling setProgress. Line 291 calls the handlers 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
Fig. 16.26 |
Runnable updater.
OnCheckedChangeListener playPauseButtonListener
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 devices microphone,
save the recordings for playback later, delete the recordings and send the recordings as email attachments. To enable recording and saving files, this apps manifest specified permissions for recording audio and for writing to a devices external storage.
To provide clickable areas in the SavedRecordings ListActivitys 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 ToggleButtons 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 buttons drawable and
Android automatically displayed the correct drawable based on the buttons state.
We used a MediaRecorder (package android.media) to record the users voice via the
devices 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 Files createTempFile method. When the user chose to save
a temporary file, we used class Files 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.