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:

Screenshot

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