Developing an Android App: MyTwitter Part 3



Goals of Part 3

 

1. Using Database Handler (DbHandler)
2. Adding a Service (UpdaterService)
3. Creating a Custom Adapter (TimelineAdapter)
4. Presentation (timeline.xml & row.xml)
5. Timeline Activity (Timeline)

To further enhance the MyTwitter application beyond the simple twitter update functionality and preference menu. A few enhancements are necessary including;

It would be nice to view all the status updates of th people who you are following and can be viewed. The status updates could be stored in a database and together with a service can be used to fetch the twitter status information in the background.


1. Using Database Handler (DbHandler)

 

 

 

Our database is used to store the Time, Statuses and Username.

As before we use a Database handler to handle the database connections including creations, updates and upgrades of the SQLite3 database.


DbHandler.java

 

 

 

Code:
package com.example;

import winterwell.jtwitter.Twitter.Status;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import android.util.Log;

public class DbHelper extends SQLiteOpenHelper {
  Context context;
  static final String TAG = "DbHelper";
  static final String DB_NAME = "timeline.db";
  static final int    DB_VERSION = 5;
  static final String TABLE = "timeline";
  static final String C_ID = BaseColumns._ID;
  static final String C_CREATED_AT = "created_at";
  static final String C_TEXT = "status";
  static final String C_USER = "user";

  /** Constructor for DbHelper */
  public DbHelper(Context context) {
    super(context, DB_NAME, null, DB_VERSION);
    this.context = context;
  }

  /** Called only once, first time database is created */
  @Override
  public void onCreate(SQLiteDatabase db) {
    // String sql = String.format(
    // "CREATE table %s ( " +
    // "%s INTEGER NOT NULL PRIMARY KEY," +
    // "%s INTEGER, %s TEXT, %s TEXT)",
    // TABLE, C_ID, C_CREATED_AT, C_TEXT, C_USER);
    String sql = context.getResources().getString(R.string.sql);
    db.execSQL(sql); // execute the sql
    Log.d(TAG, "onCreate'd sql: " + sql);
  }

  /** Called every time DB version changes */
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    Log.d(TAG, String
            .format("onUpgrade from %s to %s", oldVersion, newVersion));
    // temporary
    db.execSQL("DROP TABLE IF EXISTS timeline;");
    this.onCreate(db);
  }
  
  /** Converts Twitter.Status to ContentValues */
  public static ContentValues statusToContentValues(Status status) {
    ContentValues ret = new ContentValues();
    ret.put(C_ID, status.id);
    ret.put(C_CREATED_AT, status.getCreatedAt().getTime());
    ret.put(C_TEXT, status.getText());
    ret.put(C_USER, status.getUser().getScreenName());
    return ret;
  }
}


2. Adding a Service (UpdaterService)

 

 

UpdaterService service was created to work in the background to retrieve the twitter status updates at a set delay time. In this service the key parts again are the use of SharedPreferences to extract information from the preferences to get login information to retrieve twitter statuses.

Using getTwitter().getFriendsTimeline() method the service is able retrieve a List of Status objects. Also we have used a Androids Handler() interface to handle when the the service will run.

In our service, we have explicitly used the states of our service to handle different behaviors example onStart() - and begin an updater through the handler, onDestroy() for stopping the updater and closing the database connection.


UpdaterService.java

 

 

Code:
package com.example;

import java.util.List;

import winterwell.jtwitter.Twitter;
import winterwell.jtwitter.TwitterException;
import winterwell.jtwitter.Twitter.Status;
import android.app.Service;
import android.content.ContentValues;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;

/**
 * Responsible for pulling twitter updates from twitter.com and putting it into
 * the database.
 */
public class UpdaterService extends Service {
  static final String TAG = "UpdaterService";
  Twitter twitter;
  Handler handler;
  Updater updater;
  DbHelper dbHelper;
  SQLiteDatabase db;
  SharedPreferences prefs;

  @Override
  public void onCreate() {
    super.onCreate();
    // Get shared preferences
    prefs = PreferenceManager.getDefaultSharedPreferences(this);
    prefs
        .registerOnSharedPreferenceChangeListener(new OnSharedPreferenceChangeListener() {
          public void onSharedPreferenceChanged(SharedPreferences arg0,
              String arg1) {
            twitter = null;
          }
        });

    // Setup handler
    handler = new Handler();

    // Initialize DB
    dbHelper = new DbHelper(this);
    db = dbHelper.getWritableDatabase();
  }

  @Override
  public void onStart(Intent i, int startId) {
    super.onStart(i, startId);
    updater = new Updater();
    handler.post(updater);
    Log.d(TAG, "onStart'ed");
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    handler.removeCallbacks(updater); // stop the updater
    db.close();
    Log.d(TAG, "onDestroy'd");
  }

  @Override
  public IBinder onBind(Intent intent) {
    return null;
  }

  /** Updates the database from twitter.com data */
  class Updater implements Runnable {
    static final long DELAY = 100000L;

    public void run() {
      Log.d(UpdaterService.TAG, "Updater ran.");

      try {
        List<Status> timeline = getTwitter().getFriendsTimeline();
        for (Status status : timeline) {
          ContentValues values = DbHelper.statusToContentValues(status);
          // Insert will throw exceptions for duplicate IDs 
          try {
            db.insertOrThrow(DbHelper.TABLE, null, values);
          } catch(SQLException e) {}
          Log.d(TAG, "Got status: " + status.toString());
        }
      } catch (TwitterException e) {
        Log.e(TAG, "Updater.run exception: " + e);
      }
      // Set this to run again later
      handler.postDelayed(this, DELAY);
    }
  }

  // Initializes twitter, if needed
  private Twitter getTwitter() {
    if (twitter == null) {
      // TODO Fix case when login doesn't work
      String username = prefs.getString("username", null);
      String password = prefs.getString("password", null);

      if (username != null && password != null)
        twitter = new Twitter(username, password);
    }
    return twitter;
  }
}


3. Creating a Custom Adapter (TimelineAdapter)

 

 

 

To map the data to the Timeline view, we created a custom adapter which extends SimpleCursorAdaptor to provide corresponding data to its row view which lives in the larger timeline.xml.


TimelineAdapter.java

 

 

 

Code:
package com.example;

import android.content.Context;
import android.database.Cursor;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;

/** Custom adapter so we can apply transformations on the data */
public class TimelineAdapter extends SimpleCursorAdapter {
  static final String[] from = { DbHelper.C_CREATED_AT, DbHelper.C_USER,
      DbHelper.C_TEXT };
  static final int[] to = { R.id.textCreatedAt, R.id.textUser, R.id.textText };

  /** Constructor */
  public TimelineAdapter(Context context, Cursor c) {
    super(context, R.layout.row, c, from, to);
  }

  /** This is where data is mapped to its view */
  @Override
  public void bindView(View row, Context context, Cursor cursor) {
    super.bindView(row, context, cursor);

    // Get the individual pieces of data
    long createdAt = cursor.getLong(cursor
        .getColumnIndex(DbHelper.C_CREATED_AT));

    // Find views by id
    TextView textCreatedAt = (TextView) row.findViewById(R.id.textCreatedAt);

    // Apply custom transformations
    textCreatedAt.setText(DateUtils.getRelativeTimeSpanString(createdAt));
  }

}

 

 


4. Presentation (timeline.xml & row.xml)

timeline.xml

 

 

Contains all the rows for the timeline look.

 

Code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_height="fill_parent"
	android:layout_width="fill_parent"
	android:background="@drawable/background"
	android:orientation="vertical">
	<TextView
		style="@style/title"
		android:text="@string/labelTimeline" />
	<ListView
		android:layout_height="fill_parent"
		android:layout_width="fill_parent"
		android:id="@+id/listTimeline"
		android:background="@color/transparentBlue"></ListView>
</LinearLayout>

 


row.xml

 

 

Contains the individual row data

 

Code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:orientation="vertical">
	<LinearLayout
		android:layout_height="wrap_content"
		android:layout_width="fill_parent">
		<TextView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:id="@+id/textUser"
			android:text="Bobby"
			android:textColor="@color/darkBlue"
			android:textStyle="bold"></TextView>
		<TextView
			android:layout_height="wrap_content"
			android:id="@+id/textCreatedAt"
			android:text="10 minues ago"
			android:layout_gravity="right"
			android:gravity="right"
			android:layout_width="fill_parent"
			android:textColor="@color/darkBlue"></TextView>
	</LinearLayout>

	<TextView
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:id="@+id/textText"
		android:text="The latest status from Bobby"
		android:textColor="@color/darkBlue"
		android:autoLink="all"></TextView>

</LinearLayout>

 

 

 


5. Timeline Activity (Timeline)

 

 

 

 

Timeline is an activity that connects the timeline view with the data with a cursor to our custom data adapter (TimelineAdapter).


Timeline.java

 

 

Code:
package com.example;

import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.widget.ListView;

/** Displays the list of all timelines from the DB. */
public class Timeline extends Activity {
  static final String TAG = "Timeline";
  ListView listTimeline;
  DbHelper dbHelper;
  SQLiteDatabase db;
  Cursor cursor;
  TimelineAdapter adapter;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.timeline);

    // Find views by id
    listTimeline = (ListView) findViewById(R.id.listTimeline);

    // Initialize DB
    dbHelper = new DbHelper(this);
    db = dbHelper.getReadableDatabase();

    // Get the data from the DB
    cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
        DbHelper.C_CREATED_AT + " DESC");
    Log.d(TAG, "cursor got count: " + cursor.getCount());

    // Setup the adapter
    adapter = new TimelineAdapter(this, cursor);
    listTimeline.setAdapter(adapter);
  }
}

 

ScreenShot

Source
https://www.protechtraining.com/static/tutorials/MyTwitter-Part3.zip

Published February 3, 2010