Objectives

Introduce Services into the Xtend YambaX application. Implement a background service to periodically update the twitter timeline

Service Menus

Introduce some new menu options, which we will use to trigger start/stop of a background service:

strings.xml

  <string name="titleServiceStop">Stop Service</string>
  <string name="titleServiceStart">Start Service</string>

menu.xml

    <item android:title="@string/titleServiceStart" android:id="@+id/itemServiceStart"
          android:icon="@android:drawable/ic_media_play"></item>  
    <item android:title="@string/titleServiceStop" android:id="@+id/itemServiceStop"
          android:icon="@android:drawable/ic_media_pause"></item>

Services are introduced by an entry in the manifest:

AndroidManifest.xml

        <service android:name="com.marakana.yambax.UpdaterService" />

This is a skeleton service implementation:

UpdaterSerice

package com.marakana.yambax

import android.app.Service
import android.content.Intent

class UpdaterService extends Service
{
  override onBind(Intent intent)
  {
  }

  override onCreate()
  { 
    super.onCreate
  }

  override onStartCommand(Intent intent, int flags, int startId)
  { 
    super.onStartCommand(intent, flags, startId)
    START_STICKY;
  }

  override onDestroy()
  { 
    super.onDestroy
  }
}

We then start/stop the service in StatusActivity:

StatusActivity

  override onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case R.id.itemServiceStart: startService (new Intent(this, typeof(UpdaterService)))
      case R.id.itemServiceStop:  stopService  (new Intent(this, typeof(UpdaterService)))
      case R.id.itemPrefs:        startActivity(new Intent(this, typeof(PrefsActivity)))
    }
   true
  }

Verify that this builds and executes.


Source


Service

In the UpdaterService, we define and initiate a background thread:

package com.marakana.yambax

import android.app.Service
import android.content.Intent

class Updater extends Thread
{
  val DELAY=10000
  var running = false

  override run()
  {
    running = true
    while (!interrupted() && running)
    {
      try
      {
        Thread.sleep(DELAY);
      }
      catch (InterruptedException e)
      {
        running = false
      }
    }
  }
}

class UpdaterService extends Service
{
  var Updater updater

  override onBind(Intent intent)
  {
  }

  override onCreate()
  {
    super.onCreate
    updater = new Updater
  }

  override onStartCommand(Intent intent, int flags, int startId)
  {
    super.onStartCommand(intent, flags, startId)
    updater.start
    START_STICKY;
  }

  override onDestroy()
  {
    super.onDestroy
    updater.interrupt
  }
}

We can rework the Updater class to be a lambda instead:

package com.marakana.yambax

import android.app.Service
import android.content.Intent

class UpdaterService extends Service
{
  val DELAY   = 10000
  var running = false 
  var Thread updateThread

  var updater = [ | 
                  while (!Thread.currentThread().isInterrupted() && running)
                  {
                    try
                    {
                      Thread.sleep(DELAY);
                    }
                    catch (InterruptedException e)
                    {}
                  } ] as Runnable

  override onBind(Intent intent)
  {
  } 

  override onCreate()
  {
    updateThread = new Thread(updater)
    super.onCreate
  }

  override onStartCommand(Intent intent, int flags, int startId)
  {
    super.onStartCommand(intent, flags, startId)
    running = true
    updateThread.start
    START_STICKY;
  }

  override onDestroy()
  {
    super.onDestroy
    running = false
    updateThread.interrupt
  }
}

We should now be able to start/stop the background service via the menus.


Source


Timeline

We would like to display the users timeline on the console. First, introduce an accessor to retrieve the timeline from the api:

TwitterAPI.xtend

  def getFriendsTimeline()
  {
    return twitter.getFriendsTimeline()
  }

The UpdaterService can be adjusted to now periodically request the update in the background thread:

UpdaterService.xtend

package com.marakana.yambax

import android.app.Service
import android.content.Intent
import java.util.List
import winterwell.jtwitter.Twitter;
import com.marakana.utils.TwitterAPI
import winterwell.jtwitter.TwitterException
import android.util.Log

class UpdaterService extends Service
{
  val DELAY   = 10000
  var running = false

  var Thread               updateThread
  var TwitterAPI           twitter
  var List<Twitter.Status> timeline;

  var updater = [ | 
                  while (!Thread.currentThread().isInterrupted() && running)
                  {
                    try
                    {
                      timeline = twitter.getFriendsTimeline()
                      timeline.forEach[ Log.d("YAMBA", String.format("%s: %s", it.user.name, it.text)); ]
                      Thread.sleep(DELAY);
                    }
                    catch (TwitterException e)
                    {
                      Log.e("YAMBA", "Failed to connect to twitter service", e); 
                    }
                    catch (InterruptedException e)
                    {}
                  } ] as Runnable

  override onBind(Intent intent)
  {
    null
  } 

  override onCreate()
  {
    var app = getApplication() as YambaApplication
    this.twitter = app.twitter
    updateThread = new Thread(updater)
    super.onCreate
  }

  override onStartCommand(Intent intent, int flags, int startId)
  {
    super.onStartCommand(intent, flags, startId)
    running = true
    updateThread.start
    START_STICKY;
  }

  override onDestroy()
  {
    super.onDestroy
    running = false
    updateThread.interrupt
  }
}

Launch the app, and verify that the logs contain the output of this periodic update:

Make sure that when you stop the service the updates cease.


Source


BackgoundService Class

We define a new abstract class to encapsulate a general purpose background service in com.marakana.utils:

BackgroundService.xtend

package com.marakana.utils

import android.app.Service
import android.content.Intent

abstract class BackgroundService extends Service
{
  val DELAY   = 10000
  var running = false 
  var Thread updateThread

  var updater = [ | 
                  while (!Thread.currentThread().isInterrupted() && running)
                  {
                    try
                    {
                      this.doBackgroundTask()
                      Thread.sleep(DELAY);
                    }
                    catch (InterruptedException e)
                    {}
                  } ] as Runnable

  override onBind(Intent intent)
  {
    null
  } 

  def abstract void doBackgroundTask()

  def startBackgroundTask()
  {
    running = true
    updateThread.start
  }

  def stopBackgroundTask()
  {
    running = false
    updateThread.interrupt
  }

  override onCreate()
  {
    updateThread = new Thread(updater)
    super.onCreate
  }
}

This will allow us to simplify the UpdaterService class:

UpdaterService.xtend

package com.marakana.yambax

import android.content.Intent
import java.util.List
import winterwell.jtwitter.Twitter;
import com.marakana.utils.TwitterAPI
import winterwell.jtwitter.TwitterException
import android.util.Log
import com.marakana.utils.BackgroundService

class UpdaterService extends BackgroundService
{
  var TwitterAPI           twitter
  var List<Twitter.Status> timeline;

  override onBind(Intent intent)
  {
    null
  } 

  override def void doBackgroundTask()
  {
    try
    {
      timeline = twitter.getFriendsTimeline()
      timeline.forEach[ Log.d("YAMBA", String.format("%s: %s", it.user.name, it.text))]
    }
    catch (TwitterException e)
    {
      Log.e("YAMBA", "Failed to connect to twitter service", e); 
    }
  }

  override onCreate()
  {
    super.onCreate
    var app = getApplication() as YambaApplication
    this.twitter = app.twitter
  }

  override onStartCommand(Intent intent, int flags, int startId)
  {
    super.onStartCommand(intent, flags, startId)
    startBackgroundTask
    START_STICKY;
  }

  override onDestroy()
  {
    super.onDestroy
    stopBackgroundTask
  }
}

Test the application again.


Source


Timeline UI

We can introduce a UI to render the timeline using these resources:

res/values/strings.xml

<resources>
  <string name="app_name">Yamba X</string>
  <string name="titleYamba">Yamba</string>
  <string name="titleStatus">Status Update</string>
  <string name="hintText">Please enter your 140-character status</string>
  <string name="buttonUpdate">Update</string>

  <string name="titlePrefs">Preferences</string>
  <string name="titleUsername">Username</string>
  <string name="titlePassword">Password</string>
  <string name="titleApiRoot">API Root</string>

  <string name="summaryUsername">Please enter your username</string>
  <string name="summaryPassword">Please enter your password</string>
  <string name="summaryApiRoot">URL of Root API for your service</string>

  <string name="titleServiceStop">Stop Service</string>
  <string name="titleServiceStart">Start Service</string>

  <string name="titleTimeline">Timeline</string> 
  <string name="msgAllDataPurged">All data has been purged</string>
  <string name="titlePurge">Purge Data</string>
</resources>

res/menu/menu.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android" >
  <item android:id=    "@+id/itemStatus"                  android:title= "@string/titleStatus"        android:icon= "@android:drawable/ic_menu_edit"></item>
  <item android:title= "@string/titleTimeline"            android:id=    "@+id/itemTimeline"          android:icon= "@android:drawable/ic_menu_sort_by_size"></item>
  <item android:id=    "@+id/itemPrefs"                   android:title= "@string/titlePrefs"         android:icon= "@android:drawable/ic_menu_preferences"></item>
  <item android:icon=  "@android:drawable/ic_menu_delete" android:title= "@string/titlePurge"         android:id=   "@+id/itemPurge"></item>
  <item android:id=    "@+id/itemToggleService"           android:title= "@string/titleServiceStart"  android:icon= "@android:drawable/ic_media_play"></item>
</menu>

res/layout/row.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_height="wrap_content" android:orientation="vertical"
  android:layout_width="fill_parent">

  <LinearLayout android:layout_height="wrap_content"
    android:layout_width="fill_parent">

    <TextView android:layout_height="wrap_content"
      android:layout_width="fill_parent" android:layout_weight="1"
      android:id="@+id/textUser" android:text=""
      android:textStyle="bold" />

    <TextView android:layout_height="wrap_content"
      android:layout_width="fill_parent" android:layout_weight="1"
      android:gravity="right" android:id="@+id/textCreatedAt"
      android:text="" />
  </LinearLayout>

  <TextView android:layout_height="wrap_content"
    android:layout_width="fill_parent" android:id="@+id/textText"
    android:text="" />

</LinearLayout>

res/layout/timeline.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical" android:layout_height="fill_parent"
  android:layout_width="fill_parent">

  <TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:layout_gravity="center"
    android:layout_margin="10dp" android:text="@string/titleTimeline"
    android:textColor="#fff" android:textSize="30sp" />

  <ListView android:layout_height="fill_parent"
    android:layout_width="fill_parent" android:id="@+id/listTimeline"
    android:background="#6000" />

</LinearLayout>

Source

YambaApplication

Substantially rework the main application object to orchestrate the timeline and updater service:

YambaApplication.xtend

package com.marakana.yambax

import com.marakana.utils.TwitterAPI
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.app.Application
import android.preference.PreferenceManager
import java.util.List
import winterwell.jtwitter.Twitter;
import winterwell.jtwitter.Twitter.Status;
import java.util.LinkedList

interface TimelineUpdateListener
{
  def void timelineUpdate()
}

class YambaApplication extends Application
{
  @Property TwitterAPI             twitter        = new TwitterAPI("student", "password", "http://yamba.marakana.com/api")
  @Property boolean                serviceRunning = false
  @Property List<Twitter.Status>   timeline       = new LinkedList<Status>
  @Property TimelineUpdateListener updateListener

  var prefsChanged = [ SharedPreferences prefs, String s |
                       twitter.changeAccount(prefs)  ]  as OnSharedPreferenceChangeListener

  override onCreate()
  {
    super.onCreate
    val prefs = PreferenceManager.getDefaultSharedPreferences(this)
    prefs.registerOnSharedPreferenceChangeListener = prefsChanged    
  }

  override onTerminate()
  {
    super.onTerminate
  }

  def updateTimeline(Iterable<Twitter.Status> newTweets)
  {
    newTweets.forEach[timeline.add(0, it)]
    updateListener?.timelineUpdate
  }

  def clearTimeline()
  {
    timeline.clear
    updateListener?.timelineUpdate
  }
}

The UpdaterService can be reworked to inter operate with the application object:

UpdaterService.xtend

package com.marakana.yambax

import android.content.Intent
import java.util.List
import winterwell.jtwitter.Twitter
import winterwell.jtwitter.Twitter.Status
import com.marakana.utils.TwitterAPI
import winterwell.jtwitter.TwitterException
import android.util.Log
import com.marakana.utils.BackgroundService
import android.os.Looper
import android.os.Handler

class UpdaterService extends BackgroundService
{
  var YambaApplication app
  var TwitterAPI       twitter
  var Iterable<Status> newTweets 

  override onCreate()
  {
    super.onCreate
    app        = getApplication as YambaApplication
    twitter    = app.twitter
  }

  override onStartCommand(Intent intent, int flags, int startId)
  {
    super.onStartCommand(intent, flags, startId)
    startBackgroundTask
    app.serviceRunning = true
    START_STICKY;
  }

  override onDestroy()
  {
    super.onDestroy
    stopBackgroundTask
    app.serviceRunning = false
  }

  override def void doBackgroundTask()
  {
    try
    {
      val List<Twitter.Status> timeline  = twitter.getFriendsTimeline
      newTweets = if (app.timeline.size == 0) timeline else timeline.filter [it.id > app.timeline.get(0).id]

      Log.e("YAMBA", "number of new tweets= " + newTweets.size)       
      val handler = new Handler(Looper.getMainLooper)      

      handler.post ([| app.updateTimeline(newTweets)])
    }
    catch (TwitterException e)
    {
      Log.e("YAMBA", "Failed to connect to twitter service", e); 
    }
  }
}

Revised Activities

Introduce a new class to encapsulate a generalised Activity for the application:

BaseActivity.xtend

package com.marakana.yambax

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast

interface Command
{
  def void doCommand()
}

class BaseActivity extends Activity
{
  protected var YambaApplication app

  val prefs         = [ | startActivity(new Intent(this, typeof(PrefsActivity))
                                              .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)) ] as Command

  val toggleService = [ | intent = new Intent(this, typeof(UpdaterService))
                          if (app.isServiceRunning)
                            stopService(intent)
                          else
                            startService(intent) ] as Command

  val purge         = [ | app.clearTimeline
                          Toast.makeText(this, R.string.msgAllDataPurged, Toast.LENGTH_LONG).show() ] as Command

  val timeline      = [ | startActivity(new Intent(this, typeof(TimelineActivity))
                                        .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
                                        .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)) ] as Command

  val status        = [ | startActivity(new Intent(this, typeof(StatusActivity))
                                        .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT))] as Command

  var commands      = #{ R.id.itemPrefs         -> prefs,
                         R.id.itemToggleService -> toggleService,
                         R.id.itemPurge         -> purge,
                         R.id.itemTimeline      -> timeline,
                         R.id.itemStatus        -> status  }

  override onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    app = getApplication as YambaApplication 
  }

  override onCreateOptionsMenu(Menu menu)
  { 
    getMenuInflater.inflate(R.menu.menu, menu)
    true
  }  

  override onOptionsItemSelected(MenuItem item)
  { 
    val command = commands.get(item.getItemId) as Command
    command.doCommand
    true
  } 

  override onMenuOpened(int featureId, Menu menu)
  { 
    val toggleItem = menu.findItem(R.id.itemToggleService)
    toggleItem.title = if (app.isServiceRunning) R.string.titleServiceStop         else R.string.titleServiceStart
    toggleItem.icon  = if (app.isServiceRunning) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play
    true
  }   
}

Note that it defines a set of 'commands', which represent actions that can be triggered form the application menu.

Two views can be based on this activity:

StatusActivity.xtend

package com.marakana.yambax

import android.os.Bundle
import android.widget.EditText
import android.widget.Button
import android.view.View.OnClickListener

class StatusActivity extends  BaseActivity 
{
  var EditText       editText
  var Button         updateButton
  var update       = [ new TwitterPoster(this).execute(editText.getText.toString)  ] as OnClickListener

  override onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_status)

    editText     = findViewById(R.id.editText) as EditText
    updateButton = findViewById(R.id.buttonUpdate) as Button

    updateButton.setOnClickListener = update   
  }
}

TimelineActivity

package com.marakana.yambax

import android.os.Bundle
import android.widget.ListView
import android.widget.ArrayAdapter
import winterwell.jtwitter.Twitter
import android.content.Context
import java.util.List
import winterwell.jtwitter.Twitter.Status
import android.view.View
import android.view.ViewGroup
import android.view.LayoutInflater
import android.widget.TextView
import java.text.DateFormat
import java.text.SimpleDateFormat

class TimelineActivity extends BaseActivity
{
  var TimelineAdapter timelineAdapter
  var timelineUpdate = [| timelineAdapter.notifyDataSetChanged ] as TimelineUpdateListener

  override onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.timeline)
    timelineAdapter    = new TimelineAdapter(this, R.layout.row, app.timeline)
    app.updateListener = timelineUpdate
  }

  override onStart()
  { 
    super.onStart    
    val listTimeline = findViewById(R.id.listTimeline) as ListView
    listTimeline.setAdapter(timelineAdapter);
  }
}

class StatusAdapter
{
  @Property View view;
  var DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");

  new (Context context, ViewGroup parent, Twitter.Status status)
  {
    val LayoutInflater inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    view = inflater.inflate(R.layout.row, parent, false)    
    view.id = status.id.intValue
    updateControls(status)
  }

  def updateControls(Twitter.Status status)
  {
    val user       = view.findViewById(R.id.textUser)      as TextView
    val createdAt  = view.findViewById(R.id.textCreatedAt) as TextView
    val text       = view.findViewById(R.id.textText)      as TextView 

    user.text      = status.getUser.name
    createdAt.text = df.format(status.createdAt)
    text.text      = status.text
  }
}

class TimelineAdapter extends ArrayAdapter<Twitter.Status>
{
  var List<Status> timeline
  var Context      context

  new(Context context, int textViewResourceId, List<Twitter.Status> timeline) 
  {
    super(context, textViewResourceId, timeline)
    this.timeline = timeline
    this.context  = context
  }

  override getView(int position, View convertView, ViewGroup parent)
  {
    val status = timeline.get(position)
    val item = new StatusAdapter (context, parent, status)
    return item.view
  }

  override getCount()
  {
    return timeline.size
  }

  override Twitter.Status getItem(int position)
  {
    return timeline.get(position);
  }

  override getItemId(int position)
  {
    val status = timeline.get(position);
    status.id;
  }

  override getPosition(Twitter.Status c)
  {
    return timeline.indexOf(c);
  }  
}

AndroidManifest

The manifest will need these new activities to be defined. This is the complete manifest for the application:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.marakana.yambax"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="16" />

    <uses-permission android:name="android.permission.INTERNET" /> 

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" 
        android:name="com.marakana.yambax.YambaApplication">

       <activity android:name="com.marakana.yambax.TimelineActivity" android:label="@string/titleTimeline">
         <intent-filter> 
           <action android:name="android.intent.action.MAIN" /> 
           <category android:name="android.intent.category.LAUNCHER" />
         </intent-filter>
      </activity>  

      <activity android:name="com.marakana.yambax.StatusActivity" android:label="@string/app_name" /> 
      <activity android:name="com.marakana.yambax.PrefsActivity"  android:label="@string/titlePrefs" />  
      <service  android:name="com.marakana.yambax.UpdaterService" />
    </application>

</manifest>

Build and test the application.


Source