Developing an Android App: MyTwitter Part 4



Goal of part 4

Basic MyTwitter is complete and we turn to enhance the application.

1. Using BroadcastReceiver & Notification services in the MyTwitter application
2. Use LocationServices through LocationHelper



1. Using BroadcastReciever & Notification services in the MyTwitter application

As we enhance the MyTwitter application, there was a need to send a message to the UpdaterService that a new message has been posted on your twitter account. Using the BroadcastReceiver and its related methods inside UpdaterService class, one can send a message to the notification manager to allow us to work with the notification.

UpdaterService class has the following added or changed to make this happen;

- Leaveraged the use of db.insertOrThrow(DbHelper.TABLE, null, values) to check if an insert in the database was performed since no exception was thrown by setting our flag haveNewStatus to true.
- Checking the haveNewStatus state determined if to send a Broadcast intent and create a notfication to sent.

Notifications require a Notification instance and NotificationManager i.e.;

Notification is the icon that appears on the top left corner of the Mytwitter screen when its running. And to ensure that the notification continues when phone boots we implement this BootReceiver class;

BootReceiver.java

 

Code:
package com.example;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

/** This receiver is called when system boots, 
 * so it can start our UpdaterService.
 */
public class BootReceiver extends BroadcastReceiver {
  static final String TAG = "BootReceiver";
  @Override
  public void onReceive(Context context, Intent intent) {
    context.startService(new Intent(context, UpdaterService.class));
    Log.d(TAG, "onReceive started UpdaterService");
  }
}

 


UpdaterService.java

 

 

 

Code:
import java.util.List;

import winterwell.jtwitter.Twitter;
import winterwell.jtwitter.TwitterException;
import winterwell.jtwitter.Twitter.Status;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
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";
  static final String ACTION_NEW_TWITTER_STATUS = "ACTION_NEW_TWITTER_STATUS";
  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();

    Log.d(TAG, "onCreate'd");
  }

  @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 int NOTIFICATION_ID = 47;
    static final long DELAY = 100000L;
    Notification notification;
    NotificationManager notificationManager;
    PendingIntent pendingIntent;

    Updater() {
      notificationManager = (NotificationManager) UpdaterService.this
          .getSystemService(Context.NOTIFICATION_SERVICE);
      notification = new Notification( android.R.drawable.stat_sys_download,
          "MyTwitter", System.currentTimeMillis());
      pendingIntent = PendingIntent.getActivity(UpdaterService.this, 0,
          new Intent(UpdaterService.this, Timeline.class), 0);
    }

    public void run() {
      boolean haveNewStatus = false;
      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);

            // We have a new status
            Log.d(TAG, "run() got new status: " + status.getText());
            haveNewStatus = true;
          } catch (SQLException e) {
          }
          Log.d(TAG, "Got status: " + status.toString());
        }
      } catch (TwitterException e) {
        Log.e(TAG, "Updater.run exception: " + e);
      }

      // If there's new status, send a broadcast & notify user
      if (haveNewStatus) {
        sendBroadcast(new Intent(ACTION_NEW_TWITTER_STATUS));
        Log.d(TAG, "run() sent ACTION_NEW_TWITTER_STATUS broadcast.");

        // Create the notification
        notification.setLatestEventInfo(UpdaterService.this,
            "New Twitter Status", "You have new tweets in the timeline",
            pendingIntent);
        notification.when = System.currentTimeMillis();

        notificationManager.notify(NOTIFICATION_ID, notification);
      }

      // 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", "");
      String password = prefs.getString("password", "");

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

 

 

Another addition to our Application was a change in the Timeline.java code where the broadcaster receiver is registered and there is a cursor.requery() to reset information on the Timeline activity.

Timeline.java

 

Code:
package com.example;

import android.app.Activity;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
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;
  BroadcastReceiver twitterStatusReceiver;

  @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");
    startManagingCursor(cursor);
    Log.d(TAG, "cursor got count: " + cursor.getCount());

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

    // Register to get ACTION_NEW_TWITTER_STATUS broadcasts
    twitterStatusReceiver = new TwitterStatusReceiver();
    registerReceiver(twitterStatusReceiver, new IntentFilter(
        UpdaterService.ACTION_NEW_TWITTER_STATUS));
  }

  @Override
  public void onResume() {
    super.onResume();
    // Cancel notification
    NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    notificationManager.cancel(UpdaterService.Updater.NOTIFICATION_ID);
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(twitterStatusReceiver);
  }

  class TwitterStatusReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
      Log.d(TAG, "onReceive got ACTION_NEW_TWITTER_STATUS broadcast");
      cursor.requery();
    }
  }
}

 

NOTE: Necessary to register BootReceiver in the AndroidManifest.xml file

 

 

Code:
    <receiver android:name=".BootReceiver">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
      </intent-filter>
    </receiver>
  </application>

  <uses-sdk android:minSdkVersion="3" />
...
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
...

 

 

 

 

2. Use LocationServices through LocationHelper

Another enhancement was to be able to parser our twitter update we post to look for a code "@loc" which would refer to geolocation when we sent the tweet from.

For this we require the use of LocationHelper class that would manage using the LocationManager and get the location of the cellphone by leaving replacing the "@loc" with "(longitude, latitude)" in the tweet message and since there is 140 character limit, we also made sure the output was not going to be beyond the allowed twitter limit.


LocationHelper.java

 

 

 

 

Code:
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.util.Log;

/** Helper class to add support for location in the status updates. */
public class LocationHelper {
  static final String TAG = "LocationHelper";
  static final String LOC = "@loc"; // code that gets replaced with location
  Context context;
  LocationManager locationManager;
  Location location;
  Criteria criteria;
  String bestProvider;

  public LocationHelper(Context context) {
    this.context = context;
    locationManager = (LocationManager) context
        .getSystemService(Context.LOCATION_SERVICE);
    criteria = new Criteria();
    bestProvider = locationManager.getBestProvider(criteria, false);
    location = locationManager.getLastKnownLocation(bestProvider);
    Log.d(TAG, "construct'd with location: " + location);
  }

  /** Converts the LOC code to current location */
  public String updateStatusForLocation(String input) {
    String output;
    if (location == null) {
      output = input.replaceAll(LOC, "UNKNOWN");
    } else {
      output = input.replaceAll(LOC, String.format("(%f,%f)", 
                location.getLongitude(), location.getLatitude()));
    }
    
    // Make sure we don't go over 140 characters
    output = (output.length() > 139) ? output.substring(0, 139) : output;
    
    Log.d(TAG, String.format("updateStatusForLocation(%s)=>%s", input, output));
    return output;
  }
}


AndroidManifest.xml

 

 

Code:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.example" android:versionCode="1" android:versionName="1.0">
  <application android:icon="@drawable/twitter_icon"
    android:label="@string/app_name">
    <activity android:name=".MyTwitter" android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <activity android:name=".Prefs" />
    <activity android:name=".Timeline" />
    <service android:name=".UpdaterService" />

    <receiver android:name=".BootReceiver">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
      </intent-filter>
    </receiver>
  </application>

  <uses-sdk android:minSdkVersion="3" />

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest> 

 

 

NOTE: To avoid repeating what each method does please review the comments inserted in the code.

 


ScreenShots



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

 

 

 

Published February 4, 2010