Sie sind auf Seite 1von 29

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, youll:

Specify permissions for recording audio and writing to a


devices external storage.

Record audio files using a MediaRecorder.

Create a visual representation of the users audio input.

Use an ArrayAdapter to display the names of a


directorys files in ListView.

Use the File class to save files in a devices external


storage directory.

Use the File class to delete files from a devices 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 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

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 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

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
users 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 users 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 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.]

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 thats set up for debugging to your computer, then right click
the apps 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
devices microphone, the apps 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 youre 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 dialogs 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
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.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 apps 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 apps <manifest> element contains <uses-permission> elements for recording audio (to use the microphone)
and for writing to a devices external storage (to save a recording).
Clickable ImageViews
In our custom ListView item layouts for the Slideshow apps (Chapters 1213), 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
Androids buttons can assume various states, depending on their type. For example, a
ToggleButton can be checked or unchecked. Sometimes its 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 ToggleButtons android:drawableTop attribute, and Android automatically displays the correct
Drawable based on the ToggleButtons state.
Using a MediaRecorder to Record Audio
The Voice Recorder app uses a MediaRecorder (package android.media) to record the
users voice. A MediaRecorder records audio using the devices 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 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.

16.4 Building the Apps GUI and Resource Files


In this section, we discuss the apps 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 Apps 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 SDKs platforms folder under each platform versions data/res/drawable-hdpi folder. We copied the icons that we use into this apps
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 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.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 Apps GUI and Resource Files

16-7

Views like the VisualizerView (lines 57 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) thats 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 dont show it here.

16.4.6 saved_recordings.xml: Layout for the SavedRecordings


ListActivity
Figure 16.5 shows the SavedRecordings ListActivitys layout. Recall that you must provide a ListView with its android:id set to "@android:id/list" (lines 2628) when customizing a ListActivitys layout. This layout introduces one new featurethe
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 ToggleButtons state. The
items displayed in this layouts 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
ImageViewsspecified by setting the android:clickable attribute to true for each
ListView (lines 16 and 22).

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

<?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 ListActivitys 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 thats displayed for ToggleButtons 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 Drawablethis will place the file into the
projects /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 36 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 ToggleButtonthat is, when its checked and when its 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 Activitys Record ToggleButton.

16.5 Building the App


This app consists of three classesVoiceRecorder (an Activity subclass, Figs. 16.8
16.14), VisualizerView (a View subclass, Figs. 16.1516.18) and SavedRecordings (a
ListActivity subclass, Figs. 16.1916.27).

16.5.1 VoiceRecorder Subclass of Activity


VoiceRecorder is the apps 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 theyre 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 5058 of method onCreate get references to the layouts ToggleButton, Buttons and VisualizerView, and disable the saveButton and deleteButton. Lines 6164 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 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

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 recordings 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 methods isChecked parameter is true, lines 109148 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 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.

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 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.

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 MediaRecorders 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 dialogs Save button and the name specified by the user is not an empty String, lines 210215 give the file a permanent name
specified by the user. Line 210 gets the File object that was set as the Buttons tag in line
131 of Fig. 16.10. Lines 211214 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 224229 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 dialogs Delete
button, line 257 gets the File object that was set as the Buttons 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.1516.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 classs constructor (lines 2531) 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 thats 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
changessuch as when its added to the VoiceRecorder layouts view hierarchy. We store
the VisualizerViews 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 4346) empties the amplitudes List to prepare for
visualizing a new recording. Method addAmplitude (lines 4958) 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 screens width (line 54), we remove the oldest value, which represents the leftmost line on the screenthis 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
VisualizerViews 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 Views invalidate method. Lines 6876 process the amplitudes List. For each amplitude, we calculate a scaledHeight (line 70) that
represents the amplitudes line length on the screen, then determine the new x-coordinate
of the line. Lines 7475 then draw the line centered vertically on the View using Canvass
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.1916.27) displays the users
saved recordings in a ListView. The user can touch a recordings 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 devices 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 classs fields as theyre 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 5073) inflates this ListActivitys custom layout (line
54), then configures the ListViews ArrayAdapter (lines 5761). The SavedRecordingsAdapter class (Fig. 16.21) receives as its second constructor argument a List<String> that
contains the list of files in the apps 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 SeekBars thumb position during
playback. Lines 6672 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 7681)which is called when after

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

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 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

// 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 122158) determines whether a

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

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 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

// 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 dialogs Delete
Button, lines 197198 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 224225 create a String containing the recordings absolute
path. Lines 234236 reset the mediaPlayer, set its data source to the selected recordings
path and prepare the mediaPlayer to play the recording. Line 237 uses MediaPlayers getDuration method to get the recordings total duration, then sets that as the progressSeekBars maximum value using SeekBars setMax method. This allows the SeekBars thumb
position to represent the recordings playback position, since the thumb position will have a
one-to-one correlation with the recordings duration. Lines 239249 configure the MediaPlayers OnCompletionListener to reset the playPauseButton and progressSeekBar. Line
250 starts playing the recording and line 251 calls the updater Runnables 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 progressSeekBars events.


When the user drags the SeekBar thumb, method onProgressChanged (lines 263268) calls
SeekBars getProgress method to get the current thumb position andif the user initiated
the event (i.e., parameter fromUser is true)passes this to MediaPlayers seekTo method
to continue playback from the corresponding point in the recording. OnSeekBarChangeListeners other methods are not used in this app, so theyre 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 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

// 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 OnClickListeners 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 MediaPlayers start method, then call the updater Runnables run method; otherwise, we call
MediaPlayers 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 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.

Das könnte Ihnen auch gefallen