Introduce Services into the Xtend YambaX application. Implement a background service to periodically update the twitter timeline
Introduce some new menu options, which we will use to trigger start/stop of a background service:
<string name="titleServiceStop">Stop Service</string>
<string name="titleServiceStart">Start Service</string>
<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:
<service android:name="com.marakana.yambax.UpdaterService" />
This is a skeleton service implementation:
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
:
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.
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.
We would like to display the users timeline on the console. First, introduce an accessor to retrieve the timeline from the api:
def getFriendsTimeline()
{
return twitter.getFriendsTimeline()
}
The UpdaterService can be adjusted to now periodically request the update in the background thread:
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.
We define a new abstract class to encapsulate a general purpose background service in com.marakana.utils
:
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:
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.
We can introduce a UI to render the timeline using these resources:
<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>
<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>
<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>
<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>
Substantially rework the main application object to orchestrate the timeline and updater service:
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:
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);
}
}
}
Introduce a new class to encapsulate a generalised Activity for the application:
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:
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
}
}
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);
}
}
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.