Previous  | Next  | Home

Animal Sounds


 

In this simple project we gain some more familiarity with the UI widgets, and will also learn to play audio in Android. We shall use ImageButton, which is a kind of widget that acts like a button but accepts an image rather than text to define the visible face of the button, to present some animal images to the user and play animal sounds when a button is pressed.

 

Initial Layout of the Main Screen

Create a new project in Eclipse (File > New > Android Project), setting

Eclipse has a visual layout editor that is rather limited, but that can be useful in laying out simple configurations or the rudiments of more complex ones. Let's use this editor to lay out our initial screen. In the new project, open res/layout/main.xml and select the Layout tab at the bottom. At the top of the layout window, select Portrait for the Config setting. The layout window should now look as follows



and if you switch to the main.xml tab at the bottom the corresponding listing is


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    <TextView  
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="@string/hello" />
    </LinearLayout>

representing a single TextView displaying the value of the string variable hello from res/values/strings.xml within the LinearLayout. Switch back to the Layout tab, right-click on the displayed TextField, select Remove from the popup menu, and remove this widget. Next, select ImageButton from the Views menu on the left (scroll the menu if necessary to make the ImageButton option visible) and drag an ImageButton to the layout area. (At least in the Linux version of Eclipse, you sometimes have to try this more than once before the ImageButton can be selected.) Repeat this twice more. The layout manager should now look like the following image



with three buttons on stage. Now switch back to the main.xml tab and display the file, which should read at this point


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">

        <ImageButton android:id="@+id/ImageButton01" android:layout_width="wrap_content" 
            android:layout_height="wrap_content"></ImageButton>
        <ImageButton android:id="@+id/ImageButton02" android:layout_width="wrap_content" 
            android:layout_height="wrap_content"></ImageButton>
        <ImageButton android:id="@+id/ImageButton03" android:layout_width="wrap_content" 
            android:layout_height="wrap_content"></ImageButton>

    </LinearLayout>

So we now have three ImageButtons wrapped within the parent LinearLayout, with IDs ImageButton01, ImageButton02, and ImageButton03, respectively, that we will be able to reference later from our Java code.

 

Customizing the Layout

If you execute this in an emulator or on a device (right-click the project name and then select Run As > Android Application) you should get a display very similar to the preceding figure, with three non-descript buttons in the upper left corner of the screen. But these are not normal buttons because ImageButton is not actually a subclass of Button but rather is a subclass of ImageView, which in turn subclasses View and thus inherits image-display properties:

We now use these attributes to jazz up our buttons.

  1. Let us begin by copying some resources that we will need: From the course images directory, save the image files cow.png, cow_focused.png, cow_pressed.png, sheep.png, sheep_focused.png, sheep_pressed.png, duck.png, duck_focused.png, and duck_pressed.png into the res/drawable-hdpi directory of this project (image source). You may have to right-click on res/drawable-hdpi and select Refresh to get Eclipse to see the new files.

  2. Next we spread the buttons horizontally and vertically to fill the screen uniformly. We can accomplish this by


    Now if you execute on an emulator or device, you should have three non-descript buttons that fill the screen vertically and horizontally.

  3. Then we use the android:src attribute inherited from ImageView to attach an image to each button: add to the first ImageButton an attribute setting the image source to the cow.png image that we copied to res/drawable-hdpi:
    
        <ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent" 
            android:layout_height="fill_parent" android:src="@drawable/cow" 
            android:layout_weight="1">
        </ImageButton>
    
    
    where @drawable/cow is Android's reference to the drawable resource res/drawable-hdpi/cow.png. Repeat for the other two ImageButtons, using the image files duck.png and sheep.png as the sources. Now if you execute the app you should get a display like the following



    where the left figure shows the images attached to the buttons and the right figure illustrates for the middle (duck) button that the button background turns red if the button is pressed.

    As with most things in Android, it is also possible to specify the button image resource directly in the Java code by using the setImageResource(int) method of ImageView, where the argument is the drawable resource identifier.


  4. We can leave only the images as the visible buttons if we set the background color of the ImageButtons to transparent. Let's first define a color resource corresponding to a completely transparent color. Right-click on res/values, select New > Android XML File, give the file the name colors.xml, and edit it to read
    
        <?xml version="1.0" encoding="utf-8"?>
        <resources>
            <color name="transparent">#00ff0000</color>
        </resources>
    
    
    which defines a red color that is fully transparent (24-bit color format #AARRGGBB, where AA is alpha transparency, RR is red, GG is green, and BB is blue). Now we use the android:background property inherited from View to make the button background transparent by adding for each ViewButton an attribute
    
        android:background="@color/transparent"
    
    
    which references the color "transparent" that we just defined in res/values/colors.xml. While we are at it, we will also add an android:scaleType attribute to control how the image is attached to the button. Thus main.xml now reads
    
        <?xml version="1.0" encoding="utf-8"?>
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" >
        
                <ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent" 
                    android:layout_height="fill_parent" android:src="@drawable/cow" 
                    android:background="@color/transparent" android:scaleType="center"
                    android:layout_weight="1">
                </ImageButton>
                <ImageButton android:id="@+id/ImageButton02" android:layout_width="fill_parent" 
                    android:layout_height="fill_parent" android:src="@drawable/duck" 
                    android:background="@color/transparent" android:scaleType="center"
                    android:layout_weight="1">
                </ImageButton>
                <ImageButton android:id="@+id/ImageButton03" android:layout_width="fill_parent" 
                    android:layout_height="fill_parent" android:src="@drawable/sheep" 
                    android:background="@color/transparent" android:scaleType="center"
                    android:layout_weight="1">
                </ImageButton>
                        
        </LinearLayout>
    
    
    and if we execute the app we see that the visible representation of our buttons now consists only of the animal images, as illustrated in the following figure.



Since we have made the button background transparent, the default behavior of the button turning red when pressed is no longer operative and we have lost a standard visual clue as to the state of a button. We shall remedy that in the next section.

 

Adding Visible States for the Image Buttons

We can add back visual clues to the state of the image buttons by defining different images corresponding to different button states (for example, different color shadings of the same image), and having Android choose among them automatically according to the button state using a drawable "selector" defined in XML.

  1. Using an external editor, create an XML file with the following content
    
    <?xml version="1.0" encoding="utf-8"?>
     <selector xmlns:android="http://schemas.android.com/apk/res/android">
         <item android:state_pressed="true"
               android:drawable="@drawable/cow_pressed" /> <!-- pressed -->
         <item android:state_focused="true"
               android:drawable="@drawable/cow_focused" /> <!-- focused -->
         <item android:drawable="@drawable/cow" /> <!-- default -->
     </selector>
    
    
    Save this as the file button1.xml in the res/drawable-hdpi directory of the project. (Note: it is important to order the pressed, focused, and default items as given.) Likewise, create the file button2.xml in the same directory with the same content as above, except change all occurrences of the string "cow" to "duck", and create the file button3.xml in the same directory with all occurrences of the string "cow" in the above listing changed to "sheep". If necessary, right-click on drawable-hdpi and select Refresh in Eclipse so that the new files appear in the Eclipse project tree.

    The above instructions to create these files with an external editor is because I haven't figured out how to force Eclipse to put an XML file created by the normal Eclipse mechanism into the res/drawable directories, which is where these drawable selectors need to be. Once created in the correct directory, these files can be edited using Eclipse in the normal way.


  2. Edit main.xml and change the reference to the image source into a reference to these XML files (changes marked in red):
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    
    	<ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent" 
    		android:layout_height="fill_parent" android:src="@drawable/button1"
    		android:background="@color/transparent" android:scaleType="center"
    		android:layout_weight="1">
    	</ImageButton>
    	<ImageButton android:id="@+id/ImageButton02" android:layout_width="fill_parent" 
    		android:layout_height="fill_parent" android:src="@drawable/button2" 
    		android:background="@color/transparent" android:scaleType="center"
    		android:layout_weight="1">
    	</ImageButton>
    	<ImageButton android:id="@+id/ImageButton03" android:layout_width="fill_parent" 
    		android:layout_height="fill_parent" android:src="@drawable/button3" 
    		android:background="@color/transparent" android:scaleType="center"
    		android:layout_weight="1">
    	</ImageButton>
    		
    </LinearLayout>
    
    

Now Android will see the XML files that we just created rather than a direct image name when it looks at the src attribute of the ImageButtons (it knows to interpret @drawable/button1 as a reference to the file button1.xml in the res/drawable-hdpi directory, for example). Android will then read these files, determine the state of the button (normal, pressed, or focused), and use the appropriate image (as specified in the XML file) to represent the button. For example, the above code from button1.xml would cause Android to choose the image resource res/drawable-hdpi/cow_pressed.png if it detects that button 1 is pressed and res/drawable-hdpi/cow.png if it determines that button 1 is neither pressed nor receiving focus. If we execute the app we now obtain the following display



Where the left image is for no buttons selected and the right image is with the cow image being pressed on the phone (which causes Android to substitute the yellow-tinted image cow_pressed.png for the default cow.png).

This example corresponds to screenshots from a Motorola Backflip phone in touch-screen portrait mode, for which there is no "focused" state (corresponding to a green-tinted image); only normal and pressed. If on the Backflip phone one opens the physical keyboard, then the up and down keys on the physical keyboard can be used to change focus between buttons and then one sees the the green-tinted state indicating the button has focus, and the yellow-tinted state when the button is pressed on the touchscreen, or OK is clicked on the physical keyboard when the button has focus.


In default mode the displayed images shrank when we added the drawable selector. The ImageView attributes can modify this look. For example, changing the scaleType to android:scaleType="centerCrop" for each of the ImageButton tags in main.html produces the following screen-filling buttons.


where on the left side the buttons are in their normal state and on the right side the duck button has been pressed.

 

Adding Click Events

We have now employed some of the view aspects of ImageButton to control the look of the widgets. Let's now exploit the button aspect to cause some things to happen. Open AnimalSounds.java and make the following changes.

  1. Implement the OnClickListener interface by adding "implements OnClickListener" to the class definition. This requires importing android.view.View.OnClickListener.

  2. Implementing the OnClickListener interface requires adding an onClick(View v) method stub.

  3. Attach click listeners in the onCreate method by adding for each of the three ImageButtons statements of the form
    
        View button1 = findViewById(R.id.ImageButton01);  
        button1.setOnClickListener(this);
    
    
    where R.id.ImageButton01 is the identifier for the first ImageButton, etc. This requires importing android.view.View.

  4. Fill out the onClick method with a switch statement that decides which button was pressed by getting the id of the View v and checking against the ids for the ImageButton widgets. For now, just put a Log.i() diagnostic output to the logcat stream indicating which button was pressed (which requires the import android.util.Log).

After these changes, AnimalSounds.java should read


    package com.lightcone.animalsounds;
    import android.app.Activity;
    import android.os.Bundle;

    import android.util.Log;
    import android.view.View;
    import android.view.View.OnClickListener;
    
    public class AnimalSounds extends Activity implements OnClickListener {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            
            // Add click listeners to all the ImageButtons
            
            View button1 = findViewById(R.id.ImageButton01);
            View button2 = findViewById(R.id.ImageButton02);
            View button3 = findViewById(R.id.ImageButton03);    
            button1.setOnClickListener(this);
            button2.setOnClickListener(this);
            button3.setOnClickListener(this);

        }
    
        // Required method if OnClickListener is implemented
        
        @Override
        public void onClick(View v) {
                
            // Find which ImageButton was pressed and take appropriate action
            
            switch(v.getId()){
            
                // The cow button
                case R.id.ImageButton01:
                        Log.i("Test", "Cow Button");
                break;
                
                // The duck button
                case R.id.ImageButton02:
                        Log.i("Test", "Duck Button");
                break;

                // The sheep button                
                case R.id.ImageButton03:
                        Log.i("Test", "Sheep Button");
                break;
            
            }       
        }
    }

Now if you execute the app and press the three buttons in succession, you should get output in the logcat stream indicating that the appropriate button has been pushed. Once we have confirmed that the buttons are responding correctly to events, the log.i statements can be removed or commented out.

 

Teaching Our Buttons to Quack

Finally, let's modify the onClick method so that pressing an animal image plays a sound characteristic of that animal. The key Android class for implementing audio playback is MediaPlayer, and you will find an overview of using the MediaPlayer for audio (and video) in the Audio and Video Android document. Teaching our buttons to moo, quack, and bleat requires the following steps.

  1. First we need some sound resources. These are normally stored in the res/raw directory. If this directory does not yet exist, create it by right-clicking on res, select New > Folder, and give the folder the name raw.

    Resources in the res/raw directory are not compressed when the .apk executable archive is packaged, so it is an appropriate place to put resources like sound or video that have their own compression. In this example we package the sounds with the app, but it is also possible to use MediaPlayer to play a sound from a stand-alone file in the device file system (for example, on the SD card), or a stream from a network source; see the overview in the Audio and Video Android document.


  2. Go to the course audio-video resource directory and copy the files cow.wav, duck.wav, and sheep.mp3 to the res/raw directory. You will then probably have to right-click on res/raw in Eclipse to get it to recognize and display the new files.

    In this example we will use both MP3 and WAV files, which Android supports. The core media formats for images, audio, and video that are built into the Android platform may be found in this table. Specific hardware devices may offer additional formats not contained in this list. Applications may use any media codec available on an Android-powered device---both those provided by the Android platform and those that are device-specific. However, particularly with audio and video, a developer should test target devices extensively to determine which formats work well in practice. For short sounds, as in this example, WAV and MP3 are good choices. For longer audio MP3 is a good choice because of favorable quality versus compression.


  3. To play a sound file copied to the res/raw directory (which gets packaged in the .apk file when the Android executable is created), we may create an instance of MediaPlayer using the static convenience method create(), with a reference to the sound-file resource as an argument (which requires importing android.media.MediaPlayer), and then execute the MediaPlayer seekTo(0) method to position at the beginning and the MediaPlayer start() method to initiate the audio.
Since in this example we use only very short audio clips, we will not be too concerned with other MediaPlayer methods like stop() or setLooping(Boolean) that would be important in managing more extensive audio resources. You should call release() on a MediaPlayer as soon as it is no longer needed. The MediaPlayer may tie up hardware accelerators and failure to release them may cause a future invocation of MediaPlayer to fail or to be forced to fall back on slower software acceleration. We shall address release of the MediaPlayer in the last section of this project.

If we implement these changes for each of the three cases represented by our three buttons, the listing of AnimalSounds.java becomes


package com.lightcone.animalsounds;

import android.app.Activity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;

public class AnimalSounds extends Activity implements OnClickListener {
	
    private MediaPlayer mp;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // Add click listeners to all the ImageButtons
        
        View button1 = findViewById(R.id.ImageButton01);
        View button2 = findViewById(R.id.ImageButton02);
        View button3 = findViewById(R.id.ImageButton03);    
        button1.setOnClickListener(this);
        button2.setOnClickListener(this);
        button3.setOnClickListener(this);

    }

    // Required method if OnClickListener is implemented
    
    @Override
    public void onClick(View v) {
            
        // Find which ImageButton was pressed and take appropriate action
        
        switch(v.getId()){
        
            // The cow button
            case R.id.ImageButton01:
                mp = MediaPlayer.create(this, R.raw.cow);	
            break;
            
            // The duck button
            case R.id.ImageButton02:
                mp = MediaPlayer.create(this, R.raw.duck);
            break;
            
            // The sheep button
            case R.id.ImageButton03:
                mp = MediaPlayer.create(this, R.raw.sheep);
            break;
        
        }	
        mp.seekTo(0);
        mp.start();	
    }
}

where, for example, R.raw.sheep is Android's reference to the audio resource res/raw/sheep.mp3. Now if we execute the app we should find that when we press the buttons the cow moos, the duck quacks, and the sheep goes baah!

 

Lifecycle Management Issues

Our app does what we set out to accomplish, but there is one problem. On my phone, if I start the app that we just created and hit the buttons in random order many times in rapid succession, it will eventually crash (although there is a pretty cool animal symphony going on before that happens). This is presumably because, even though we are doing something simple with short sounds, failure to release the MediaPlayer between invocations eventually overtaxes the system.

In this context this isn't too serious since I forced the error by rather abusive use of the app. But in a more complex setting such problems could be much more serious, cropping up even in more normal usage. So let's learn to be better Android citizens by releasing the MediaPlayer object before we start another sound if it has already played a sound, and also by releasing the MediaPlayer (if it was instantiated) when the app goes into the background. We can accomplish this by inserting a statement


    if(mp != null) mp.release();

in the onClick method before the switch statement, and overriding the onPause() method and adding the same statement there. (We must test for null in calling onRelease() because the MediaPlayer might not have been created yet.) Thus our final listing for AnimalSounds.java is


    package com.lightcone.animalsounds;
    
    import android.app.Activity;
    import android.media.MediaPlayer;
    import android.os.Bundle;
    import android.view.View;
    import android.view.View.OnClickListener;
    
    public class AnimalSounds extends Activity implements OnClickListener {
        
        private MediaPlayer mp;
        
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            
            // Add click listeners to all the ImageButtons
            
            View button1 = findViewById(R.id.ImageButton01);
            View button2 = findViewById(R.id.ImageButton02);
            View button3 = findViewById(R.id.ImageButton03);    
            button1.setOnClickListener(this);
            button2.setOnClickListener(this);
            button3.setOnClickListener(this);
            
        }
    
        // Required method if OnClickListener is implemented
        
        @Override
        public void onClick(View v) {
            
            // Play only one sound at a time
            if(mp != null) mp.release();
            
            // Find which ImageButton was pressed and take appropriate action
            
            switch(v.getId()){
            
                // The cow button
                case R.id.ImageButton01:
                    mp = MediaPlayer.create(this, R.raw.cow);	
                break;
                
                // The duck button
                case R.id.ImageButton02:
                    mp = MediaPlayer.create(this, R.raw.duck);
                break;
                
                // The sheep button
                case R.id.ImageButton03:
                    mp = MediaPlayer.create(this, R.raw.sheep);
                break;
            
            }
            mp.seekTo(0);
            mp.start();	
        }
        
        @Override
        public void onPause() {
            super.onPause();
            // Release the MediaPlayer if going into background
            if(mp != null) mp.release();
        }
        
    }

(For our simple example it isn't necessary to restore the MediaPlayer by overriding onResume() when coming back to the foreground, since it will be instantiated and initialized within onClick anyway.) With this version, even if I hit the buttons many times in rapid succession on my phone, there is never more than one sound playing and the system is stable, and if the app is sent to the background and then brought back to the foreground it still functions correctly.

 

Providing Alternative Resources

Best practices in Android programming promote externalizing resources: separation of function and content from display, which is facilitated by putting layout in XML resource files separate from the Java programming. This permits these resources to be maintained separate from the actual code, but equally important is that this makes it easy for a programmer to provide alternative resources that support different device configurations (for example, different screen sizes, orientations, or default languages) that are managed by Android.

 

Default and Alternative Resources

This works in the following way. For any type of resource (strings, images, sound files ...), the programmer has the option of supplying two categories of resources:

To specify that a group of resources are alternative resources designed for a specific configuration, we append an appropriate qualifier to the directory (folder) name.


The list of valid directory qualifiers may be found in the Providing Resources Android document.

For example, the default UI layout is placed in the res/layout directory, but you can specify a different UI layout for landscape orientation simply by creating a res/layout-land directory and saving the alternative resource there. Then Android automatically applies the appropriate resources by matching the device's current configuration to your resource directory names: if it senses that a portrait layout is appropriate for the current configuration of the device it automatically uses the resources that you supplied in res/layout, and if it senses that the current device configuration corresponds to landscape mode it instead uses the resources that you have supplied in the res/layout-land directory. It really is that simple!

 

Example:Portrait and Landscape Modes for AnimalSounds

Let's illustrate for our current animal sounds project. My Motorola Backflip phone exhibits both of the common ways in which smartphones switch between vertical (portrait) mode and horizontal (landscape) mode:

  1. Rotation of the phone between vertical and horizontal causes the switch (triggered by the phone's acceleration/orientation sensors).

  2. Opening the physical keyboard automatically switches the screen to horizontal mode, since the keyboard is arranged horizontally.

If the phone is switched to horizontal mode by one of these actions the AnimalSounds app as presently written gives the following display



which is not an optimal use of the horizontal screen space. To provide an alternative view for when the screen of the device is in the landscape display mode

  1. Right-click on res, select New > Folder, give the new folder the name layout-land, and click Finish.

  2. Right-click on res/layout/main.xml, copy it, and paste it into the new res/layout-land folder. NOTE: be sure to put it inside the layout-land folder; a file pasted directly into res will give a compiler error.

  3. Edit the new res/layout-land/main.xml file to read
    
        <?xml version="1.0" encoding="utf-8"?>
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" >
        
            <ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent" 
                android:layout_height="fill_parent" android:src="@drawable/button1" 
                android:background="@color/transparent" android:scaleType="fitStart"
                android:layout_weight="1">
            </ImageButton>
            <ImageButton android:id="@+id/ImageButton02" android:layout_width="fill_parent" 
                android:layout_height="fill_parent" android:src="@drawable/button2" 
                android:background="@color/transparent" android:scaleType="fitCenter"
                android:layout_weight="1">
            </ImageButton>
            <ImageButton android:id="@+id/ImageButton03" android:layout_width="fill_parent" 
                android:layout_height="fill_parent" android:src="@drawable/button3" 
                android:background="@color/transparent" android:scaleType="fitEnd"
                android:layout_weight="1">
            </ImageButton>
                        
        </LinearLayout>
    
    
    where changes from res/layout/main.xml are marked in red.

That's it; the rest is up to Android. Now if we execute the app, while the phone is in portrait mode we get the same display as before (because Android is using the default res/layout/main.xml resource to format the screen), but if the phone is flipped to horizontal mode (the reason and means are irrelevant for us as developers), we get the following display



because now Android has sensed that the device is in horizontal mode and has selected our layout in res/layout-land/main.xml to format the screen. If we press the buttons we find that they moo, quack, and bleat, just as before, but now the display is at least a little more appropriate visually for a landscape layout.

Obviously we could do even better, for example by using in the landscape layout different images with aspect ratios better suited to a horizontal display. But this exercise illustrates the essential point that it is very easy as a developer to supply alternative resources that Android will manage without our intervention.

For many other potential applications of alternative resources, see the overviews in Providing Resources and Localization.


The complete project for the application described above is archived at the link AnimalSounds.



Exercises

1. Gather some additional animal images and sounds. Modify the AnimalSounds app so that each time a button is pressed the image changes to a new animal after the sound plays. Add some logic so that each button on the screen corresponds to a different animal at any one time. [Solution]

2. In the above project we prevent animal sounds from overlapping. Modify the app to create an "animal symphony" by allowing sounds to loop and overlap when a button is pressed. However, limit the number of sounds that can play concurrently so that the app is stable (too many instances of the MediaPlayer will eventually crash the app). [Solution]


Previous  | Next  | Home