Most apps employing background audio
are going to want to provide an enhanced interface within the
foreground app for controlling playback and displaying track
information. This section looks at the MainPage
and associated classes of the WPUnleashed.BackgroundAudio project. The page allows the user to play audio via the BackgroundAudioPlayer
class. It provides application bar buttons for play and pause, stop,
rewind, and forward, and several fields for displaying the current
track information.
Note
It is a Windows Phone Marketplace
certification requirement that when the user is already playing
background music on the phone when your app is launched, your app must
ask the user for consent to stop playing or to adjust the background
music. This prompt must occur each time the app launches, unless there
is an opt-in setting provided to the user, and the user has used this
setting to opt in.
A Testable BackgroundAudioPlayer
The BackgroundAudioPlayer
class is inherently difficult to use in unit tests because of the cross-process interaction with the AudioPlayerAgent
and its dependence on native phone components. For this reason I have provided a wrapper called BackgroundAudioPlayerProxy
, which implements a custom IBackgroundAudioPlayer
interface. In addition, a second implementation, MockBackgroundAudioPlayer
,
allows you to test whether your code called the background audio player
as expected; it allows you to test, for example, whether your code
attempted to play a track.
MainPageViewModel
The viewmodel is declared as a field of the MainPage, as shown:
readonly MainPageViewModel viewModel = new MainPageViewModel(
new BackgroundAudioPlayerProxy(),
new IsolatedStorageUtility());
The MainPageViewModel
class constructor accepts an IBackgroundAudioPlayer
as well as an IIsolatedStorageUtility
instance (see Listing 1). A subscription to the PlayStateChanged
event of the background audio player allows the viewmodel to update various properties whenever the play state changes.
The viewmodel contains five commands for controlling the BackgroundAudioPlayer
while the app is in the foreground, which provide for playing, pausing, stopping, and skipping between tracks.
The viewmodel’s Refresh
method is called to update the viewmodel, which relies on the state of the BackgroundAudioPlayer
. A Timer
is used to monitor the progress of the track while it is being played.
LISTING 1. MainPageViewModel
Constructor
public MainPageViewModel(
IBackgroundAudioPlayer backgroundAudioPlayer,
IIsolatedStorageUtility isolatedStorageUtility)
{
player = ArgumentValidator.AssertNotNull(
backgroundAudioPlayer, "backgroundAudioPlayer");
ArgumentValidator.AssertNotNull(
isolatedStorageUtility, "isolatedStorageUtility");
player.PlayStateChanged += HandlePlayStateChanged;
BackgroundAudioPlayerAgent.CopyAudioToIsolatedStorage(isolatedStorageUtility);
playCommand = new DelegateCommand(obj => player.Play());
pauseCommand = new DelegateCommand(obj => player.Pause());
stopCommand = new DelegateCommand(obj => player.Stop());
previousTrackCommand = new DelegateCommand(obj => player.SkipPrevious());
nextTrackCommand = new DelegateCommand(obj => player.SkipNext());
Refresh(false);
timer = new Timer(HandleTimerTick, null, 3000, 1000);
}
The viewmodel’s TrackArtist
and TrackTitle
properties reflect the Artist
and Title
properties of the current AudioTrack
.
The viewmodel’s VisualState
property, which is of type string
, determines the visual state of the view, and the visibility of various application bar buttons. The VisualState
value is set to the PlayerState
property of the BackgroundAudioPlayer
, as shown:
void Refresh(bool setVisualState = true)
{
switch (player.PlayerState)
{
case PlayState.Playing:
CanPause = true;
TrackArtist = player.Track.Artist;
TrackTitle = player.Track.Title;
break;
case PlayState.Paused:
CanPause = false;
break;
}
BufferingProgress = player.BufferingProgress;
if (setVisualState)
{
VisualState = player.PlayerState.ToString("G");
}
if (player.Error != null)
{
MessageService.ShowError("A problem occured:" + player.Error);
}
}
When the viewmodel’s VisualState
property changes, the view responds by refreshing the VisualStateManager
state. This is performed from the MainPage
constructor, as shown:
public MainPage()
{
InitializeComponent();
DataContext = viewModel;
viewModel.PropertyChanged
+= (sender, args) =>
{
if (args.PropertyName == "VisualState")
{
SetVisualState();
}
};
}
The view’s custom AppBar
includes icon buttons bound to the viewmodel’s commands, as shown:
<u:AppBar>
<u:AppBarIconButton
Command="{Binding PreviousTrackCommand}"
Text="Previous"
IconUri="/Images/ApplicationBarIcons/Previous.png" />
<!-- A toggle button is used rather than visual state to avoid
repopulating the app bar when the user taps play or pause. -->
<u:AppBarToggleButton
x:Name="Button_Play"
Command1="{Binding PlayCommand}"
Text1="Play"
Icon1Uri="/Images/ApplicationBarIcons/Play.png"
Command2="{Binding PauseCommand}"
Text2="Pause"
Icon2Uri="/Images/ApplicationBarIcons/Pause.png"
Toggled="{Binding CanPause}"/>
<u:AppBarIconButton
x:Name="Button_Stop"
Command="{Binding StopCommand}"
Text="Stop"
IconUri="/Images/ApplicationBarIcons/Stop.png" />
<u:AppBarIconButton
Command="{Binding NextTrackCommand}"
Text="Previous"
IconUri="/Images/ApplicationBarIcons/Next.png" />
</u:AppBar>
The viewmodel’s ViewState
property determines the visibility of each button. The XAML that defines the VisualStateGroups
is not shown, but if you are interested, see the downloadable sample code.
Two TextBlock
controls are used to display the viewmodel’s TrackArtist
and TrackTitle
properties, like so:
<TextBlock Text="{Binding TrackArtist}"
Style="{StaticResource PhoneTextTitle3Style}"
TextWrapping="Wrap" />
<TextBlock Text="{Binding TrackTitle}"
Style="{StaticResource PhoneTextTitle2Style}"
TextWrapping="Wrap" />
Whenever the track changes, these fields are updated according to the new track.