Saturday, December 26, 2009

Web Service truy xuất cơ sở dữ liệu

Trong bài này, chúng ta sẽ làm 1 ứng dụng web service truy xuất đến cơ sở dữ liệu.

Phần 1: tạo Cơ sở dữ liệu

Tạo Databse tên LogonWS, tạo 1 bảng tên Users với cấu trúc như hình sau
Column Name Data Type

userID


nvarchar(30)

Password


nvarchar(50)

Nhập vài mẫu tin để thử.

Tạo 1 DSN với tên LogonWS tham chiếu đến database này.

Phần 2: Tạo Web Service

File->New->Dynamic Web Project, đặt tên cho project là WS_DB



Nhấn Finish

Thêm vào 1 package tên vovanhai.wordpress.com

Thêm vào lớp tên LongonService.java có nội dung như sau:

package vovanhai.wordpress.com;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.ResultSet;

import java.sql.Statement;

public class LogonService {

/**

* Đăng nhập hệ thống

* @param userName

* @param password

* @return 1: thành công; 0: sai Password; -1: sai username

*/

public int Logon(String userName,String password){

Connection con = null;

Statement stm = null;

ResultSet rs = null;

int result = 0;

String selectStatement = “select * from Users where userID=’”+userName+“‘”;

try{

Class.forName(“sun.jdbc.odbc.JdbcOdbcDriver”);

con = DriverManager.getConnection(“jdbc:odbc:LogonWS”,“sa”,“”);

stm = con.createStatement();

rs = stm.executeQuery(selectStatement);

if(!rs.next())

result = -1;//sai username

else{

String psw=rs.getString(“password”);

if(psw.equals(password)){

result= 1; //thành công

}

else

result= 0;//sai password

}

}catch(Exception ex){

ex.printStackTrace();

}

return result;

}

}

Kết quả như hình sau khi xem trong Package Explorer



Nhấn phải chuột lên lớp LogonService.java trong project Explorer, chọn New->Others…



Chọn Web Service, nhấn Next. Kết quả như hình



Nhấn Finish để hoàn tất công việc. Eclipse sẽ tựn động cài các thư viện và phát sinh các trong web quản lý của Axis2. Đồng thời eclipse cũng mở cho chúng ta cửa sổ Web Service Exploere như hình, và chúng ta có thể kiểm tra web service của chúng ta trên đó.



Bây giờ chúng ta mở rộng thư mục axis2-web của project trong cửa sổ Project Explorer, tìm đến trang index.jsp, nhấn chuột phải lên trang này, chọn Run As->Run on Server, chọn Tomcat rồi nhấn Finish, kết quả như hình sau





Nhấn chọn Service ta được



Ta có thể xem WSDL file bằng cách nhấn vào link LogonService. Ghi nhớ lại URL này.

http://localhost:8086/WS_DB/services/LogonService?wsdl

Vậy là chúng ta có 1 Web Service truy xuất CSDL.

Bây giờ ta thử phát triển 1 ứng dụng JSP Web Client truy xuất Web Service này, xem ở đây

Tạo Web services với JAX-WS 2.0 và Java SE 6 Platform

Phần 1: tạo Web Service và publish nó

Tạo lớp .java với nội dung

package hello;

import javax.jws.WebService;

@WebService

public class CircleFunctions {
public double getArea(double r) {
return java.lang.Math.PI * (r * r);
}

public double getCircumference(double r) {
return 2 * java.lang.Math.PI * r;
}
}

Chúng ta có thể dùng phương thức publish() của lớp javax.xml.ws.Endpoint để publish lớp vừa tạo thành 1 web service trong context root được chỉ định. Ở đây ta tạo lớp PubWS.java có nội dung sau:

package hello;
import javax.xml.ws.Endpoint;
public class PubWS{
public static void main(String[] args) {
Endpoint.publish(
"http://localhost:8080/WebServiceExample/circlefunctions",
new CircleFunctions());
}
}

Bây giờ ta hãy compile source code bằng lệnh sau
javac -d . *.java

Phải đảm bảo rằng không có lỗi nào xảy ra.

Sau đó ta dùng tool wsgen để sinh ra các lớp hỗ trợ cho việc tạo webservice cũng như sinh WSDL file đồng thời public lên Web Server, cú pháp như sau:
wsgen -cp . hello.CircleFunctions

Bây giờ ta có thể public webservice bằng cách thự thi lệnh sau:
java hello.PubWS

Như vậy ta đang có 1 webservice đang được thực thi trên web server.
Mở trình duyệt, duyệt đến địa chỉ http://localhost:8080/WebServiceExample/circlefunctions?WSDL ta sẽ có kết quả như hình:
Ws_J2SE



Và, chúng ta có thể viết client ứng dụng web service này được rồi đó!
Phần 2: Tạo client

1. Tạo thư mục client, mở cửa sổ command, chạy lệnh sau để sinh ra các lớp đặc tả dịch vụ web service từ WSDL URL
wsimport http://localhost:8080/WebServiceExample/circlefunctions?WSDL

2. Tạo lớp client với nội dung sau

import hello.*;
import javax.xml.ws.WebServiceRef;
public class Client{
@WebServiceRef(wsdlLocation = "http://localhost:8080/WebServiceExample/circlefunctions?WSDL")
public static void main(String[] args){
Client client = new Client();
client.test();
}
public void test(){
try{
CircleFunctionsService service=new CircleFunctionsService();
CircleFunctions port =service.getCircleFunctionsPort();
double ret =port.getArea(3.0d);
System.out.println("Area result = " + ret);
}catch(Exception e){
e.printStackTrace();
}
}
}

3. Biên dịch file Client.java bằng lệnh sau
javac Client.java

Bạn chắc là không có lỗi nào xảy ra chứ?
4. Thực thi ứng dụng
java Client

Kết quả thu được của bạn như sau
D:\Bai giang Aptech\Aptech – Semester 4\XML Webservices with Java\examples\ex03_
JAX_WS\client>java Client
Area result = 28.274333882308138

D:\Bai giang Aptech\Aptech – Semester 4\XML Webservices with Java\examples\ex03_
JAX_WS\client>pause
Press any key to continue . . .
Chúc thành công!

Thursday, December 17, 2009

MusicDroid - Audio Player Part III

Introduction

In part 1 and 2 we created a simple media player, but there was no way to do anything except play a selected song. Now we must create some kind of interface to control the music. I am not a great graphical designer so the controls might be a little bland for now, but it demonstrates how to control layouts using RelativeLayouts and ImageViews, as well as animating the image views. It also demonstrates how to create a transparent activity.

Click here to download the full source.

Layout for 4-way controls

For the controls menu that is pictured we will use 4 image views, in 2 relative layouts. Here is controls.xml:


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

       

          <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

              android:layout_width="170dip"

              android:layout_height="170dip"

              android:layout_centerVertical="true"

              android:layout_centerHorizontal="true">

         

              <ImageView id="@+id/pause"

                  android:layout_alignParentTop="true"

                  android:layout_centerHorizontal="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menupause" />

         

              <ImageView id="@+id/skipb"

                  android:layout_alignParentLeft="true"

                  android:layout_centerVertical="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menuskipb" />

             

              <ImageView id="@+id/skipf"

                  android:layout_alignParentRight="true"

                  android:layout_centerVertical="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menuskipf" />

         

              <ImageView id="@+id/stop"

                  android:focusable="true"

                  android:layout_alignParentBottom="true"

                      android:layout_centerHorizontal="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menustop" />

       

          </RelativeLayout>

      </RelativeLayout>

The first Relative layout fills the full width and height of the screen so that we can center the second relative layout which is 170x170 pixel square which will hold our buttons.

To center a View in the horizontal we use "android:layout_centerHorizontal", and to center in the vertical we use "android:layout_centerVertial". With RelativeLayouts the order in which you list the items does not matter, you place them using these alignment techniques.

So, we create 4 ImageViews here. The first is the pause button which is centered horizontally and aligned at the top vertically. The bottom button is the stop button, which is also centered horizontally, but vertically positioned at the bottom. And of course skip forward and back are both centered vertically, but aligned to the right and left.

Animation

Animation xml files are placed in a folder (you will have to create), "res/anim". Animation XML files are very easy to use to translate, scale, rotate, adjust alpha, and create a tint. There are currently some limitations with the animation functionality unfortunately. The 2 big issues right now is that the AnimationListener functionality does not work. This means that there is no way to tell when an animation is finished running. Also, there is a property called "fillAfter" which does not work. This property, when set, will not redraw the object when an animation is finished. For example if you set fillAfter to true, and then rotate an object, when the object is done rotating it should stay at the new angle. Unfortunately, it will always redraw the original object when the animation is finished.

For this example we will do just a very simple scale animation, so that when you hit a control the icon gets large and then scales smaller. Please feel free to mess around with this, remember you can scale, translate, rotate, adjust alpha, and tint objects. If you come up with something really cool let us know in the forum!

Here is our simple scale animation, which we can create in "res/anim" and call selected.xml:

 


      <?xml version="1.0" encoding="utf-8"?>

      <set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false" android:fillAfter="true">

       

          <scale

              android:fromXScale="1.5"

              android:fromYScale="1.5"

              android:toXScale="1"

              android:toYScale="1"

              android:duration="600"

              android:pivotX="50%"

              android:pivotY="50%"

              android:fillAfter="true"

              />

         

      </set>

So lets talk about what we are saying here. We are saying that when this animation starts we want to have the image scaled to 1.5 in the X and Y, and when the animation is done we want it to scale back to 1 in the X and Y. The duration states that this animation will take 600 ms. The pivotX and pivotY describe the points at which to scale around. Ie, you could have the left edge stay where it is and have it expand to the right, but this case we just specifiy to scale from the center.

This is a simple animation that works for the controls that we have designed, but lets say after the scale you wanted to do another animation, for example fade the object away. Although that does not make sense for these controls, I thought it would be good information to include. So to create another action that will execute after the scale you can create another tag, alpha in this example, and set the "startOffset" equal to the ammount of time that it took for the first animation to complete (or more or less if you want to have it start sooner or delay). Here is an example of an alpha animation that could be inserted after the scale animation above:

 


      <alpha android:fillAfter="true"

          android:startOffset="600"

          android:fromAlpha="1.0"

          android:toAlpha="0.0"

          android:duration="400" />

I recommend taking a look at the different types of animations in the API demos to see more.

Transparency using Themes and Colors

To make this activity transparent we will use a custom theme. All that we need to do is create a custom theme and set the background color to a color that we must define. This is very easy because colors are 8 digit hexidecimal numbers which include the alpha value. The last 6 digits are the RGB values, much like an HTML color. The first 2 digits is the alpha value, and that is what we must set to get a translucent background.

To create a theme you create a file called "styles.xml" in the "res/values" folder. It is not required to be named styles.xml, but that is the best practice. Here is the styles.xml file we will use:

 


      <?xml version="1.0" encoding="utf-8"?>

      <resources>

       

          <style name="Theme" parent="android:Theme.Dark"></style>

       

          <style name="Theme.Transparent">

              <item name="android:windowBackground">@drawable/transparent_background</item>

              <item name="android:windowNoTitle">true</item>

          </style>

      </resources>

As you can see the theme we created is a child to the standard dark theme, this means that it will inherit its properties. All we need to do is set the background color and hide the window's title. The color referred to here as "@drawable/transparent_background" is defined in colors.xml in the "res/values" folder:

 


      <?xml version="1.0" encoding="utf-8"?>

      <resources>

           <drawable name="transparent_background">#a0000000</drawable>

      </resources>

Notice that there is no color defined in the last 6 digits, we just define the alpha value to be A0 out of FF (160 out of 255 for those that don't automatically convert hex in their heads).

Now that we have this custom theme setup we have to somehow tell our ControlsMenu Activity (which we'll create on the next page) to use this custom theme. That is done in the AndroidManifest.xml file, here is the entry we will add for this new Activity:

 


      <activity class=".ControlsMenu" android:label="@string/app_name"

                  android:theme="@style/Theme.Transparent" />

No we have created all the XML we need, so lets put it to use in the next page and create the ControlsMenu Activity...

The Activity

This is a very simple Activity. It's not very smart, it's not aware of what is being played (yet). It's only role is to display 4 images, and when a user hits a key corresponding to one of them it needs to send a command to the service using the Interface from part 2 and trigger an animation to start.

Because this is a simple Activity we'll show the whole thing and discuss after:

 


      public class ControlsMenu extends Activity {

       

          private ImageView pauseImage;

          private ImageView skipbImage;

          private ImageView skipfImage;

          private ImageView stopImage;

          private MDSInterface mpInterface;

       

          @Override

          public void onCreate(Bundle icicle) {

              super.onCreate(icicle);

       

              setContentView(R.layout.controls);

       

              pauseImage = (ImageView) findViewById(R.id.pause);

              skipbImage = (ImageView) findViewById(R.id.skipb);

              skipfImage = (ImageView) findViewById(R.id.skipf);

              stopImage = (ImageView) findViewById(R.id.stop);

       

              this.bindService(new Intent(this, MDService.class), null, mConnection,

                      Context.BIND_AUTO_CREATE);

          }

       

          @Override

          public boolean onKeyUp(int keyCode, KeyEvent event) {

              try {

                  switch (keyCode) {

                  case KeyEvent.KEYCODE_DPAD_UP:

                      handleAnimation(pauseImage);

                      mpInterface.pause();

                      break;

                  case KeyEvent.KEYCODE_DPAD_LEFT:

                      handleAnimation(skipbImage);

                      mpInterface.skipBack();

                      break;

                  case KeyEvent.KEYCODE_DPAD_RIGHT:

                      handleAnimation(skipfImage);

                      mpInterface.skipForward();

                      break;

                  case KeyEvent.KEYCODE_DPAD_DOWN:

                      handleAnimation(stopImage);

                      mpInterface.stop();

                      break;

                  }

       

              } catch (DeadObjectException e) {

                  Log.e(getString(R.string.app_name), e.getMessage());

              }

       

              return super.onKeyUp(keyCode, event);

          }

       

          public void handleAnimation(View v) {

              v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.selected));

          }

       

          private ServiceConnection mConnection = new ServiceConnection() {

              public void onServiceConnected(ComponentName className, IBinder service) {

                  mpInterface = MDSInterface.Stub.asInterface((IBinder) service);

              }

       

              public void onServiceDisconnected(ComponentName className) {

                  mpInterface = null;

              }

          };

      }

First in the onCreate() function we initialize all of our private variables for the ImageView objects. Then we attempt to bind to our service we created. Once this binds it will call onServiceConnected down on line 74 which will initialize our interface to the service.

The user input is handled in the onKeyUp(int, KeyEvent) function on line 41. We just create a switch with the KeyCode that was pressed. If a relavent key is pressed we initiate an animation with the handleAnimation(View) function (line 69) and then send the appropriate command to the service.

You can see on line 70 that it's very easy to start playing an animation on a view. You just call it's startAnimation(Animation) function passing in an Animation as it's only argument. We use AnimationUtils to get the animation from it's resource id (R.anim.selected).

Now, all that is left is to launch this new Activity when a user selects a song, here is the new onListItemClick() from the MusicDroid ListView:

 


      @Override

      protected void onListItemClick(ListView l, View v, int position, long id) {

          try {

              Intent intent = new Intent(this, ControlsMenu.class);

              startSubActivity(intent,0);

              mpInterface.playFile(position);

          } catch(DeadObjectException e) {

              Log.e(getString(R.string.app_name), e.getMessage());

          }

      }

So playing with these new controls you can see that they are not ideal, because the controls have no idea what's going on...in part 4 we will attempt to change that. We will also add ID3 support so that we can also display the Artist/Song information, and a progress indicator on the song playing....so check back!

MusicDroid - Audio Player Part II

In part one of the MusicDroid tutorial we created a simple MP3 player that will list all of the songs on the SD card and allow the user to select a song to play. Now, we will move the MediaPlayer object into a remote service. This will allow the music to continue in the background while the user is doing other things on their phone.

Click here to download the complete source to reference for this tutorial.

What are services?

Services are components that run in the background and do not display a view for the user to interact with. These components must be listed in the Androidmanifest.xml file with a <service> element. When the Activity that started the service is closed Android will attempt to keep the service running if possible.

To create a new Service you extend the android.app.Service class. Then for your activity to connect to the service it would call either Context.startService() or Context.bindService(). Attempts to connect to services are asynchronous, that means that after you call one of the functions to start your service your code will not wait for it to connect to the service before continuing on. Instead, you must pass in a ServiceConnection object, and then ServiceConnection.onServiceConnected() will be called when the service is connected.

Interacting with services

We need a way to send and receive data from the service to our Activities which are using this service. This is done using an IInterface class. It is quite complicated to create this interface, so Google has made it easy on us. They have created the "Android Interface Definition Language" or more frequently referred to as AIDL. So, to create an Interface for a service you just need to create an AIDL file in your src folder (the same folder with all your other classes), and when you save the .aidl file it will automatically generate a class that extends IInterface based on your AIDL file.

For the MusicDroid project we are going to create a service called MDService (Music Droid Service). We will call our interface for this service MDSInterface. Here is MDSInterface.aidl:


      package com.helloandroid.android.musicdroid2;

       

      interface MDSInterface {

              void clearPlaylist();

              void addSongPlaylist( in String song );

              void playFile( in int position );

       

              void pause();

              void stop();

              void skipForward();

              void skipBack();

      }

This is just a very basic outline of the functions that we will need to control a media player. Unfortunately, there are still some issues with AIDL and Interfaces, so you are kinda limited on the types of the arguments and return values. For example I would have liked to use a function "void setPlaylist( in List songs )", but due to a bug in the current SDK that will give an error. So, I'm making due by using a function "void addSongPlaylist( in String song )".

Notice the that the arguments in the functions say things like "in String song". This signifies the direction of the data, ie you are are reading the value from song, not intending to write the value of song. You can use "out String song" if that was the case.

So when you save this MDSInterface.aidl it will create a file called MDSInterface.java which defines the class MDSInterface. In the class MDSInterface is the is a public abstract class called Stub, and this is what you must create a subclass of in your MDService class.

So, to make this a little easier lets look at the MDService class, with all of the MediaPlayer specific code pulled out for the time being so you can see how the interface is created:


      public class MDService extends Service {

       

              private MediaPlayer mp = new MediaPlayer();

              private List<String> songs = new ArrayList<String>();

              private int currentPosition;

       

              private NotificationManager nm;

              private static final int NOTIFY_ID = R.layout.songlist;

...


              @Override

              public IBinder getBinder() {

                      return mBinder;

              }

...


              private final MDSInterface.Stub mBinder = new MDSInterface.Stub() {

       

                      public void playFile(int position) throws DeadObjectException {

                              try {

                                      currentPosition = position;

                                      playSong(MusicDroid.MEDIA_PATH + songs.get(position));

       

                              } catch (IndexOutOfBoundsException e) {

                                      Log.e(getString(R.string.app_name), e.getMessage());

                              }

                      }

       

                      public void addSongPlaylist(String song) throws DeadObjectException {

                              songs.add(song);

                      }

       

                      public void clearPlaylist() throws DeadObjectException {

                              songs.clear();

                      }

       

                      public void skipBack() throws DeadObjectException {

                              prevSong();

       

                      }

       

                      public void skipForward() throws DeadObjectException {

                              nextSong();

                      }

       

                      public void pause() throws DeadObjectException {

                              Notification notification = new Notification(

                                              R.drawable.playbackpause, null, null, null, null);

                              nm.notify(NOTIFY_ID, notification);

                              mp.pause();

                      }

       

                      public void stop() throws DeadObjectException {

                              nm.cancel(NOTIFY_ID);

                              mp.stop();

                      }

       

              };

      }

The code above is everything that you need to implement the interface. You'll see first on line 40 the getBinder() function. This will return the mBinder variable that is defined starting on line 86. This mBinder variable is the MDSInterface.Stub class that you must create to define all of those functions in the AIDL file.

Tip: In Eclipse to make overriding all these functions easier you can type in line 86 and hit enter. Then in the empty block starting on line 87 you can right click and goto "Source -> Override / Implement Methods"

So, we are implementing all of these interface functions that we defined earlier in the AIDL file. First in playFile(int) we simply set the currentPosition and call playSong(String) passing in the path to the song. This is very similar to the functionality that was built into the MusicDroid ListActivity in MusicDroid back in Part 1.

Here is the the playSong(String) method:


      private void playSong(String file) {

              try {

       

                      Notification notification = new Notification(

                                      R.drawable.playbackstart, file, null, file, null);

                      nm.notify(NOTIFY_ID, notification);

       

                      mp.reset();

                      mp.setDataSource(file);

                      mp.prepare();

                      mp.start();

       

                      mp.setOnCompletionListener(new OnCompletionListener() {

       

                              public void onCompletion(MediaPlayer arg0) {

                                      nextSong();

                              }

                      });

       

              } catch (IOException e) {

                      Log.e(getString(R.string.app_name), e.getMessage());

              }

      }

Note that this is almost identical to the playSong function from Part 1. However, those really paying attention will notice a difference on lines 47-49. As you see we are now going to create a notification each time a song plays.

To do this we will need to add 2 files to the res/drawable folder, playbackstart.png and playbackpause.png. Once they are added to the "res/drawable" folder they will be referenced by their int value for their id. The first argument for the Notification constructor in the top status bar icon to use. Since this is the playSong() function we want to use the playbackstart.png, which can be referred to as "R.drawable.playbackstart". For the second and fourth parameter we are passing in the filename, this is the text that will be displayed on the status bar animation.

After we create out Notification object we will use our NotificationManager to initiate the notification. this is done on line 49, with nm.notify(int,Notification). We pass in an int, NOTIFY_ID that we will use refer to this notification icon when we need to modify or remove it, along with the Notification that we created.

The NotificationManager is initialized in our onCreate() function, and we make sure to remove the icon with nm.cancel(int) when the service is destroyed in the onDestroy() function:


      @Override

      protected void onCreate() {

              super.onCreate();

              nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

             

      }

       

      @Override

      protected void onDestroy() {

              mp.stop();

              mp.release();

              nm.cancel(NOTIFY_ID);

      }

       

      @Override

      public IBinder getBinder() {

              return mBinder;

      }

We must also cancel the notification after playing the last song on the playlist in our very familiar nextSong() function that is moved into our new Service:


      private void nextSong() {

              // Check if last song or not

              if (++currentPosition >= songs.size()) {

                      currentPosition = 0;

                      nm.cancel(NOTIFY_ID);

              } else {

                      playSong(MusicDroid.MEDIA_PATH + songs.get(currentPosition));

              }

      }

One last function that we have created is prevSong(). We saw it called above on line 107 to handle the work in skipBack() from the interface. Here is prevSong():


      private void prevSong() {

              if (mp.getCurrentPosition() < 3000 && currentPosition >= 1) {

                      playSong(MusicDroid.MEDIA_PATH + songs.get(--currentPosition));

              } else {

                      playSong(MusicDroid.MEDIA_PATH + songs.get(currentPosition));

              }

      }

This function is designed to for the skip back functionality that will be shown in Part 3. The idea here is that if you hit the skip back button in the controls then your song will restart, and if you hit it again then it will go the previous song. So here we go to the previous song if we are less than 3 seconds into the song, and we are not listening to the first song on the list (position 0).

So there you have it, a service to handle everything. On the next page we will look at how to bind to this service, and the changes that we need to make to the MusicDroid class to use the new service...

<!--pagebreak-->

Using Services

In order to use a remote service first we must add a line to our AndroidManifest.xml file inside our application tag to define our service, here is that line:

<service class=".MDService" android:process=":remote" />

Now we can use this service in our MusicDroid ListActivity class that we created in the previous tutorial. We will now try to bind to our newly created service in the onCreate(Bundle) function of our ListActivity:


      public class MusicDroid extends ListActivity {

       

              public static final String MEDIA_PATH = new String("/sdcard/");

              private List<String> songs = new ArrayList<String>();

              private MDSInterface mpInterface;

             

              @Override

              public void onCreate(Bundle icicle) {

                      super.onCreate(icicle);

                      setContentView(R.layout.songlist);

                      this.bindService(new Intent(MusicDroid.this,MDService.class),

                                      null, mConnection, Context.BIND_AUTO_CREATE);

              }

      }

We are binding to our service using a new Intent object, and we are creating the Intent object using the direct reference to a class. You could use a Action and Category to bind to a service also, but for this we can just refer to it directly. We pass in the mConnection variable, this is a ServiceConnection object, and when the service is connected it will call the mConnection.onServiceConnected method, and when disconnected it will call mConnection.onServiceDisconnected. Here is how we define mConnection:


      private ServiceConnection mConnection = new ServiceConnection()

      {

              public void onServiceConnected(ComponentName className, IBinder service) {

                      mpInterface = MDSInterface.Stub.asInterface((IBinder)service);

                      updateSongList();

              }

       

              public void onServiceDisconnected(ComponentName className) {

                      mpInterface = null;

              }

      };

So, when the service is connected we can initialize our mpInterface class on line 74. Now that we have this mpInterface class we need to populate the playlist by adding each song using mpInterface.addSongPlaylist(String), we do this in the updateSongList() function:


      public void updateSongList() {

              try {

                      File home = new File(MEDIA_PATH);

                      File fileList[] = home.listFiles(new Mp3Filter());

                      mpInterface.clearPlaylist();

                      if (fileList != null) {

                              for (File file : fileList ) {

                                      songs.add(file.getName());

                                      mpInterface.addSongPlaylist(file.getName());

                              }

             

                              ArrayAdapter<String> songList = new ArrayAdapter<String>(this,

                                              R.layout.song_item, songs);

                              setListAdapter(songList);

                      }

              } catch(DeadObjectException e) {

                      Log.e(getString(R.string.app_name), e.getMessage());

              }

      }

This function is pretty much identical to it's first incarnation in the previous tutorial. The only thing we needed to add is the mpInterface.addSongPlaylist(String) call on line 49 so that our Service is aware of the entire playlist, so that it can play the next song without having help from another Activity.

And lastly, we must play a song when a user clicks on a song, so here is that method:


      @Override

      protected void onListItemClick(ListView l, View v, int position, long id) {

              try {

                      mpInterface.playFile(position);

              } catch(DeadObjectException e) {

                      Log.e(getString(R.string.app_name), e.getMessage());

              }

      }

We simply use the mpInterface.playFile(int) method that we designed earlier in this tutorial.

So now we have a playlist ListActivity and a Service to handle playing the music, but we still have no way to control the music. In the next section we will create a graphical user interface for the controls, introducing ImageViews and Animation...

MusicDroid - Audio Player Part I

As you can imagine a popular way to load music onto a cell phone will be via removable storage, such as an SD card. In part 1 of our media player tutorial we will build a simple media player that will allow the user to select a song from the SD card and play it.

Click here if you would like to download the source for this tutorial.

Note: there is a known issue with the Emulator where the mixer will cut in and out on some systems resulting in very choppy audio, hopefully this will be addressed in the next SDK release.

Layouts

This project only consists of one Activity, a ListActivity. So, for a ListActivity we need a ListView for the actual list, and another view that will be used for each item in the list. You can get fancy, but for this example we will just use a TextView to display the name of each file.

First, here is our ListView (songlist.xml):


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

       

          <ListView id="@id/android:list"

                    android:layout_width="fill_parent"

                    android:layout_height="fill_parent"

                    android:layout_weight="1"

                    android:drawSelectorOnTop="false"/>

       

          <TextView id="@id/android:empty"

                    android:layout_width="fill_parent"

                    android:layout_height="fill_parent"

                    android:text="No songs found on SD Card."/>

      </LinearLayout>

Very standard ListView. The TextView entry will display when there are no items in the ListView because it's using the built-in id of "@id/android:empty".

And for each file here is the TextView to be used (song_item.xml):


      <?xml version="1.0" encoding="utf-8"?>

      <TextView id="@+id/text1" xmlns:android="http://schemas.android.com/apk/res/android"

          android:layout_width="wrap_content"

          android:layout_height="wrap_content"/>

Again, very basic, shouldn't be anything you haven't seen before.

You may be thinking, why does the screenshot above show a black ListView, when nothing in these layouts mentions color? Well, that is determined by the "theme" in the AndroidManifest.xml. In the "application" element you can define the theme by adding "android:theme="@android:style/Theme.Dark"".

The ListActivity

We now must work on our ListActivity which we will call MusicDroid. Here is the declaration of this class and it's onCreate() function:


      public class MusicDroid extends ListActivity {

       

              private static final String MEDIA_PATH = new String("/sdcard/");

              private List<String> songs = new ArrayList<String>();

              private MediaPlayer mp = new MediaPlayer();

              private int currentPosition = 0;

       

              @Override

              public void onCreate(Bundle icicle) {

                      super.onCreate(icicle);

                      setContentView(R.layout.songlist);

                      updateSongList();

              }

First we set up some private member variables to be used by this Activity. The first one is MEDIA_PATH, and we set it to "/sdcard" because that is the location of the SD card. Next comes a List of Strings that will hold the filename for each song on the list. And of course we need a MediaPlayer object, which we call mp. The final one up there is currentPosition, which we will use to store the index of the song currently playing.

The onCreate() function is pretty basic, we set our view to be the songlist view that we created above and call the function updateSongList(), here is that function:


      public void updateSongList() {

              File home = new File(MEDIA_PATH);

              if (home.listFiles(new Mp3Filter()).length > 0) {

                      for (File file : home.listFiles(new Mp3Filter())) {

                              songs.add(file.getName());

                      }

       

                      ArrayAdapter<String> songList = new ArrayAdapter<String>(this,

                                      R.layout.song_item, songs);

                      setListAdapter(songList);

              }

      }

Here we create a File Object called "home" which points to "/sdcard". We loop through the files returned by home.ListFiles(), adding each file to our List object "songs". Once we have this list filled we create an ArrayAdapter passing in the songs list and then set it to be our ListActivity's ListAdapter on line 47. This will populate our ListView.

You may have noticed the object above called "Mp3Filter". This is an object that implements FilenameFilter. This object is used to filter out what files should be returned, this is done by implementing the accept(File, String) function. Here is the object that we can use to filter so that listFiles() only returns MP3 files:


      class Mp3Filter implements FilenameFilter {

              public boolean accept(File dir, String name) {

                      return (name.endsWith(".mp3"));

              }

      }

Now we should be able to build a list of all the MP3 files in /sdcard. So now we just need to be able to select a song and play it. Fist things first, let's override onListItemClick() so we will be notified when a song is clicked on:


      @Override

      protected void onListItemClick(ListView l, View v, int position, long id) {

              currentPosition = position;

              playSong(MEDIA_PATH + songs.get(position));

      }

Pretty basic function here. We set currentPosition to hold the index of the position that was clicked on and then pass in the path of the song to playSong(String), so lets take a look at what's happening in playSong(String):


      private void playSong(String songPath) {

              try {

       

                      mp.reset();

                      mp.setDataSource(songPath);

                      mp.prepare();

                      mp.start();

       

                      // Setup listener so next song starts automatically

                      mp.setOnCompletionListener(new OnCompletionListener() {

       

                              public void onCompletion(MediaPlayer arg0) {

                                      nextSong();

                              }

       

                      });

       

              } catch (IOException e) {

                      Log.v(getString(R.string.app_name), e.getMessage());

              }

      }

The MediaPlayer object makes things really easy for us here. First we call mp.reset(), which will reset the MediaPlayer to its normal state. This is required if you were playing a song and want to change the data source. The reset() function will also stop whatever is playing, so if a song is playing and then you select another it will stop that one before starting the next song.

We then pass in the path to the song to mp.setDataSource(String) and call prepare() and start(). At this point the MediaPlayer will start playing your song.

Next job is to setup an OnCompletionListener starting on line 66. The function onCompletion(MediaPlayer) will be called when the song is over. All we do there is call the function nextSong() from our Activity. Here is nextSong():


      private void nextSong() {

              if (++currentPosition >= songs.size()) {

                      // Last song, just reset currentPosition

                      currentPosition = 0;

              } else {

                      // Play next song

                      playSong(MEDIA_PATH + songs.get(currentPosition));

              }

      }

Here we check to make sure this isn't the last song on the list, if it is we won't do anything, if not we'll play the next song using the playSong(String) function.

So that's it for the code, on the next page we'll figure out how to get this thing running...

<!--pagebreak-->

Emulator Command Line Options

There are 2 command line options that must be set for this project to work. One to mount an SD card so that you can access it at "/sdcard" and another to enable audio.

To enable the SD card first you need to create an sd card image, to do this you need to open a command prompt in your SDK/tools folder and type the following command:

mksdcard <size>M <file>

Where <size> is the size of the SD card in MB, and this must be reasonably large as a small SD card image actually crashes the emulator. And <file> is the path to the file to create. This is the command that I used:

mksdcard 128M c:\temp\sd.img

Now your SD card image is setup, so once you have your project setup you will need to supply the 2 command line arguments. So, in Eclipse go to "Run -> Open Run Dialog". Now you may need to create a new configuration if there is not one there for your application, you can do that by double clicking on "Android Application" and supplying the Name, Project, and starting Activity on the "Android" tab. On the "Emulator" tab you need to supply 2 arguments, "-sdcard <file>" and "-useaudio".

It should look something like this:

So now that this is all setup we should be ready to run this thing right? Doh, here's what we see:

We need to add songs to the SD card for this thing to work. This is pretty easy, just open a command prompt in the SDK/tools folder once again and use the "adb push <file> <destination>" command. For example:

adb push "c:\music\02 Paranoid Android.mp3" /sdcard

And after you push a song or many songs out there you'll be able to start using this very primitive music player.

What do you have to look forward to in the next installments?

    * Creating a service for the MediaPlayer
    * Adding animated controls
    * ID3 Tag Support

So, be sure to check back!

NewsDroid RSS Reader

Welcome to our first tutorial! This tutorial assumes that you are familiar with the official tutorial supplied by Google. If you have not run through that I would highly recommend it!

The syntax highlighting in these tutorials will create hyperlinks to the documentation out of general Java and Android specific classes. So when you see a class like "Activity" in the code you can click on it and be taken to the documentation for that class. Also, the line numbers next to the code correspond to the line numbers in the java files.

In this tutorial we will build a simple RSS reader called "NewsDroid" so that you can get all the headlines without having to hit all your favorite sites in that little browser. The application will open with a list of all of the RSS feeds that you have added, and when a user clicks on a feed then it will display a list of the headlines for that feed. When a user then selects a headline they will be taken directly to that story in the web browser.

This application consists of 7 classes:

  • Simple storage classes:
    • Article - Simple class to hold the values of an article, ie the the title and url
    • Feed - Simple class to hold the values of a feed, ie the title of the feed and url to the XML file
  • Backend Classes:
    • NewsDroidDB - Handles all the SQL Lite database work
    • RSSHandler - Parses RSS feed and saves what we need from them
  • Frontend Classes:
    • ArticlesList - ListActivity that displays the Articles
    • FeedsList - ListActivity that displays the Feeds
    • URLEditor - Activity that allows the user to enter the URL for the feed

You may want to download the complete source for reference as you view this tutorial.

On page 2 we discuss the database backend components.
On page 3 we discuss the RSS Parsing using a SAX object.
On page 4 we put it all together with a couple ListActivities and a URL Entry Box

NewsDroid Backend

Our first task will be to create the NewsDroidDB class. This class will be responsible for creating the database and inserting or deleting records. Before we create the NewsDroidDB we need to design the layout of the database:

For each RSS Feed we need the the title of the feed and the URL for the xml file so that we can fetch the articles. In addition we will want a unique id for each feed so that we can have an articles table that relates to the feeds table. So to create our table "feeds" we will run a create statement as follows:

create table feeds (feed_id integer primary key autoincrement, title text not null, url text not null);

And for the articles we need to store the feed_id so that we know what feed the article is for. We also need the title of the article and the url to the article:

create table articles (article_id integer primary key autoincrement, feed_id int not null, title text not null, url text not null);

Now that we know the layout of our tables we can start working on our NewsDroidDB class. Here are the list of things this class will need to accomplish:

  • Create Tables - We will need to be able to create tables for the feeds and articles if they don't exist, this will be handled in the constructor.
  • Insert Data - Will need to be able to insert a feed or an article.
  • Delete Data - If we can insert we need to be able to delete right?
  • Get Data - We need to be able to pull the data to display in our ListActivities

We'll tackle these tasks in order. Here is the NewsDroidDB class declaration and the constructor:

  1. public class NewsDroidDB {
  2. private static final String CREATE_TABLE_FEEDS = "create table feeds (feed_id integer primary key autoincrement, "
  3. + "title text not null, url text not null);";
  4. private static final String CREATE_TABLE_ARTICLES = "create table articles (article_id integer primary key autoincrement, "
  5. + "feed_id int not null, title text not null, url text not null);";
  6. private static final String FEEDS_TABLE = "feeds";
  7. private static final String ARTICLES_TABLE = "articles";
  8. private static final String DATABASE_NAME = "newdroid";
  9. private static final int DATABASE_VERSION = 1;
  10. private SQLiteDatabase db;
  11. public NewsDroidDB(Context ctx) {
  12. try {
  13. db = ctx.openDatabase(DATABASE_NAME, null);
  14. } catch (FileNotFoundException e) {
  15. try {
  16. db = ctx.createDatabase(DATABASE_NAME, DATABASE_VERSION, 0,
  17. null);
  18. db.execSQL(CREATE_TABLE_FEEDS);
  19. db.execSQL(CREATE_TABLE_ARTICLES);
  20. } catch (FileNotFoundException e1) {
  21. db = null;
  22. }
  23. }
  24. }

Here we declare the class and some of the variables that we will be using for database access, including the create statements, table names, database name, and database version. The constructor takes the context as the argument so that it can initialize the SQLiteDatabase object "db".

On line 33 it will attempt to connect to the database, if the database does not exists it will throw the FileNotFoundException and we will create the database and the tables in the database on lines 36-39.

Now we need functions for inserting feeds and deleting feeds:

  1. public boolean insertFeed(String title, URL url) {
  2. ContentValues values = new ContentValues();
  3. values.put("title", title);
  4. values.put("url", url.toString());
  5. return (db.insert(FEEDS_TABLE, null, values) > 0);
  6. }
  7. public boolean deleteFeed(Long feedId) {
  8. return (db.delete(FEEDS_TABLE, "feed_id=" + feedId.toString(), null) > 0);
  9. }

Both of these functions are boolean and return true if the task was sucessful. As you can see using a ContentValues object and a SQLiteDatabase object it is very easy to do these basic operations.

Here are 2 very similar functions for dealing with articles:

  1. public boolean insertArticle(Long feedId, String title, URL url) {
  2. ContentValues values = new ContentValues();
  3. values.put("feed_id", feedId);
  4. values.put("title", title);
  5. values.put("url", url.toString());
  6. return (db.insert(ARTICLES_TABLE, null, values) > 0);
  7. }
  8. public boolean deleteAricles(Long feedId) {
  9. return (db.delete(ARTICLES_TABLE, "feed_id=" + feedId.toString(), null) > 0);
  10. }

Now we must create functions to return the data, and to help us do this we will create 2 classes mentioned in the introduction Articles and Feeds. These just objects that store all of our values and will be very similar to the layout of the tables. Here are their declarations:

  1. class Article extends Object {
  2. public long articleId;
  3. public long feedId;
  4. public String title;
  5. public URL url;
  6. }
  7. class Feed extends Object {
  8. public long feedId;
  9. public String title;
  10. public URL url;
  11. }

Now that we have these storage objects we can create a function that will return a List object for each of these. First we will look at the getFeeds() function:

  1. public List<Feed> getFeeds() {
  2. ArrayList<Feed> feeds = new ArrayList<Feed>();
  3. try {
  4. Cursor c = db.query(FEEDS_TABLE, new String[] { "feed_id", "title",
  5. "url" }, null, null, null, null, null);
  6. int numRows = c.count();
  7. c.first();
  8. for (int i = 0; i < numRows; ++i) {
  9. Feed feed = new Feed();
  10. feed.feedId = c.getLong(0);
  11. feed.title = c.getString(1);
  12. feed.url = new URL(c.getString(2));
  13. feeds.add(feed);
  14. c.next();
  15. }
  16. } catch (SQLException e) {
  17. Log.e("NewsDroid", e.toString());
  18. } catch (MalformedURLException e) {
  19. Log.e("NewsDroid", e.toString());
  20. }
  21. return feeds;
  22. }

This function returns a List of Feed objects. This list will be used to populate the FeedsList ListActivity, as we will see later. The first step is to query the table using the SQLiteDatabase.query function. Because we just want all of the records from the feeds table this is easy. The query will return a Cursor object which we can loop through and create a new Feed object. As we loop through we add each new feed to the ArrayList feeds via the feeds.add(feed) on line 82.

Here is the function to get the list of articles for a specific feed:

  1. public List<Article> getArticles(Long feedId) {
  2. ArrayList<Article> articles = new ArrayList<Article>();
  3. try {
  4. Cursor c = db.query(ARTICLES_TABLE, new String[] { "article_id",
  5. "feed_id", "title", "url" },
  6. "feed_id=" + feedId.toString(), null, null, null, null);
  7. int numRows = c.count();
  8. c.first();
  9. for (int i = 0; i < numRows; ++i) {
  10. Article article = new Article();
  11. article.articleId = c.getLong(0);
  12. article.feedId = c.getLong(1);
  13. article.title = c.getString(2);
  14. article.url = new URL(c.getString(3));
  15. articles.add(article);
  16. c.next();
  17. }
  18. } catch (SQLException e) {
  19. Log.e("NewsDroid", e.toString());
  20. } catch (MalformedURLException e) {
  21. Log.e("NewsDroid", e.toString());
  22. }
  23. return articles;
  24. }

As you can see this function is almost identical to to the getFeeds function except that it takes in an argument "feedId" and uses this argument in the query function on line 96. The 3rd argument in the db.query function is the "WHERE" argument (minus the "WHERE"), and here we specify "feed_id=X", where X is the feedId passed into the function.

This is all we need for the NewDroidDB class. We can now move on to parsing the RSS document and getting the data into the database...

NewsDroid RSS Parsing

Now it is our goal to download an RSS xml file and get what we need out of it. To do this we will use "Simple API for XML", or SAX. This library is included in the Android SDK and is great for parsing XML data. Before we get started figuring out a parser, lets take a look at the format of an RSS feed:

  1. xml version="1.0" encoding="utf-8" ?>
  2. <rss version="2.0" xml:base="http://www.helloandroid.com" xmlns:dc="http://purl.org/dc/elements/1.1/">
  3. <channel>
  4. <title>Hello Android - Android OS news, tutorials, downloadstitle>
  5. <link>http://www.helloandroid.com
  6. <description />
  7. <language>enlanguage>
  8. <item>
  9. <title>Biggest story of the year!title>
  10. <link>http://www.helloandroid.com/node/59
  11. <description>Here is a teaser for the story.description>
  12. <comments>http://www.helloandroid.com/node/59#comments
  13. <pubDate>Sat, 17 Nov 2007 15:07:25 -0600pubDate>
  14. <dc:creator>hobbsdc:creator>
  15. item>
  16. channel>
  17. rss>

Seems pretty simple right? It better, considering it's really simple syndication! So, lets figure out what we want to pull out of this, and when. If the user is adding a new feed, he or she will supply the URL and then we need to go out and figure out the title of this feed before we can add it to the database. That would be found in the "title" element on line 4, in this case "Hello Android - Android OS news, tutorials, downloads". So once a feed is added and the users selects a feed on the feeds list we need to grab all of the headlines for the articles table, and the url direct link to the article. This is the "title" element and "link" element found in each "item" element. In this example it is lines 9 and 10.

So, to do this we will now create a class "RSSHandler" and extend org.xml.sax.helpers.DefaultHandler. For this class I'm just going to show you the complete class and will explain what's happening below:

  1. public class RSSHandler extends DefaultHandler {
  2. // Used to define what elements we are currently in
  3. private boolean inItem = false;
  4. private boolean inTitle = false;
  5. private boolean inLink = false;
  6. // Feed and Article objects to use for temporary storage
  7. private Article currentArticle = new Article();
  8. private Feed currentFeed = new Feed();
  9. // Number of articles added so far
  10. private int articlesAdded = 0;
  11. // Number of articles to download
  12. private static final int ARTICLES_LIMIT = 15;
  13. // The possible values for targetFlag
  14. private static final int TARGET_FEED = 0;
  15. private static final int TARGET_ARTICLES = 1;
  16. // A flag to know if looking for Articles or Feed name
  17. private int targetFlag;
  18. private NewsDroidDB droidDB = null;
  19. public void startElement(String uri, String name, String qName,
  20. Attributes atts) {
  21. if (name.trim().equals("title"))
  22. inTitle = true;
  23. else if (name.trim().equals("item"))
  24. inItem = true;
  25. else if (name.trim().equals("link"))
  26. inLink = true;
  27. }
  28. public void endElement(String uri, String name, String qName)
  29. throws SAXException {
  30. if (name.trim().equals("title"))
  31. inTitle = false;
  32. else if (name.trim().equals("item"))
  33. inItem = false;
  34. else if (name.trim().equals("link"))
  35. inLink = false;
  36. // Check if looking for feed, and if feed is complete
  37. if (targetFlag == TARGET_FEED && currentFeed.url != null
  38. && currentFeed.title != null) {
  39. // We know everything we need to know, so insert feed and exit
  40. droidDB.insertFeed(currentFeed.title, currentFeed.url);
  41. throw new SAXException();
  42. }
  43. // Check if looking for article, and if article is complete
  44. if (targetFlag == TARGET_ARTICLES && currentArticle.url != null
  45. && currentArticle.title != null) {
  46. droidDB.insertArticle(currentFeed.feedId, currentArticle.title,
  47. currentArticle.url);
  48. currentArticle.title = null;
  49. currentArticle.url = null;
  50. // Lets check if we've hit our limit on number of articles
  51. articlesAdded++;
  52. if (articlesAdded >= ARTICLES_LIMIT)
  53. throw new SAXException();
  54. }
  55. }
  56. public void characters(char ch[], int start, int length) {
  57. String chars = (new String(ch).substring(start, start + length));
  58. try {
  59. // If not in item, then title/link refers to feed
  60. if (!inItem) {
  61. if (inTitle)
  62. currentFeed.title = chars;
  63. } else {
  64. if (inLink)
  65. currentArticle.url = new URL(chars);
  66. if (inTitle)
  67. currentArticle.title = chars;
  68. }
  69. } catch (MalformedURLException e) {
  70. Log.e("NewsDroid", e.toString());
  71. }
  72. }
  73. public void createFeed(Context ctx, URL url) {
  74. try {
  75. targetFlag = TARGET_FEED;
  76. droidDB = new NewsDroidDB(ctx);
  77. currentFeed.url = url;
  78. SAXParserFactory spf = SAXParserFactory.newInstance();
  79. SAXParser sp = spf.newSAXParser();
  80. XMLReader xr = sp.getXMLReader();
  81. xr.setContentHandler(this);
  82. xr.parse(new InputSource(url.openStream()));
  83. } catch (IOException e) {
  84. Log.e("NewsDroid", e.toString());
  85. } catch (SAXException e) {
  86. Log.e("NewsDroid", e.toString());
  87. } catch (ParserConfigurationException e) {
  88. Log.e("NewsDroid", e.toString());
  89. }
  90. }
  91. public void updateArticles(Context ctx, Feed feed) {
  92. try {
  93. targetFlag = TARGET_ARTICLES;
  94. droidDB = new NewsDroidDB(ctx);
  95. currentFeed = feed;
  96. SAXParserFactory spf = SAXParserFactory.newInstance();
  97. SAXParser sp = spf.newSAXParser();
  98. XMLReader xr = sp.getXMLReader();
  99. xr.setContentHandler(this);
  100. xr.parse(new InputSource(currentFeed.url.openStream()));
  101. } catch (IOException e) {
  102. Log.e("NewsDroid", e.toString());
  103. } catch (SAXException e) {
  104. Log.e("NewsDroid", e.toString());
  105. } catch (ParserConfigurationException e) {
  106. Log.e("NewsDroid", e.toString());
  107. }
  108. }
  109. }

So what is going on here you ask?? Lets look the body of the createFeed() function:

  1. targetFlag = TARGET_FEED;
  2. droidDB = new NewsDroidDB(ctx);
  3. currentFeed.url = url;
  4. SAXParserFactory spf = SAXParserFactory.newInstance();
  5. SAXParser sp = spf.newSAXParser();
  6. XMLReader xr = sp.getXMLReader();
  7. xr.setContentHandler(this);
  8. xr.parse(new InputSource(url.openStream()));

First we set the targetFlag so that the rest of the functions know what our target is from the XML file, we are looking specifically for a feed. Next we initialize our droidDB object so that once we get everything we need from the XML we can stick it in the database. After that we set what we know in our currentFeed object (the url the user input). We will update the currentFeed object or currentArticle object as we find out more from the XML file. For the feed object we just need to find its title. For the articles we need the title and the url from the XML.

Now things get SAX specific. We create a SAXParserFactory so we can create a SAXParser so we can create a XMLReader! Anyways, lines 120 and 121 are where we kick this off. We set this class as our ContentHandler and we pass in the xml stream to the XMLReader using the supplied url. At this point the parsing begins.

Now we have 3 functions working to parse this document, it starts at the beginning of the document and calls each of these 3 functions as it moves through the document:

  • startElement - This function is called when the parser reaches an element
  • endElement - This function is called when the parser reaches the end of an element
  • characters - This is called when we are between the start and end

To illustrate this using a simple xml tag: chars

All 3 funtions would be called to parse this string. First startElement() would be called passing in "ele" for the second argument. Then characters() would be called passing in an array of characters "chars". Then endElement() would be called passing in "ele" for the second argument.

So what we can do to parse this XML file is to setup 3 boolean variables starting at line 23. These variables are inItem, inTitle, and inLink. Now, when startElement() is called we can set these values to true if we are entering into one of the 3 elements "item", "title" or "link". We also set these to false when leaving these elements in endElement(). So now when characters() is called we know where we are at and can update the values in currentArticle and currentFeed, you can see this happening at line 96.

Eventually all the fields of currentArticle or currentFeed will be set and it will be time to insert it into the database. We can check this in endElement(), and that can be seen on line 66 and line 75. After we insert a feed we can throw a SAXException to stop parsing, or after we collect enough articles we can stop parsing.

Now we can parse the XML files, handle the database stuff so lets show it to the user now! We'll build the UI on the next page...

User Interface

The user interface for this application is made up of 3 different Activities. The first activity the user comes to, FeedsList, is a simple list activity that displays all of the feeds the user has added, and gives the option to add more feeds or delete the selected feed. The layout of these Activities are very similar to that in Google's Notepad tutorial, so the complete code will not be included in this page. Instead we will highlight a couple of important points.

An important function to look at is the onListItemClick() function for this class:

  1. @Override
  2. protected void onListItemClick(ListView l, View v, int position, long id) {
  3. super.onListItemClick(l, v, position, id);
  4. Intent i = new Intent(this, ArticlesList.class);
  5. i.putExtra("feed_id", feeds.get(position).feedId);
  6. i.putExtra("title", feeds.get(position).title);
  7. i.putExtra("url", feeds.get(position).url.toString());
  8. startSubActivity(i, ACTIVITY_VIEW);
  9. }

This code is executed when the user clicks on a feed. The job of this function is to bring up the ArticleList Activity showing the headlines for the selected feed. The information is passed by adding all of the information to describe a feed to the Intent and then starting the Activity. If we look at the constructor for the ArticlesList Activity we can see how this is used:

  1. @Override
  2. protected void onCreate(Bundle icicle) {
  3. try {
  4. super.onCreate(icicle);
  5. droidDB = new NewsDroidDB(this);
  6. setContentView(R.layout.articles_list);
  7. feed = new Feed();
  8. if (icicle != null) {
  9. feed.feedId = icicle.getLong("feed_id");
  10. feed.title = icicle.getString("title");
  11. feed.url = new URL(icicle.getString("url"));
  12. } else {
  13. Bundle extras = getIntent().getExtras();
  14. feed.feedId = extras.getLong("feed_id");
  15. feed.title = extras.getString("title");
  16. feed.url = new URL(extras.getString("url"));
  17. droidDB.deleteAricles(feed.feedId);
  18. RSSHandler rh = new RSSHandler();
  19. rh.updateArticles(this, feed);
  20. }
  21. fillData();
  22. } catch (MalformedURLException e) {
  23. Log.e("NewsDroid",e.toString());
  24. }
  25. }

When the ArticlesList Activity is created we need to define the values of the private feed object. There are 2 ways that this info could be coming in, either because the Activity was paused or even killed, or because it's being called for the first time via an Intent from FeedsList. When it's called for the first time then we will check the extras from the intent. This is what's happening starting at line 37. If it's being called for the first time we also need to delete all the articles currently in the database and update the table with the new articles from the XML file.

If it is being resumed then we will get the data from the icicle, and we will use the same data that was in there before. To make sure that information is in the icicle we must implement the onFreeze() function as follows:

  1. @Override
  2. protected void onFreeze(Bundle outState) {
  3. super.onFreeze(outState);
  4. outState.putLong("feed_id", feed.feedId);
  5. outState.putString("title", feed.title);
  6. outState.putString("url", feed.url.toString());
  7. }

When a user clicks on a headline we need to launch the browser, it is very simple to create an intent to launch a browser, and we will do this in OnListItemClick():

  1. @Override
  2. protected void onListItemClick(ListView l, View v, int position, long id) {
  3. super.onListItemClick(l, v, position, id);
  4. Intent myIntent = new Intent(Intent.VIEW_ACTION, ContentURI.create(articles.get(position).url.toString()));
  5. startActivity(myIntent);
  6. }

The final Activity is the URLEditor shown above, this is the screen where the user inputs the URL to the RSS feed. This is a simple activity, here is the source:

  1. public class URLEditor extends Activity {
  2. EditText mText;
  3. @Override
  4. public void onCreate(Bundle icicle) {
  5. super.onCreate(icicle);
  6. setContentView(R.layout.url_editor);
  7. // Set up click handlers for the text field and button
  8. mText = (EditText) this.findViewById(R.id.url);
  9. if (icicle != null)
  10. mText.setText(icicle.getString("url"));
  11. Button ok = (Button) findViewById(R.id.ok);
  12. ok.setOnClickListener(new View.OnClickListener() {
  13. public void onClick(View arg0) {
  14. okClicked();
  15. }
  16. });
  17. Button cancel = (Button) findViewById(R.id.cancel);
  18. cancel.setOnClickListener(new View.OnClickListener() {
  19. public void onClick(View arg0) {
  20. finish();
  21. }
  22. });
  23. }
  24. protected void okClicked() {
  25. try {
  26. RSSHandler rh = new RSSHandler();
  27. rh.createFeed(this, new URL(mText.getText().toString()));
  28. finish();
  29. } catch (MalformedURLException e) {
  30. showAlert("Invalid URL", "The URL you have entered is invalid.", "Ok", false);
  31. }
  32. }
  33. @Override
  34. protected void onFreeze(Bundle outState) {
  35. super.onFreeze(outState);
  36. outState.putString("url", mText.getText().toString());
  37. }
  38. }

There are 2 buttons that must be handeld, and we do that with nested functions in the onCreate() function. When the "OK" button is clicked we call the okClicked() function. This function will create a feed by calling RSSHandler.createFeed() passing in the text from the input field. We also must handle the Activity lifecycle, and save whatever is in the text box so that if this activity is interupted the partial URL will remain in the box when it's resumed.

As you can see the UI is very basic, but could easily be extended to look much slicker. Hopefully you have enjoyed our first tutorial, and hopefully we will be able to get some more your way.