My first Android app (desktop widget)
Having spent quite a bit of time away from Eclipse lately I've been itching to play with some code again. During a short meeting this morning an idea hit me for a really simple Android desktop widget: a twitter search widget that you can drop on your desktop and configure with a hash-tag search.
Here's how it looks in the Android emulator:
I know, not exactly eye-candy, but that wasn't really the point :)
The important pieces of the puzzle are:
- A BroadcastReceiver that receives events telling the widget to update. Here I extended AppWidgetProvider to override onUpdate only.
- A Service that does the Twitter API request asynchronously - we don't want to use the UI thread and fail the responsiveness criteria
- The "widget info" descriptor xml
- Android manifest entries that register the Service and the broadcast receiver
- SAX ContentHandler implementation to parse the Tweet text from the atom response to the Twitter API request
Here's the code, currently wired to display a search for "#uml"
The BroadcastReceiver
public class NewsTicker extends AppWidgetProvider {
@Override
public void onUpdate(Context aContext,
AppWidgetManager aAppWidgetManager,
int[] aAppWidgetIds) {
super.onUpdate(aContext, aAppWidgetManager, aAppWidgetIds);
aContext.startService(
new Intent(aContext, NewsTickerDataService.class));
}
}
The Service
public class NewsTickerDataService extends Service {
@Override
public void onStart(Intent aIntent, int aStartId) {
super.onStart(aIntent, aStartId);
RemoteViews _views = buildUpdatedViews(this);
ComponentName _widget =
new ComponentName(this, NewsTicker.class);
AppWidgetManager _manager =
AppWidgetManager.getInstance(this);
_manager.updateAppWidget(_widget, _views);
}
@Override
public IBinder onBind(Intent aParamIntent) {
// not supporting binding
return null;
}
private RemoteViews buildUpdatedViews(Context aContext) {
List<Story> _stories = getStories();
RemoteViews _result = new RemoteViews(
aContext.getPackageName(),
R.layout.newsticker_widget
);
if (_stories.isEmpty()) {
_result.setTextViewText(R.id.title,
"Sadly there's nothing to read today.");
} else {
_result.setTextViewText(
R.id.title, _stories.get(0).getTitle());
}
return _result;
}
private List<Story> getStories() {
try {
URL _url = new URL("http://search.twitter.com" +
"/search.atom?q=%23uml&" +
"result_type=mixed&count=5"
);
InputStream _in = _url.openStream();
return parse(new InputSource(_in));
} catch (Exception anExc) {
Log.e("NewsTicker", anExc.getMessage(), anExc);
return new ArrayList<Story>();
}
}
private List<Story> parse(InputSource aSource)
throws Exception {
SAXParserFactory _f = SAXParserFactory.newInstance();
SAXParser _p = _f.newSAXParser();
XMLReader _r = _p.getXMLReader();
AbstractParser _h = new AtomParser();
_r.setContentHandler(_h);
_r.parse(aSource);
return _h.getStories();
}
}
The SAX ContentHandler
public abstract class AbstractParser extends DefaultHandler {
static interface Handler {
Handler startElement(String aName);
Handler characters(char[] aChars, int aStart, int aLength);
Handler endElement(String aName);
}
public static AbstractParser newAtomParser() {
return new AbstractParser() {
protected String getStoryElementName() {
return "entry";
}
};
}
private Handler handler;
private List<Story> stories;
public AbstractParser() {
stories = new ArrayList<Story>();
handler = newDefaultHandler();
}
protected abstract String getStoryElementName();
@Override
public void startElement(
String aUri, String aLocalName,
String aQName, Attributes aAttributes)
throws SAXException {
handler = handler.startElement(aLocalName);
}
@Override
public void characters(char[] aCh, int aStart, int aLength)
throws SAXException {
handler = handler.characters(aCh, aStart, aLength);
}
@Override
public void endElement(String aUri, String aLocalName, String aQName)
throws SAXException {
handler = handler.endElement(aLocalName);
}
public List<Story> getStories() {
return stories;
}
private Handler newDefaultHandler() {
return new Handler() {
@Override
public Handler startElement(String aName) {
if (aName.equals(getStoryElementName())) {
return newItemHandler(this);
}
return this;
}
@Override
public Handler characters(
char[] aChars, int aStart, int aLength) {
return this;
}
@Override
public Handler endElement(String aName) {
return this;
}
};
}
private Handler newItemHandler(final Handler aParent) {
stories.add(new StoryImpl());
return new Handler() {
@Override
public Handler startElement(String aName) {
if ("title".equals(aName)) {
return newTitleHandler(this);
}
return this;
}
@Override
public Handler characters(
char[] aChars, int aStart, int aLength) {
return this;
}
@Override
public Handler endElement(String aName) {
if (getStoryElementName().equals(aName)) {
return aParent;
}
return this;
}
};
}
private Handler newTitleHandler(final Handler aParent) {
final StringBuilder _title = new StringBuilder();
return new Handler() {
@Override
public Handler startElement(String aName) {
return this;
}
@Override
public Handler characters(
char[] aChars, int aStart, int aLength) {
_title.append(aChars, aStart, aLength);
return this;
}
@Override
public Handler endElement(String aName) {
if ("title".equals(aName)) {
StoryImpl _s = (StoryImpl)
stories.get(stories.size()-1);
_s.setTitle(_title.toString());
return aParent;
}
return this;
}
};
}
}
The Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.knowledgeview.android.ticker"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="7" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:icon="@drawable/icon"
android:label="@string/app_name">
<receiver android:name=".NewsTicker">
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/newsticker_widget_info"/>
</receiver>
<service android:name=".NewsTickerDataService"/>
</application>
</manifest>
The Widget descriptor
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/newsticker_widget">
</appwidget-provider>
The layout descriptor
<LinearLayout
android:id="@+id/linearLayout1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:textAppearance="?android:attr/textAppearanceSmall"
android:id="@+id/title"
android:text="TextView"
android:textSize="10sp"
android:textColor="#fff"
android:layout_height="60dp"
android:layout_width="fill_parent"
android:background="#c333"
android:padding="3dp"
android:layout_margin="5dp">
</TextView>
</LinearLayout>
Complete project and source-code
The complete project and source code is available from github.
blog comments powered by Disqus