Building Up a Media Center Application with TWUIK

Part of the “Coding MIDlet with TWUIK” Tutorial Series

TUTORIAL PART 3
The Audio Player

Introduction

As a recap, in the previous part of this tutorial, we looked at the implementation of the Photo Gallery screen, from where you have learnt how to use the TFFXGeometry engine for rescaling and/or rotating the photo images as well the various transition FX engines for the slide show feature. We have also seen the built-in widget PhotoPreviewList that is suitable for showing gallery of photos in thumbnail size. We used the JSR-75 file connection API to search for PNG and JPEG image files in the file system. These formats have a built-in support in TWUIK. We also looked at how a custom widget can be created in TWUIK, exemplified by the IndicatorBar widget that we then used in the Photo Gallery screen.

In this third and last instalment of the tutorial series, we will look at the implementation of the Audio Player screen. For this, we are going to see a few more features of TWUIK that we have not previously used. First, we will use another TWUIK built-in widget called PowerList for the audio file listing purpose. A new thing we have not seen before is the TINY PopupWindow library that provides you the ability to conveniently show dialog boxes such as alerts and input prompts. Another feature would be the transformation FX engine called TFFXColor, with which we are going to perform some color-space transformation in creating a visualization effects when the player is in playback.

The audio file listing will be built based on using the JSR-75 file connection API again to search for supported audio files on the file system of the device, like we did previously for the Photo Gallery. For playback of the audio files, we are going to use the Mobile Media API (MMAPI) as there is no built-in support in TWUIK yet (but in the near future it will, i.e., version 2.0). And also we are going to use the flashing-ball mode of the IndicatorBar widget that we have created previously when the player is in playback, thus showing an animated progress along the duration of the audio.

And in the end, to complete the tutorial series as well as the implementation of our multi media center application, we are going to take a shortcut by making the Audio Player to support video files as well. That is, it is in fact a Media Player screen that we are going to implement eventually. Therefore, the file listing will include both supported audio files and supported video files that are found in the file system of the device. The implementation of the video playback will feature a discussion about how to perform a screen transition effects as we will switch to a new screen whenever a video file is played. So let us get started with implementing the screen as the Audio Player screen first.

Screen Design

Now we are going to look at the design for our third screen in the application, the Audio Player. In summary, in this screen the user can choose to play an audio file from the given listing, and then can perform basic playback functionalities such as pause, stop, seek, previous, or next. The user can also turn up/down the volume level or switch to/from mute mode. Furthermore, she can also show/hide the visualization, which will animate when the player is in playback. And when this is shown, she can switch it to the full-screen mode.

In essence, we allow the user to navigate between the buttons in the FishEyeMenu to change the current interaction state. For example, when a playlist button is highlighted, the UI is in the playlist state, i.e., the user can then navigate the audio file listing up and down. On the other hand, when a volume button is highlighted, the UI is in the volume state, i.e., the user can then turn up/down the volume level. And similarly, this mechanism applies for the other buttons. Furthermore, some of the buttons will have an alternative state. For example, the play button will become a pause button when it is fired. On the other hand, the volume button will become a mute button when it is fired. And similarly, this applies for the other buttons.

For playing the currently selected audio file, the user can press the FIRE key if the UI is in the playlist mode, or alternatively the user can fire the play menu button. The other buttons serve the remaining functionalities commonly found in an audio player. From this screen, the user can also go back to the main menu or to exit the application. So, let us take a look at the UI design in further details first:

1. VolumeBar

2. PowerList

3. Visualization

4. IndicatorBar

5. FishEyeMenu

Note that the layer ordering for the screen-level components should be like this: the ImageVisualization is added to the canvas first (thus making it the most-background), then followed by the BeatVisualization, and then the PowerList, and then the VolumeBar (when shown). Also, for the PowerList, when the UI is in the playlist state, a highlight bar will be shown on the currently selected file. Another thing to note is that when the visualization is switched to the full-screen mode, no other components (including the application-level components) will be hidden, leaving only the ImageVisualization and the BeatVisualization alone, but then occupying the whole screen.

That’s all for the warming up. Let’s get started with the details and the coding now.

Using the PowerList Widget for the Audio File Listing

The Audio Player will support MIDI, WAV, and MP3 audio files. In our design, we show a listing of the supported audio files found on the file system of the device. TWUIK provides a built-in widget called PowerList. This is a perfect widget for our audio file listing purpose. It displays a list of text items with different fonts (typically of different sizes, where the selected on has the largest size and those closer to it are larger than those farther from it). It also shows the list in a dynamic animated way as it always tries to center the currently selected item. So, without further ado, let us look at how to use this widget in details. Following is the API signature for constructing a PowerList object:

public PowerList (int iAnimTickCount, int iX, int iY, int iWidth, 
                  int iDisplayedItemCount, TwuikFont[] fonts)

The PowerList widget offers a certain degree of customization when it is constructed (based on the parameters we pass for the aforementioned constructor). First, you specify how many items are displayed at any one time. Then you also specify what fonts are to be used, as an array (ideally should be different in sizes, starting from the smallest one at index 0 and increasing). Then the widget will automatically distribute the fonts among the items being displayed such that the selected one will always have the largest font (i.e., font at the last index of the provided array).

PowerList also provides the following API methods for appending items and controlling appearance:

public final void configureHighlightBar (int iHighlightBarColor,
                                   boolean bTransparentHighlightBar,
                                   byte btHighlightBarTransparency)
public final void clear()
public final void appendItem (String itemStr, int iIconIndex)
public final int getCurrentSelectedIndex()
public final void showHideSelectionFocus (boolean bShow)
public final boolean isSelectionFocusShown()

The above API methods are straightforward and self-explanatory in their names. Now that we know the basics of customizing the PowerList widget when it is constructed as well as for using it after that, let us take a look at the code in our application that creates and sets it up below:

class AudioPlayer extends  MMCScreen 
                  implements ComponentEventListener{
/*Private Constants*/
private static final int FRAMEDELAY = 1000 / 25; 
  /*Controller-midlet relateds*/
  private MMCMIDlet midlet;
/*Components*/
private PowerList pwl; 
  private VolumeBar vb; 
  /*Working variables*/   
  private int iFiredButtonID; 
  private Player player;
  private VolumeControl volumeControl;
  private long lMediaDuration;
  private long lPausedMediaTime;
  private int iCurVolume;  
  private boolean bMute;   
  private boolean bEverPlayed;
  private boolean bShowVis; 
  private boolean bInSeekMode;
  private boolean bInFullscreenMode; 
  private static LinkedList fileNamesNURLs;
  static{ //Search for audio files in root and some particular
          //folders of each available+accessible file system...   
fileNamesNURLs = MMCMIDlet.searchForMediaFiles (
                       new String[]{
                           "",
                          "Music/",
                          "Sounds/",
                           "Sounds/Digital",
                          "Nokia/Sounds/",
                          "Nokia/Sounds/Digital",                 
                        },
                        new String[]{
                           "WAV",
                           "MP3",
                           "MID",
                        });
};     
public AudioPlayer (MMCMIDlet midlet){       
   /*Initialize canvas-relateds*/
    this.midlet = midlet;
    canvas = new AnimationCanvas (FRAMEDELAY);
    canvas.setBgColor (0x000000);
    canvas.startStopAnimation();       
    /*Initialize components*/         
    pwl = new PowerList (1,0,0,canvas.getWidth(),
                          7,
               new TwuikFont[]{ MMCMIDlet.fontSysPropSmallPlain,
                                  MMCMIDlet.fontSysPropMediumBold,
                                  MMCMIDlet.fontSysPropLargePlain,
                                  MMCMIDlet.fontSysPropLargeBold,
                              });   
pwl.configureHighlightBar (0x996633,false,(byte) 75);
    pwl.setEventListener (this);
    vb = new VolumeBar (1,0,5);
    vb.iX = canvas.getWidth() - vb.getWidth() - 5;
    /*Initialize working variables*/
    bReadyToAcceptInputEvents = false;
    bMenuCanBeInteracted = true;           
    bEverPlayed = false;
    bMute = false;
    iCurVolume = 100;
   bShowVis = false;
   bInSeekMode = false;
   bInFullscreenMode = false;
}
...
public void menuEntryAnimEnded(){           
   pwl.startStopAnimation();
   canvas.addComponent (pwl); //add the PowerList
   for (int i=0; i < fileNamesNURLs.size(); i++){
      pwl.appendItem (((String[])fileNamesNURLs.elementAt (i))[0],
               -1);
   };
   pwl.showHideSelectionFocus (true);
   vb.setVisibleMode (false); //initially hidden first
  canvas.addComponent (vb); //add the VolumeBar
     
   bReadyToAcceptInputEvents = true;
   bMenuCanBeInteracted = true;                       
}
...
}

In the code above, basically we first search for WAV, MP3, or MIDI files in certain folders on every root available on the file system of the device, similar to what we did for the image files previously. For this purpose, we call the searchForMediaFiles method that we have created in tutorial part 1 which will return a list of filename-and-URL pairs. And then at the time the Audio Player screen is ready, we then initialize the PowerList with the file names of the audio files from this list (note that we pass -1 for the icon index to the appendItem method as we do not want any icon image to be displayed; we will use this later in the final section when we extend the Audio Player to support video as well, for now just ignore it first).

Note that there is a declaration of a widget variable of the type VolumeBar. This is our own custom widget whose implementation will not be shown here. Nevertheless, it is a very simple indicator widget, which we use for showing the current volume level of the player when the UI is in the volume state (which we will discuss later on). It provides the two simple API methods: getCurVolume and setCurVolume. We will see this again when we discuss about using it later.

You can also note that there are declarations of many working variables. We will ignore them first, we just show their declarations first here but we will use them later mostly for the playback purpose.

That is all for creating and setting up the PowerList widget. Now for the action, PowerList provides the following stimulus API methods:

public final void selectPrev()
public final void selectNext()

As their names say, selectPrev is for navigating the selection to the previous item in the list (if any), while selectNext is for navigating to the next. Thus in the keyPressed event we process the UP and DOWN keys for calling the selectPrev and selectNext mehods, respectively. The code for this is straightforward and thus will not be shown here. 

Implementing the UI Interaction Mechanism

Earlier, we have mentioned about the UI interaction state that we employ for the screen design. Here we discuss how we implement it. Basically there are buttons that can switch the UI interaction state when they are being selected: playlist, volume, and seek. Other buttons when selected would reset the UI interaction state, that is, to one that does not accept any other input except for switching the menu button selection. For allowing the user to switch to any one of those three UI states as to follow our design, we implement the following code:

public void  menuSelectionChanged (int iButtonID){    
  /*For switching to playlist state*/
  if (iButtonID != ButtonFactory.ID_PLAYLIST){
   pwl.showHideSelectionFocus (false);
  } else{
   pwl.showHideSelectionFocus (true);
  };
/*For switching to volume state*/
  if (iButtonID != ButtonFactory.ID_VOLUME){
   vb.setVisibleMode (false);
  } else{
   if (bMute) vb.setCurVolume (0);
    else vb.setCurVolume (iCurVolume);
    vb.setVisibleMode (true);
};   
  /*For switching to seek state*/
  if (iButtonID != ButtonFactory.ID_SEEK){
    bInSeekMode = false;
  } else{
    bInSeekMode = true;
  };
}

For playlist, we either show or hide the selection focus of the PowerList widget. When the focus is shown (which can be queried using its isSelectionFocusShown API method), the UI interaction is in the playlist state, allowing the user to navigate the selection in the file listing.

For volume, we either show or hide the VolumeBar widget. When it is shown, the UI interaction is in the volume state, allowing the user to turn up/down the volume level of the player.

For seek, we either set the boolean working variable bInSeekMode to true or false. When it is true, the UI interaction is in the seek state, allowing the user to seek backward or seek forward the playback of the current audio file.

Based on all this, we now need to implement the code that processes the key inputs that are allowed during each of the three UI interaction states, as follows:

public void keyPressed (int  iKeyCode){    
switch (iKeyCode){     
   default:   
      switch (canvas.getGameActionNoAlt (iKeyCode)){
        case Canvas.UP:    
          if (pwl.isSelectionFocusShown()){
            pwl.selectPrev();
          } else if (vb.isVisible()){
            if (!bMute){
              iCurVolume += 5; if(iCurVolume > 100) iCurVolume = 100;
              vb.setCurVolume (iCurVolume);
               if (player != null) volumeControl.setLevel(iCurVolume);
            };
          } else if (bInSeekMode){
            if (player != null
                 && player.getState() == Player.STARTED){
              try{
                player.setMediaTime (player.getMediaTime()+10000);
             } catch (Exception ex){ MMCMIDlet.alert (this,ex); };
            };
          };
          break;
         case Canvas.DOWN:           
          if (pwl.isSelectionFocusShown()){
            pwl.selectNext();
          } else if (vb.isVisible()){
            if (!bMute){
              iCurVolume -= 5; if (iCurVolume < 0) iCurVolume = 0;
              vb.setCurVolume (iCurVolume);
               if (player != null) volumeControl.setLevel(iCurVolume);
            };
          } else if (bInSeekMode){
             if (player != null
                && player.getState() == Player.STARTED){
              try{
                player.setMediaTime (player.getMediaTime() - 10000);
              } catch (Exception ex){ MMCMIDlet.alert (this,ex); };
            };
          };
          break;
        case Canvas.FIRE:           
          if (bInFullscreenMode) switchFullScreenMode();
          break;
   };
    break;
  };
}

There we go with all the UI stuffs that we need to code to get the Audio Player screen up working the least. Now we are ready to implement the main functionalities of the player, including the audio playback, which we discuss next.

Playback of the Audio Files

As we have designed, the audio player is to be implemented to support playback of MP3, WAV, and MIDI files. We use the file extension to indicate whether a particular file is of which of the three audio formats that we support.

TWUIK does not (yet) provide a built-in support for media (both audio and video) playback. For providing the audio playback in our application, we therefore have to rely on the J2ME Mobile Media API (MMAPI) extension. Following is how we implement the method that is used to play/pause the currently selected audio file in the PowerList widget as well as the method that is used to stop the playback (if any):

private void pauseResumePlayback  (boolean bPause){
try{
   if (! bPause){ //Play            
     stopPlayback(); //stop previous playback if any       
      String theFileURLStr = ((String[])
        fileNamesNURLs.elementAt (pwl.getCurrentSelectedIndex()))[1];
      String theFileURLStrU = theFileURLStr.toUpperCase();
     FileConnection fc = (FileConnection)
                       Connector.open (theFileURLStr,Connector.READ);
      InputStream is = fc.openInputStream();
      if (is == null){
        return;
      };
     /*Start the audio playback, depending on the file format*/
      if (theFileURLStrU.endsWith ("MP3"))
       player = Manager.createPlayer (is, "audio/mpeg");         
      else if (theFileURLStrU.endsWith ("WAV"))
        player = Manager.createPlayer (is, "audio/x-wav");         
      else //must be MIDI
       player = Manager.createPlayer (is, "audio/midi");         
      player.setLoopCount (-1); //repeated playback       
      player.realize();       
      player.prefetch();       
      player.start(); 
      lMediaDuration = player.getDuration();
     /*Adjust volume based on current settings*/
      volumeControl = (VolumeControl)
                                 player.getControl ("VolumeControl");
      volumeControl.setMute (bMute);                         
      volumeControl.setLevel (iCurVolume);                         
      bEverPlayed = true;
    } else{
      lPausedMediaTime = player.getMediaTime();
      if (player != null
          && (player.getState() == Player.PREFETCHED
              || player.getState() == Player.STARTED)){
       player.stop(); 
      };
  };
} catch (Exception ex){
    ex.printStackTrace();
    //reset the play button back to its normal-state (PLAY)
    midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,false);
  };
}
private void stopPlayback(){
//if is currently playing (or paused)
  if (player != null
  && (player.getState() == Player.PREFETCHED
          || player.getState() == Player.STARTED)){         
    try{
      player.stop(); player.close(); //stop/destroy previous
      System.gc();
    } catch (Exception ex){ /*ignored*/ };
  };   
}

The above code is straightforward and should be familiar to those that have used the MMAPI before. One thing we can note, however, is the FishEyeMenu’s API method named setButtonAltState which we call when the playback attempt has failed to initiate. When we call the pauseResumePlayback method upon the user’s request (which we will see later) as a result of firing the play menu button, the button will switch to the pause icon since it has an alternative state (i.e., play or pause).

We have noted during our discussion of the screen design that some of the buttons in this screen have an alternative state. The play button is one of them. Thus, when it is fired, it will automatically become a pause button. Therefore, when our attempt to initiate the playback has failed, we revert back the pause button into the play button, using the setButtonAltState API method of the FishEyeMenu, passing in the ID of the play button along with a false boolean value (i.e., true for switching to the alternative state).

Now that we have coded these two functionality methods, we shall look at the implementation code that processes the user input that requests such functionalities to be performed. The input for this is based on firing one of the buttons in the FishEyeMenu. Therefore, since AudioPlayer is an MMCScreen, we are looking at the implementation of the menuButtonFired notification method.

So, without further ado, below we present the code for this method to process the request for the various playback functionalities, including play, pause, stop, previous, next, seek, and volume. For seek, we leave out the implementation code until later when we discuss about prompting an input from the user as the design for this button is that, when it is fired, we prompt the user to key in the desired seek time. We also leave out the implementation code for the visual and fullscreen buttons, which we will later discuss at the end of this tutorial part. So, here is the code:

public void menuButtonFired  (int iButtonID){
int iPrevCurSelectedIndex;
  switch (iButtonID){     
    case ButtonFactory.ID_PLAYLIST:
      //start/resume playback of currently selected file
      pauseResumePlayback (false);
      //implicitly switch the play button to alternate-state (PAUSE)
      midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,true);
      break;
   case ButtonFactory.ID_PLAY:
      //if is currently playing (not paused!)
      if (player != null && player.getState() == Player.STARTED){ 
        pauseResumePlayback (true); //pause playback
      } else{
        //start/resume playback of currently selected file
        pauseResumePlayback (false);
      };
      break;
   case ButtonFactory.ID_STOP:
      stopPlayback(); //stop playback if any       
      //reset the play button back to its normal-state (PLAY)
      midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,false);
      break;
    case ButtonFactory.ID_PREV:
      iPrevCurSelectedIndex = pwl.getCurrentSelectedIndex();
      pwl.selectPrev(); //implicitly navigate the file selection
      if (pwl.getCurrentSelectedIndex() != iPrevCurSelectedIndex){
        pauseResumePlayback (false);
        //switch the play button to its alternate-state (PAUSE)
        midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,true);
      };
     break;
   case ButtonFactory.ID_NEXT:
      iPrevCurSelectedIndex = pwl.getCurrentSelectedIndex();
      pwl.selectNext(); //implicitly navigate the file selection
      if (pwl.getCurrentSelectedIndex() != iPrevCurSelectedIndex){
        pauseResumePlayback (false);
        //switch the play button to its alternate-state (PAUSE)
        midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,true);
      };
      break;
      
    case ButtonFactory.ID_SEEK:
      ...
      break;
    case ButtonFactory.ID_VOLUME:
      if (bMute){ //to un-mute
        bMute = false;
        vb.setCurVolume (iCurVolume); //update the VolumeBar
        if (player != null){            
          volumeControl.setMute (false);
          volumeControl.setLevel (iCurVolume);
        };
    } else{ //to mute
        bMute = true;
        vb.setCurVolume (0); //update the VolumeBar
        if (player != null) volumeControl.setMute (true);
     };
      break;
   case ButtonFactory.ID_VISUAL:
      ...         
      break;    
   case ButtonFactory.ID_FULLSCREEN:
      ...
      break;
   case ButtonFactory.ID_TOMENU:
    case ButtonFactory.ID_EXIT:
      iFiredButtonID = iButtonID;
      bReadyToAcceptInputEvents = false;
      midlet.menu.initButtonEntryExitEffect (false); 
      break;   };
}

There we go with the implementation of the basic functionalities of the audio player. One thing related to this that we should not forget about is to update the IndicatorBar widget accordingly. So far, we have not made use of the IndicatorBar widget that we have added onto the canvas. We now look at how we should use it and update it accordingly as the audio player carries out operations.

First, remember that for this Audio Player screen, we use the flashing-ball mode of the IndicatorBar widget as to show an animated progress when the player is in playback. The flashing-ball runs from the beginning till the end during the playback of an audio file, depending on its duration. When there is no file being played, the flashing-ball should not be shown. And when the playback is paused, the flashing-ball should not progress nor animate.

To use the IndicatorBar’s flashing-ball to show the progress of playing an audio file during its playback, we need some trick. The MMAPI does not provide a way such that we can be notified as an event about the progress of the playback of a media file. Because of this limitation, we need a workaround. Since we have designed that the IndicatorBar widget can generate an event if it is of the flashing-ball mode, such that when the flashing-ball switches between the smaller version and the larger version, the event will be fired, and also since that this event is generated repeatedly very frequently, we can then use this event notification to update our IndicatorBar widget accordingly based on the progress of the playback, for which we can fortunately retrieve using the Player’s getMediaTime method.

Anyway, with all this in mind, following is the implementation code that we need to add/modify for this complete purpose:

public void  activated(){    
if (!midlet.indicatorBar.isAnimationStopped())
   midlet.indicatorBar.startStopAnimation(); //no flashing yet
  midlet.indicatorBar.showHideFlashingBall (false);
  midlet.indicatorBar.setEventListener (this);       
}
...
private void pauseResumePlayback (boolean bPause){
  try{
   if (! bPause){ //Play            
      ...
      midlet.indicatorBar.configures (IndicatorBar.MODE_FLASHINGBALL,
                                     0,(int) (lMediaDuration/1000));
      if (midlet.indicatorBar.isAnimationStopped())
        midlet.indicatorBar.startStopAnimation();
   midlet.indicatorBar.showHideFlashingBall (true);
    } else{
      ...
     if (!midlet.indicatorBar.isAnimationStopped())
        midlet.indicatorBar.startStopAnimation();
      midlet.indicatorBar.showHideFlashingBall (false);
};
} catch (Exception ex){
    ...
  };
}
private void stopPlayback(){
if (!midlet.indicatorBar.isAnimationStopped())
midlet.indicatorBar.startStopAnimation();
  midlet.indicatorBar.showHideFlashingBall (false);
  ...
}
...
public void componentEventFired (Component c, int iEventID,
                                 Object paramObj, int iParam){
  if (c == midlet.indicatorBar){
   if (iEventID == IndicatorBar.EVENTID_FLASHING){
      long lCurPlaybackTime = player.getMediaTime();
    /*Update the IndicatorBar*/
      midlet.indicatorBar.setCurrentValue (
                                    (int) (lCurPlaybackTime / 1000));
    };       
  };
}

Alerting and Prompting Input Using the TINY PopupWindow Library

As we can see from our implementation code for the audio playback, there are some possibilities that the request may fail, such as due to invalid audio data, or unsupported format, etc. To create user-friendly software, in a situation where error may occur and is not recoverable, the application should inform the user of the happening by showing some message dialog boxes. Furthermore, dialog boxes can also be used by an application to prompt the user to provide some input, such as the login name, or password, etc.

TWUIK provides a powerful and generic built-in support for popup windows. Dialog boxes are just specific types of popup windows. The TINY PopupWindow provides the library of popup windows, including dialog boxes such as MessageBox and InputDialog. These are all pre-built popup window abstractions, however, meaning that they cannot yet be directly used by the application programmer. The MessageBox and InputDialog dialog boxes have been abstracted in TINY PopupWindow such that it can be used with different Look-And-Feels (LAFs).

In TWUIK, some components such as those for form items and dialog boxes have been designed such that they can be extended into one having a specific appearance and behaviour depending on the LAF library it belongs to. There are currently two LAF libraries in TWUIK: BasicGraph and MicroMac. For these two LAF libraries, there are essentially no differences in both the appearance and behaviour of the dialog box components (i.e., MessageBox and InputDialog). Since in this tutorial, we are not going to show the use of form items, we can choose either BasicGraph or MicroMac. The important thing though, is that we still need to choose one of them, since we need to initialize the LAF before we use any components that belong to it. We will choose the BasicGraph LAF in this tutorial.

So, as we have mentioned above, there are two pre-built dialog boxes in general: MessageBox and InputDialog. MessageBox is typically used to show a dialog box displaying some message description to the user and waiting to be dismissed using one of its provided buttons. There are several types of MessageBox: TYPE_INFO, TYPE_WARNING, TYPE_ERROR, or TYPE_QUESTION. For the first three types, there is only one button (BUTTON_OK) available. While for TYPE_QUESTION, the available button can be a combination of the following: BUTTON_OK, BUTTON_CANCEL, BUTTON_YES, BUTTON_NO, BUTTON_ABORT, BUTTON_RETRY, and BUTTON_IGNORE.

InputDialog, on the other hand, is typically used to show a dialog box displaying an input component (of form UI) along with a description label and waiting to be dismissed using either its BUTTON_OK or BUTTON_CANCEL button. There is currently only one type of InputDialog: TYPE_TEXTBOX which uses the DirectTextBox form item for allowing the user to type in their input characters (around some defined constraints and limit) directly on the given box.

In the BasicGraph LAF, the concrete implementation classes for MessageBox and InputDialog are named BGMessageBox and BGInputDialog, respectively. The API signature for the constructor of BGMessageBox is as follows:

public BGMessageBox (int iWidth, int iHeight, 
                    String titleStr, String msgStr,
                    int iMessageType, int iButtonFlags)

As we can see above, we are allowed to customize the size of the shown message dialog box. If we do not wish to do so, we just pass in -1 for both the width and the height. The message type and the button flags are based on the constants we mentioned earlier above. The button flags will only be considered if the specified message type is TYPE_QUESTION.

The API signature for the constructor of BGInputDialog, on the other hand, is as follows:

public BGInputDialog (int iWidth, int iHeight, 
                     String titleStr, String labelStr,
                     int iInputType)

Again, as we can see above, we are allowed to customize the size of the shown input dialog box. If we do not wish to do so, we just pass in -1 for both the width and the height. The input type must be currently specified as TYPE_TEXTBOX.

As subclasses of DialogBox, both MessageBox and InputDialog (and thus BGMessageBox and BGInputDialog) provides the following API method for configuring the popup motion effects:

public final void setPopupMotion (int iEntryMotionType, 
                                  MotionFX mofxCustomEntry,
                                  int iExitMotionType,
                                  MotionFX mofxCustomExit)

Currently, the entry motion type can be only one of the following: MOTION_ENTRY_NONE, MOTION_ENTRY_CUSTOM, or MOTION_ENTRY_BUBBLINGUP. The first one is to switch off the entry motion effects, meaning that the dialog box just immediately appears on the given canvas when shown. The second one is to use the specified custom MotionFX engine for the entry motion effects while the third one is to request that the dialog box, when shown, will appear from the bottom of the given canvas to its center position.

Similar case applies to the exit motion effects. The type can be only one of the following currently: MOTION_EXIT_NONE, MOTION_EXIT_CUSTOM, or MOTION_EXIT_FREEFALL. Again, the first type is to switch off the exit motion effects, while the second one is to use the specified custom MotionFX engine. The third type is to request that the dialog box, when dismissed, will go away from the canvas, starting from its current position and going down to the bottom until it is beyond the canvas area.

Now, in our Audio Player screen, we will use the BGMessageBox when we want to display an alert or information message to the user. We use the BGInputDialog in implementing the seek time functionality, for which we prompt the user for typing in the desired seek time. Since the use of these dialog boxes should not be limited to the Audio Player screen only, we define a generic framework for conveniently showing either of these two dialog boxes whenever we need to. We thus implement the following in the MMCMIDlet class:

...
import com.tricastmedia.twuik.popupwindows.*;
import com.tricastmedia.twuik.lafs.basicgraph.*;
public class MMCMIDlet extends MIDlet
                       implements ComponentEventListener,
                                  CanvasEventListener{   
/*Working variables*/
  ...
  private static BGMessageBox alertMsgBox;
  private static BGMessageBox infoMsgBox;
  private static BGInputDialog strInputDialog;
  private static BGInputDialog numInputDialog;
  public MMCMIDlet(){
  /*Initialize working variables*/
    ...
    alertMsgBox = new BGMessageBox (0,0,
                                    "Alert","",
                                    MessageBox.TYPE_ERROR,0);
alertMsgBox.setPopupMotion (DialogBox.MOTION_ENTRY_BUBBLINGUP,
                            null,
                                DialogBox.MOTION_EXIT_FREEFALL,
                                null);
infoMsgBox = new BGMessageBox (0,0,
                               "Info","",
                               MessageBox.TYPE_INFO,0);
infoMsgBox.setPopupMotion (DialogBox.MOTION_ENTRY_BUBBLINGUP,
                           null,
                           DialogBox.MOTION_EXIT_FREEFALL,
                           null);
strInputDialog = new BGInputDialog (0,0,"Input","",
                                    InputDialog.TYPE_TEXTBOX);
strInputDialog.setPopupMotion (DialogBox.MOTION_ENTRY_BUBBLINGUP,
                              null,
                               DialogBox.MOTION_EXIT_FREEFALL,
                               null);   
numInputDialog = new BGInputDialog (0,0,"Input","",
                                    InputDialog.TYPE_TEXTBOX);
numInputDialog.setPopupMotion (DialogBox.MOTION_ENTRY_BUBBLINGUP,
                               null,
                               DialogBox.MOTION_EXIT_FREEFALL,
                               null);    }
...   static void alert (final MMCScreen screen,
                     final String errMsgStr){
   new Thread(){
      public void run(){   
        alertMsgBox.setMessage (errMsgStr,fontSysPropSmallPlain);
      alertMsgBox.show (screen.canvas,true,true);
      }
    }.start();
  }

static void alert (MMCScreen screen, Exception ex){
   ex.printStackTrace();
alert (screen,"Exception occured.\n\n: "
              + ex.getClass().toString()
              + ": " + ex.getMessage());
  }
static void message (final MMCScreen screen, final String msgStr){
   new Thread(){
      public void run(){   
        infoMsgBox.setMessage (msgStr,fontSysPropSmallPlain);
        infoMsgBox.show (screen.canvas,true,true);   
      }
    }.start();
  }
  static String promptStringInput (MMCScreen screen, String labelStr,
                                   String defaultTextStr){
   strInputDialog.configureInput (defaultTextStr,-1,TextField.ANY);
    strInputDialog.setLabel (labelStr,fontSysPropSmallPlain);
    strInputDialog.show (screen.canvas,true,true);           
    return strInputDialog.getInputText();
  } 
static long promptNumberInput (MMCScreen screen, String labelStr,
                                 long lDefaultNumber)
     throws NumberFormatException, Exception{
numInputDialog.configureInput (String.valueOf (lDefaultNumber),
                               -1,TextField.NUMERIC);
    numInputDialog.setLabel (labelStr,fontSysPropSmallPlain);
    numInputDialog.show (screen.canvas,true,true);       
if (numInputDialog.getSelectedButton()== DialogBox.BUTTON_CANCEL)
      throw new Exception(); //just to indicate it was cancelled
    else
      return Long.parseLong (numInputDialog.getInputText());   
  } 

We then apply the dialog box framework above in action as exemplified in the following modified implementation code in the AudioPlayer class:

private void  pauseResumePlayback (boolean bPause){
  try{
    if (! bPause){ //Play            
      ...
      InputStream is = ...
     if (is == null){
        MMCMIDlet.alert (this,"Unable to load the audio data");
        //reset the play button back to its normal-state (PLAY)
        midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,false);
        return;
     };
     ...
   };
} catch (Exception ex){
MMCMIDlet.alert (this,ex);     
//reset the play button back to its normal-state (PLAY)
    midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,false);
  };
}

There we go with the use of the alert method from our dialog box framework for showing error messages or exceptions that occur causing the failure of the requested operation. We then use the promptNumberInput and message method in the implementation of the seek-time functionality below:

public void menuButtonFired  (int iButtonID){
  ...
   case ButtonFactory.ID_SEEK:
      //if is currently playing (or paused)
      if (player != null
          && (player.getState() == Player.PREFETCHED
              || player.getState() == Player.STARTED)){
        final AudioPlayer this_ = this;
      new Thread(){
          public void run(){                     
            boolean bCancelled = false;
            long lNewTime = 0;
            try{
              lNewTime = MMCMIDlet.promptNumberInput (this_,
                                          "Enter seek time (in ms):",
                                              player.getMediaTime());
            } catch (NumberFormatException ex){               
              MMCMIDlet.alert (this_,
                               "Invalid input for the seek time.");
            } catch (Exception ex){ /*means cancelled*/ };
            if (!bCancelled){
              try{
                player.setMediaTime (lNewTime);
              } catch (Exception ex){ MMCMIDlet.alert (this_,ex); };
            };
          }
        }.start();
      } else{
        MMCMIDlet.message (this,"No audio is in playback."); 
      };
      break;
      ...
...
}

Now with all this, we are done with the complete basic implementation of the Audio Player which provides the main functionalities of the audio playback as well as providing reliable error messages when they occur. Now let’s get more advanced by incorporating the feature of showing some simple visualization effects when the player is in playback, which we cover in next section.

Creating a Visualization Effects

We are going to introduce an interesting feature into our Audio Player – a visualization effects. TWUIK obviously does not provide this as a built-in feature as this is only a feature used by music applications. We however can create this with a combination of other more granular built-in features provided in TWUIK.

Our simple visualization effects will consist of two layer: background image visualization and circle visualization (which we call beat visualization as it is meant to visualize the beat of the music being played). These two layers will be represented as two custom widgets: ImageVisualization and BeatVisualization. We will start the discussion with the BeatVisualization first.

BeatVisualization when started performs the animation of a number of glowing circles (using ready-made glowing circle frame images), each goes alive one after another at random speed and random position around the area of the widget, and each starting from a small size and going to the largest size as it fades out. There is given a maximum number of circles that can be alive at any one time, and this is provided as a parameter when the BeatVisualization widget is constructed.

The implementation of this widget will not be shown here as there is nothing new to be discussed about. Nevertheless, we need to mention that it provides an API method called reset which we can use to reset the visualization to the starting state (i.e., no circles shown yet). Also, the API signature for its constructor is as follows (note the parameter for specifying the maximum number of circles):

public BeatVisualization (int iAnimTickCount, 
                          int iX, int iY,
                          int iWidth, int iHeight,
                          int iMaxDisplayedCircleCount)

On the other hand, ImageVisualization when started performs color transformation of a given image based on the Hue-Saturation-Value (HSV) color-space. HSV transformation is provided in TWUIK by the transformation FX engine called TFFXColor. This FX engine in fact provides transparency-level adjustment as well in addition to the HSV color-space transformation of a given image.

Because pixel-by-pixel real-time computation for the conversion from HSV to RGB is expensive, TFFXColor employs the technique of using a lookup table of HSV to RGB mappings. Ranges for Hue, Saturation, and Value are normally {0..256}, {0..256}, and {0..256}, respectively. Providing a lookup table using these ranges are still expensive, it will use too much memory. Due to this, TFFXColor uses sub-samples for each of the HSV components. For example, by default the lookup table is based on 6x40x40 sub-sampling, meaning that maximum value for Hue is 6, for Saturation is 40, and for Value is 40. These are sub-samples, need to note, so 6 for Hue would map to 256 in the original sample while 3 would map to 128, and so on.

The sub-sample of the lookup table can be modified, using the HSV2RGBLookupTableGenerator tool provided in the TWUIK distribution. For example, to provide more color variation for the transformation knowing that memory is not an issue, we can use the tool to create a lookup table that is based on 40x40x40 sub-sampling. It’s up to your choice since before you can use TFFXColor engine, you need to specify which lookup table to use for the HSV transformation, by calling its loadLookupTable static method. At anytime you can also query for the sub-sampling of the loaded lookup table, by calling one of the following API methods: getMaxH, getMaxS, or getMaxV. All these methods return the corresponding maximum value from the sub-sampling. Note that we do not need to know the minimum value, as no matter what the sub-sampling is, the minimum value for each will always be 0.

Generally, using TFFXColor for performing a HSV transformation involves the following basic steps:

  1. Ensures that the TFFXColor.loadLookupTable static method has been called at least once before use, which is of the following API signature:
    public static void loadLookupTable (String resourceFileName, 
                                        int iMaxH, int iMaxS, int iMaxV)
    The resource file name for the default lookup table is called hsv2rgb_lookup.dat, while the default sub-sampling is 6x40x40.
  2. Constructs a new TFFXColor object if not yet.
  3. Re-initializes it with the desired source (before-transform) Image object by calling the reinit method.
  4. Configures the parameters appropriately as desired by calling the configure method. The API signature for this method is:
    public final void configure (int iOpacityPercentage, 
                               short sHueAdjustment,
                                 short sSatAdjustment,
                                 short sValAdjustment,
                                 int iHSVAdjustmentMode)
    iOpacityPercentage is the reverse transparency-level for the transformation, meaning that the output image will have the specified opacity as opposed to the original image. sHueAdjustment, sSatAdjustment, and sValAdjustment are all the adjustment values for the Hue, Saturation, and Value, respectively. The adjustment technique is based on the mode specified for the iHSVAdjustmentMode parameter. There are two modes available: HSVADJUSTMENTMODE_SET or HSVADJUSTMENTMODE_CHANGE. The former is to indicate that the specified HSV adjustments are to be applied by setting every pixel in the image to be of the specified HSV values. If we do not wish to adjust a particular component of the HSV, specifying -1 would do so. On the other hand, the latter adjustment mode is to indicate that the specified HSV adjustments are to be applied relative to the current HSV value of every pixel in the image. For example, if the current Hue for a particular pixel is 5, specifying -2 for the Hue adjustment parameter would change that pixel to be of Hue 3. The legal adjustment value for this mode thus ranges from {–MAX..MAX}. Specifying 0 for any of the adjustment parameters in this mode implies no change for that corresponding component.
  5. Performs the transformation by calling the doTransform method.
  6. Finally, as for any TransformationFX engine, we then retrieve the output/resulting image after the transformation by calling the getTransfImage method passing either a true or false boolean value. If true, then if the output image is not ready yet, previous transformation output image (if any) will be returned instead. If false, null will be returned if the output image is not ready yet.
  7. Ensures that the TFFXColor.cleanup static method is called when the application will no longer use any TFFXColor engine anymore. The API signature for this method is:
    public static void cleanup()

Equipped with all this information, now we are ready to implement the ImageVisualization widget. Following is the complete code listing for it:

 Listing 3.1. Code for ImageVisualization

import  javax.microedition.lcdui.*;
import com.tricastmedia.twuik.*;
import com.tricastmedia.twuik.extras.*;
import com.tricastmedia.twuik.transformfxs.*;
import java.util.*;
public class ImageVisualization extends Component{ 
  /*Private Constants*/
  private static final int DEFAULT_TRANSFORMDELAY = 500; 
  /*Primary Properties*/ 
  private Image theImg;
  /*Secondary Properties*/
  private int iTransformDelay;
  /*Working Variables*/
  private TFFXColor tffxColor;
  private long lLastTransformTime;   
private Image transformedImg;
  private int iCurH;
  private int iCurS;
  private int iCurSDir;
  private Thread transformThread;
public ImageVisualization (int iAnimTickCount, int iX, int iY,
                           int iWidth, int iHeight,
                           Image theImg){
super (iAnimTickCount,false,iX,iY,iWidth,iHeight);
   if (theImg == null) throw new IllegalArgumentException();
   /*Sets primary properties*/   
   this.theImg = theImg;
  /*Makes default secondary properties*/
   iTransformDelay = DEFAULT_TRANSFORMDELAY;
   /*Initialize working variables*/
   tffxColor = new TFFXColor();       
reset();   
}   
/***********************************
   PRIVATE HELPER METHODS
   **********************************/
private void computeNextFrame(){
    transformThread = new Thread(){
      public void run(){   
        tffxColor.reinit (theImg);
        tffxColor.configure ((byte) 100,
                              (short) iCurH,(short) iCurS,(short) -1,
                            TFFXColor.HSVADJUSTMENTMODE_SET);
        tffxColor.doTransform();
        transformedImg = tffxColor.getTransfImage (false);
if (iCurSDir < 0){
          if (iCurS == 0){
            iCurH = (iCurH % TFFXColor.getMaxH()) + 1;
            iCurSDir = 1;
          } else{ // if (iCurS > 1)
            iCurS = Math.max (0,iCurS-4);
          };    
        } else{
          if (iCurS == TFFXColor.getMaxS()){        
            iCurSDir = -1;
          } else{ //if (iCurS < TFFXColor.getMaxS()-1)
            iCurS = Math.min (TFFXColor.getMaxS(),iCurS+4);
          };    
        };   
        long lCurTime = System.currentTimeMillis();        
        lLastTransformTime = lCurTime;
        transformThread = null;       }
     };
    transformThread.setPriority (Thread.MIN_PRIORITY);
    transformThread.start();
  }
/***********************************
   INTERNAL EVENT HANDLERS
   **********************************/
   protected void animationRestarted (boolean bNeverAnimatedYet){
   long lCurTime = System.currentTimeMillis();
    if (bNeverAnimatedYet) lLastTransformTime = lCurTime;
  }
  protected void drawCurrentFrame (Graphics g){
   int iPrevClipX = g.getClipX();
    int iPrevClipY = g.getClipY();
    int iPrevClipWidth = g.getClipWidth();
   int iPrevClipHeight = g.getClipHeight();
   g.clipRect (0,0,iWidth,iHeight);   
    g.drawImage (transformedImg,0,0,Graphics.LEFT | Graphics.TOP);
    g.setClip (iPrevClipX,iPrevClipY,iPrevClipWidth,iPrevClipHeight);
  }   
protected boolean animate(){
   boolean bFrameAdvanced = false;
    long lCurTime = System.currentTimeMillis(); 
    /*Compute next frame if it's already time to advance*/
    if (lCurTime - lLastTransformTime >= iTransformDelay){
      if (transformThread == null) computeNextFrame();
   };
   return bFrameAdvanced;
}
/***********************************
  PUBLIC APIs
   **********************************/ 
public final void reset(){
   iCurH = 1;
    iCurS = TFFXColor.getMaxS();
    iCurSDir = -1;
    computeNextFrame();
    lLastTransformTime = System.currentTimeMillis();
}
}

Now that we have got all the visualization widgets up, we need to get them ready for action in the Audio Player screen. Thus we have the following modified implementation code in the AudioPlayer class:

/*Components*/
...
private ImageVisualization imageVis;
private BeatVisualization beatVis;
...
public AudioPlayer (MMCMIDlet midlet){       
/*Initialize components*/         
...
imageVis = new ImageVisualization (1,0,0,
                             canvas.getWidth(),pwl.getHeight(),
    ImageUtil.loadImageFromResource ("/audioplayer/vis_bg.png"));
  beatVis = new BeatVisualization (1,0,0,
                             canvas.getWidth(),pwl.getHeight(),
                                   8);         
}
...
public void menuEntryAnimEnded(){           
  imageVis.setVisibleMode (false); //initially hidden first
  canvas.addComponent (imageVis); //add the ImageVisualization
  beatVis.setVisibleMode (false); //initially hidden first
  canvas.addComponent (beatVis); //add the BeatVisualization
  //add the PowerList
  ...
  //add the VolumeBar
  ...
...
}
...
private void showHideVisualizations (boolean bShow){
if (bShow){ //to show
   imageVis.reset();
    imageVis.setVisibleMode (true); 
    beatVis.reset();
    beatVis.setVisibleMode (true);
} else{ //to hide
    imageVis.setVisibleMode (false);
    beatVis.setVisibleMode (false);
  };
}
private void startStopVisualizations (boolean bStart){
  if (bStart){ //to start
   if (imageVis.isAnimationStopped())
   imageVis.startStopAnimation(); //start
   if (beatVis.isAnimationStopped())
      beatVis.startStopAnimation(); //start     
} else{ //to stop
   if (! imageVis.isAnimationStopped())
       imageVis.startStopAnimation(); //stop
    if (! beatVis.isAnimationStopped())
      beatVis.startStopAnimation(); //stop 
  };
}

Note that the visualization widgets are added to the canvas before the PowerList and the VolumeBar widgets. This is because we want to ensure that the visualization stays in the background of the main components of the AudioPlayer such as the playlist, etc. Now, we are almost there! We just need to enable the user to show/hide the visualization as reflected in the following modified implementation code in the AudioPlayer class again:

public void menuButtonFired  (int iButtonID){
  ...
   case ButtonFactory.ID_VISUAL:
      if (bShowVis){ //to hide
        bShowVis = false;         
        showHideVisualizations (false);
      } else{ //to show         
        bShowVis = true;             
        showHideVisualizations (true);
      };         
      break;    
   ...
...
}
...
private void pauseResumePlayback (boolean bPause){
  try{
    if (! bPause){ //Play             
      ...
      if (bShowVis && !imageVis.isVisible()){
        showHideVisualizations (true); 
      };
      startStopVisualizations (true);
    } else{
      ...
      if (bShowVis) startStopVisualizations (false);
    };
  } catch (Exception ex){
...
  }
}
private void stopPlayback(){
...
  startStopVisualizations (false);
//if is currently playing (or paused)
  if (player != null
    && (player.getState() == Player.PREFETCHED
           || player.getState() == Player.STARTED)){
    ...
  };   
}

By default, we show the visualization when requested. However, if the player is not in playback at the time, we do not start the animation of the visualization yet. Therefore, in the case where user enables the visualization but starts the playback of an audio later on, we thus check whether the visualization is enabled or not, and if so, we implicitly starts it. Furthermore, whenever we start the visualization, we ensure that the ImageVisualization and BeatVisualization widgets start their animation from the starting state, by calling their reset method, as we have implemented inside the startStopVisualizations method.

There is one more feature we provide in the AudioPlayer, that is, to show the visualization (when enabled) in full-screen mode. To allow the user to switch the visualization to the full-screen mode, we provide the following method, which needs to be called when the full-screen button of the FishEyeMenu is fired:

private void switchFullScreenMode(){
  //only if currently playing (or paused)
  if (bShowVis){         
   bInFullscreenMode = !bInFullscreenMode;
   if (bInFullscreenMode){       
      midlet.indicatorBar.setVisibleMode (false);
      midlet.menu.setVisibleMode (false);
      pwl.setVisibleMode (false);       
     imageVis.setSize (canvas.getWidth(),canvas.getHeight());
      beatVis.setSize (canvas.getWidth(),canvas.getHeight());
      bMenuCanBeInteracted = false;
   } else{
      midlet.indicatorBar.setVisibleMode (true);
      midlet.menu.setVisibleMode (true);
      pwl.setVisibleMode (true);         
      imageVis.setSize (canvas.getWidth(),pwl.getHeight());
      beatVis.setSize (canvas.getWidth(),pwl.getHeight());
     bMenuCanBeInteracted = true;
    };
  };
}

Basically, the above method simply resizes the visualization widgets, and then hides the other widgets that make up the Audio Player UI, such as the PowerList, VolumeBar, IndicatorBar, and FishEyeMenu. We have ensured that the visualization widgets, when resized, know that they are given larger/smaller area to animate and thus will behave accordingly (such as for the random positioning of the circles in the case of the BeatVisualization). Pretty straightforward, huh!?

Alright, that’s all we need to do for implementing the Audio Player screen completely. Below you can see a demo of it, showing its various features including the visualization effects.

Extending the Audio Player into a Media Player: Supporting Video Playback

Now that we have completed the implementation of the Audio Player screen, we are going to discuss about how we will extend it to support video files as well, thus making it a Media Player in fact. There is no built-in support in TWUIK for playback of video files as well, so we still have to stick to the MMAPI extension. Since we have implemented the playback for audio files, the playback for video files is just as straightforward.

Our design for this extension is to switch the player canvas to another canvas showing only the video being played. In essence, there are a couple things we need to do to extend the Audio Player: 1) modify the file listing to find and list video files as well; 2) extend the playback functionality to also support video files of MPEG or 3GPP format; and 3) to perform a screen transition effects (using TWUIK’s TransitionFX framework) when we switch from the player canvas to the video canvas.

For the file listing, we just need to add more search directories as well as the target file extensions. To differentiate between entries for audio files and entries for video files, we will display icons next to the file names in the PowerList widget. PowerList supports association of an icon image (as an Image object) to each appended item description text. Recall that when we implemented the Audio Player, we just ignore the second parameter to the appendItem method and always pass in -1.

Icon image in PowerList is displayed before each item decription text. In order to associate an appended item description text with an icon image, you first specify a list of Image objects that are to be used as icon images. This is done via the following API method:

public final void setIconImgs (Image[] iconImgs)

Upon calling this API method providing an appropriate list of Image objects, PowerList will automatically scales and creates an internal pool of those icon images but with different sizes so that when the description text is displayed with smaller/larger font, the size of the icon image will also match. The number of the size variations is equal to the number of fonts made available to the PowerList widget when it is constructed.

When the list of icon images have been configured, associating an item description text with one of the icons is done whenever an item is appended using the appendItem method. The second parameter to this method is the index into that previously given list of icon images.

So, all in all, to also list files of MPEG and 3GPP formats and with different icons between audio and video files, thus we have the following modified implementation in the MediaPlayer class (note that AudioPlayer has thus been renamed to MediaPlayer):

...
static{
  fileNamesNURLs = MMCMIDlet.searchForMediaFiles (
               new String[]{"",                 
                            "Music/",
                            "Sounds/",
                            "Sounds/Digital",
                            "Nokia/Sounds/",
                            "Nokia/Sounds/Digital",                            "videos",                             "Nokia/Video",
                           },
               new String[]{"WAV",
                            "MP3",
                           "MID",
                            "MPG",
                           "MPEG",
                           "3GP",
                           });
};
...
public MediaPlayer (MMCMIDlet midlet){       
...
/*Initialize components*/     
  Image[] iconImgs = new Image[2];
  iconImgs[0] = ImageUtil.loadImageFromResource
                               ("/audioplayer/fileicon_audio.png");
   iconImgs[1] = ImageUtil.loadImageFromResource
                               ("/audioplayer/fileicon_video.png");
   pwl = new PowerList (1,0,0,canvas.getWidth(),
                         7,
                         new TwuikFont[]{
                                  MMCMIDlet.fontSysPropSmallPlain,
                                 MMCMIDlet.fontSysPropMediumBold,
                                 MMCMIDlet.fontSysPropLargePlain,
                                 MMCMIDlet.fontSysPropLargeBold,
                                        });   
  pwl.configureHighlightBar (0x996633,false,(byte) 75);
  pwl.setIconImgs (iconImgs);
  pwl.setEventListener (this);
  ...
}
...
public void menuEntryAnimEnded(){           
...
  for (int i=0; i < fileNamesNURLs.size(); i++){
   String fileNameStr =
                      ((String[]) fileNamesNURLs.elementAt (i))[0];
    String fileNameStrU = fileNameStr.toUpperCase();
  boolean bVideoFile = (fileNameStrU.endsWith ("MPG")
                          || fileNameStrU.endsWith ("MPEG")
                          || fileNameStrU.endsWith ("3GP"));
    pwl.appendItem (fileNameStr,(bVideoFile ? 1 : 0));
  };
...                 
}

Now that we have got video files listed on the PowerList widget as well, without any further changes in the implementation, the user can already now choose a video file to be played. So now we need to add the video playback functionality.

As mentioned earlier, our design implies that whenever a video file is requested to be played, we switch from the main player canvas to another canvas that displays the video clip only. Then we request the MMAPI’s Player using its VideoControl to display the video frames onto that new canvas. The new canvas, that is, the video canvas, will be an instance of AnimationCanvas.

Need to note that AnimationCanvas is actually not a subclass from the LCDUI’s Canvas class. In TWUIK, there is only one instance of a subclass of Canvas that is used to “display” any current AnimationCanvas. This Canvas object can be retrieved from the Display object that can be retrieved by calling TWUIKDisplay.getDisplay(). Therefore, to get that Canvas object, we call the getCurrent() method of the Display object. We need this especially for implementing the video playback, as the MMAPI requires us to specify a Canvas object onto where the video frames are to be displayed. Anyway, following is the complete modified implementation of certain parts of the MediaPlayer class to support the video playback according to our design:

class MediaPlayer extends MMCScreen 
           implements ComponentEventListener,           
                      CanvasEventListener, PlayerListener{
...
/*Working variables*/   
  ...
  private AnimationCanvas videoCanvas;
  private ImageBox ibVideoPlaceHolder;
  private VideoControl videoControl;
  ...
public MediaPlayer (MMCMIDlet midlet){       
   /*Initialize working variables*/
    ...
    videoCanvas = new AnimationCanvas (FRAMEDELAY);   
    videoCanvas.setBgColor (0xFFFFFF);
    videoCanvas.setEventListener (this);
    ibVideoPlaceHolder = new ImageBox (1,false,0,0,
                                       canvas.getWidth(),
                                       canvas.getHeight(),
                                       null,null,null,0);
   ibVideoPlaceHolder.setBgColor (0x000000);   
    ibVideoPlaceHolder.startStopAnimation();
   videoCanvas.addComponent (ibVideoPlaceHolder);
  }
  ...
  /*Event handlers for the video extension*/
  public void timerHasReallyStopped (AnimationCanvas prevCanvas){}
  public void showNotify (AnimationCanvas canvas){} 
  public void hideNotify (AnimationCanvas canvas){} 
  public void screenModeChanged (AnimationCanvas canvas){}       
  public void keyPressed (AnimationCanvas canvas, int iKeyCode){ 
    if (canvas.getGameActionNoAlt (iKeyCode) == Canvas.FIRE)
      stopPlayback();
  }
  public void keyReleased (AnimationCanvas canvas, int iKeyCode){}
  public void keyRepeated (AnimationCanvas canvas, int iKeyCode){}
  public void playerUpdate (Player player,
                            String event, Object eventData){
   if (event == PlayerListener.END_OF_MEDIA){
      stopPlayback();
    };
  }
  /*End of Event handlers for the video*/
private void pauseResumePlayback (boolean bPause){
    try{
      if (! bPause){ //Play             
        ...
        /*Start the playback*/
        System.gc();
        boolean bVideo = false;
        if (theFileURLStrU.endsWith ("MP3"))
          player = Manager.createPlayer (is, "audio/mpeg");
        else if (theFileURLStrU.endsWith ("WAV"))
          player = Manager.createPlayer (is, "audio/x-wav");
        else if (theFileURLStrU.endsWith ("MID"))
          player = Manager.createPlayer (is, "audio/midi");
        else if (theFileURLStrU.endsWith ("MPG")
                || theFileURLStrU.endsWith ("MPEG")){
          player = Manager.createPlayer (is, "video/mpeg"); 
          bVideo = true;
        } else if (theFileURLStrU.endsWith ("3GP")){
          player = Manager.createPlayer (is, "video/3gpp"); 
          bVideo = true;
        };
        if (!bVideo) player.setLoopCount (-1); //repeated playback 
        player.realize();       
        player.prefetch();               
        lMediaDuration = player.getDuration();
        if (bVideo){
          player.addPlayerListener (this);         
          videoControl =
                 (VideoControl) player.getControl ("VideoControl");
          videoControl.initDisplayMode (
                                     VideoControl.USE_DIRECT_VIDEO,
                           TWUIKDisplay.getDisplay().getCurrent());
          videoControl.setDisplayLocation (
                             Math.max (0,(videoCanvas.getWidth()
                            -videoControl.getSourceWidth())/2),
                              Math.max (0,(videoCanvas.getHeight()
                              -videoControl.getSourceHeight())/2));
          ibVideoPlaceHolder.iX = videoControl.getDisplayX();
          ibVideoPlaceHolder.iY = videoControl.getDisplayY();
          ibVideoPlaceHolder.setSize (
                                    videoControl.getDisplayWidth(),
                                  videoControl.getDisplayHeight());
          videoControl.setVisible (true);          
       } else{
          /*Adjust volume based on current settings, only for
          audio, for video sometime has problems, dunno why*/
          volumeControl =
              (VolumeControl) player.getControl ("VolumeControl");
          volumeControl.setMute (bMute);                  
          volumeControl.setLevel (iCurVolume); 
        };                            
//start immediately (if not video)
        if (!bVideo) player.start();
        midlet.indicatorBar.configures (
                                    IndicatorBar.MODE_FLASHINGBALL,
                                    0,(int) (lMediaDuration/1000)); 
        if (!bVideo){
          if (midlet.indicatorBar.isAnimationStopped())
            midlet.indicatorBar.startStopAnimation();
         midlet.indicatorBar.showHideFlashingBall (true); 
          if (bShowVis && !imageVis.isVisible())
            showHideVisualizations (true);    
          startStopVisualizations (true);
        };
     } else{
        lPausedMediaTime = player.getMediaTime();
        if (player != null
            && (player.getState() == Player.PREFETCHED
                || player.getState() == Player.STARTED))
          player.stop(); 
        if (!videoCanvas.isShown()){                   
          if (!midlet.indicatorBar.isAnimationStopped())
            midlet.indicatorBar.startStopAnimation();
        midlet.indicatorBar.showHideFlashingBall (false);
          if (bShowVis) startStopVisualizations (false);
        } else{
          TWUIKDisplay.setCurrent (canvas);
          videoControl.setVisible (false);
       };
      };
    } catch (Exception ex){
     ...     
    };
}
private void stopPlayback(){
   if (videoCanvas == null || !videoCanvas.isShown()){ 
      if (!midlet.indicatorBar.isAnimationStopped())
       midlet.indicatorBar.startStopAnimation();
    midlet.indicatorBar.showHideFlashingBall (false); 
     startStopVisualizations (false);
    } else{
      TWUIKDisplay.setCurrent (canvas);
      videoControl.setVisible (false);
   };
   ...
  }
...
}

Now to add some animation sugar into our Media Player as well as to introduce a new thing in TWUIK for you, we are going to make the screen switching more animated, that is, via some transition effects. We have seen the use of TWUIK’s Transition FX framework in the previous part of this tutorial series, when we implement the slide show feature of the Photo Gallery. However, we used it in a more high-level perspective in the sense that we just specify to the ImageBox widget which transition FX engines are to be used during the transition between images. Now we are going to apply Transition FX in a different way, in the context of screen transition.

In fact, TWUIK has made this very easy to do. Every Transition FX engine, since it extends from the TransitionFX class, provides the following API methods in the context of screen transition:

public final void doScreenTransition (AnimationCanvas beforeCanvas, 
                                      AnimationCanvas afterCanvas) public final void doScreenTransition (Image beforeCanvasImg,
                                      AnimationCanvas afterCanvas)

So, basically there are two versions of the API to perform a screen transition. The first version is the most convenient one, where you just need to specify two AnimationCanvas objects, the one before and the one after. The second one is different in that the parameter for the one before is passed as an Image object rather than an AnimationCanvas object. This Image object is meant to represent the “before” screen. Virtually, this can be any Image object in fact, as long as the size match the size of the afterCanvas. Typically, this Image is obtained from an AnimationCanvas for the previous screen, which we can get by calling its getCurrentOffscreenImage() method.

The second version of the API is typically useful when we have a very limited amount of memory on the device, such that when we proceed to the next screen, we want to destroy the previous screen. To achieve this, we first get its offscreen Image object before destroying it. Thus, we use the second version of the screen transition API as we no longer hold the reference to the AnimationCanvas object for the previous screen anymore. In our case of the video playback, we will just use the first version since we will not be destroying the main canvas screen. Nevertheless, for either version of the API, before using them, we have to ensure that the TWUIK rendering system is using double-buffering mode. By default this is true. Otherwise, we need to call the useDoubleBuffering static method of the TWUIKDisplay class, passing a true boolean value.

As for the MotionFX and TransformFX as we have seen previously, a TransitionFX engine also can generate events. These can be notified to any implementer of the TransitionFXListener interface. There are following event callback methods available:

public void transitionStarted (TransitionFX tsfx)
public void transitionFinished (TransitionFX tsfx)

transitionStarted event is fired every time the TransitionFX engine is started, while transitionFinished event is fired every time the TransitionFX engine has finished performing the effects. The latter one is typically used in screen transition where we want to wait for the transition to finish first before activating the new screen.

Generally, using a TransitionFX engine for performing a screen transition involves the following basic steps (based on the first version of the API):

  1. Ensures that the the TWUIK rendering system is using double-buffering mode by calling “TWUIKDisplay.useDoubleBuffering (true)” unless already done so.
  2. Constructs an object of any of the TransitionFX’s concrete subclasses if not yet, passing in the appropriate parameters as necessary. Pre-built TransitionFX engines are: TSFXSliding, TSFXWiping, TSFXFadingInOut, TSFXFadeThrough, TSFXRandomFall, TSFXBreakOut, and TSFXDissolve. We have mentioned this previously in the end of the tutorial part 2, so we are not going to repeat their constructor APIs here.
  3. Registers a TransitionFXListener for that constructed object, if necessary and if not yet.
  4. Ensures that animation for the AnimationCanvas of the previous screen has been stopped.
  5. Ensures that animation for the AnimationCanvas of the next screen has been stopped.
  6. Finally calls the doScreenTransition method on the TransitionFX object, passing in the AnimationCanvas object of the previous screen as well as the AnimationCanvas object of the next screen.
  7. Then, optionally intercepts the transitionFinished event by implementing the callback method to activate the next screen.

We will use the TSFXDissolve engine for our screen transition for the video playback. So, let us see it in action with the following modified implementation of the MediaPlayer class:

class MediaPlayer extends MMCScreen 
           implements ComponentEventListener, TransitionFXListener,
                      CanvasEventListener, PlayerListener{
...
/*Working variables*/   
...
private TransitionFX tsfxBeforePlayingVideo;   
...
public MediaPlayer (MMCMIDlet midlet){       
   /*Initialize working variables*/
   ...
  tsfxBeforePlayingVideo = new TSFXDissolve (1,10,20); 
  tsfxBeforePlayingVideo.setTransitionFXListener (this);
   ...
}
...
/*Event handlers for the video extension*/
public void transitionStarted (TransitionFX tsfx){}
public void transitionFinished (TransitionFX tsfx){
  TWUIKDisplay.useDoubleBuffering (false); //disable for MMAPI
   try{
      player.start();
      videoControl.setVisible (true);
   } catch (Exception ex){
      MMCMIDlet.alert (this_,ex);     
       //reset the play button back to its normal-state (PLAY)
      midlet.menu.setButtonAltState (ButtonFactory.ID_PLAY,false);
   };       
}
...
/*End of Event handlers for the video extension*/
private void pauseResumePlayback (boolean bPause){
   try{
       if (! bPause){ //Play            
      ...
          if (bVideo){
       player.addPlayerListener (this);            
          videoControl =
                (VideoControl) player.getControl ("VideoControl");
         videoControl.initDisplayMode (
                                      VideoControl.USE_DIRECT_VIDEO,
                            TWUIKDisplay.getDisplay().getCurrent());
          videoControl.setDisplayLocation (
                               Math.max (0,(videoCanvas.getWidth()
 &nb