/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.codelab.rssexample;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Bundle;
import android.database.Cursor;
import android.content.ContentResolver;
import android.os.Handler;
import android.text.TextUtils;
import java.io.BufferedReader;
import java.net.URL;
import java.net.MalformedURLException;
import java.lang.StringBuilder;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.GregorianCalendar;
import java.text.SimpleDateFormat;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.text.ParseException;
public class RssService extends Service implements Runnable{
private Logger mLogger = Logger.getLogger(this.getPackageName());
public static final String REQUERY_KEY = "Requery_All"; // Sent to tell us force a requery.
public static final String RSS_URL = "RSS_URL"; // Sent to tell us to requery a specific item.
private NotificationManager mNM;
private Cursor mCur; // RSS content provider cursor.
private GregorianCalendar mLastCheckedTime; // Time we last checked our feeds.
private final String LAST_CHECKED_PREFERENCE = "last_checked";
static final int UPDATE_FREQUENCY_IN_MINUTES = 60;
private Handler mHandler; // Handler to trap our update reminders.
private final int NOTIFY_ID = 1; // Identifies our service icon in the icon tray.
@Override
protected void onCreate(){
// Display an icon to show that the service is running.
mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Intent clickIntent = new Intent(Intent.ACTION_MAIN);
clickIntent.setClassName(MyRssReader5.class.getName());
Notification note = new Notification(this, R.drawable.rss_icon, "RSS Service",
clickIntent, null);
mNM.notify(NOTIFY_ID, note);
mHandler = new Handler();
// Create the intent that will be launched if the user clicks the
// icon on the status bar. This will launch our RSS Reader app.
Intent intent = new Intent(MyRssReader.class);
// Get a cursor over the RSS items.
ContentResolver rslv = getContentResolver();
mCur = rslv.query(RssContentProvider.CONTENT_URI, null, null, null, null);
// Load last updated value.
// We store last updated value in preferences.
SharedPreferences pref = getSharedPreferences("", 0);
mLastCheckedTime = new GregorianCalendar();
mLastCheckedTime.setTimeInMillis(pref.getLong(LAST_CHECKED_PREFERENCE, 0));
//BEGIN_INCLUDE(5_1)
// Need to run ourselves on a new thread, because
// we will be making resource-intensive HTTP calls.
// Our run() method will check whether we need to requery
// our sources.
Thread thr = new Thread(null, this, "rss_service_thread");
thr.start();
//END_INCLUDE(5_1)
mLogger.info("RssService created");
}
//BEGIN_INCLUDE(5_3)
// A cheap way to pass a message to tell the service to requery.
@Override
protected void onStart(Intent intent, int startId){
super.onStart(startId, arguments);
Bundle arguments = intent.getExtras();
if(arguments != null) {
if(arguments.containsKey(REQUERY_KEY)) {
queryRssItems();
}
if(arguments.containsKey(RSS_URL)) {
// Typically called after adding a new RSS feed to the list.
queryItem(arguments.getString(RSS_URL));
}
}
}
//END_INCLUDE(5_3)
// When the service is destroyed, get rid of our persistent icon.
@Override
protected void onDestroy(){
mNM.cancel(NOTIFY_ID);
}
// Determines whether the next scheduled check time has passed.
// Loads this value from a stored preference. If it has (or if no
// previous value has been stored), it will requery all RSS feeds;
// otherwise, it will post a delayed reminder to check again after
// now - next_check_time milliseconds.
public void queryIfPeriodicRefreshRequired() {
GregorianCalendar nextCheckTime = new GregorianCalendar();
nextCheckTime = (GregorianCalendar) mLastCheckedTime.clone();
nextCheckTime.add(GregorianCalendar.MINUTE, UPDATE_FREQUENCY_IN_MINUTES);
mLogger.info("last checked time:" + mLastCheckedTime.toString() + " Next checked time: " + nextCheckTime.toString());
if(mLastCheckedTime.before(nextCheckTime)) {
queryRssItems();
} else {
// Post a message to query again when we get to the next check time.
long timeTillNextUpdate = mLastCheckedTime.getTimeInMillis() - GregorianCalendar.getInstance().getTimeInMillis();
mHandler.postDelayed(this, 1000 * 60 * UPDATE_FREQUENCY_IN_MINUTES);
}
}
// Query all feeds. If the new feed has a newer pubDate than the previous,
// then update it.
void queryRssItems(){
mLogger.info("Querying Rss feeds...");
// The cursor might have gone stale. Requery to be sure.
// We need to call next() after a requery to get to the
// first record.
mCur.requery();
while (mCur.next()){
// Get the URL for the feed from the cursor.
int urlColumnIndex = mCur.getColumnIndex(RssContentProvider.URL);
String url = mCur.getString(urlColumnIndex);
queryItem(url);
}
// Reset the global "last checked" time
mLastCheckedTime.setTimeInMillis(System.currentTimeMillis());
// Post a message to query again in [update_frequency] minutes
mHandler.postDelayed(this, 1000 * 60 * UPDATE_FREQUENCY_IN_MINUTES);
}
// Query an individual RSS feed. Returns true if successful, false otherwise.
private boolean queryItem(String url) {
try {
URL wrappedUrl = new URL(url);
String rssFeed = readRss(wrappedUrl);
mLogger.info("RSS Feed " + url + ":\n " + rssFeed);
if(TextUtils.isEmpty(rssFeed)) {
return false;
}
// Parse out the feed update date, and compare to the current version.
// If feed update time is newer, or zero (if never updated, for new
// items), then update the content, date, and hasBeenRead fields.
// lastUpdated = <rss><channel><pubDate>value</pubDate></channel></rss>.
// If that value doesn't exist, the current date is used.
GregorianCalendar feedPubDate = parseRssDocPubDate(rssFeed);
GregorianCalendar lastUpdated = new GregorianCalendar();
int lastUpdatedColumnIndex = mCur.getColumnIndex(RssContentProvider.LAST_UPDATED);
lastUpdated.setTimeInMillis(mCur.getLong(lastUpdatedColumnIndex));
if(lastUpdated.getTimeInMillis() == 0 ||
lastUpdated.before(feedPubDate) && !TextUtils.isEmpty(rssFeed)) {
// Get column indices.
int contentColumnIndex = mCur.getColumnIndex(RssContentProvider.CONTENT);
int updatedColumnIndex = mCur.getColumnIndex(RssContentProvider.HAS_BEEN_READ);
// Update values.
mCur.updateString(contentColumnIndex, rssFeed);
mCur.updateLong(lastUpdatedColumnIndex, feedPubDate.getTimeInMillis());
mCur.updateInt(updatedColumnIndex, 0);
mCur.commitUpdates();
}
} catch (MalformedURLException ex) {
mLogger.warning("Error in queryItem: Bad url");
return false;
}
return true;
}
// BEGIN_INCLUDE(5_2)
// Get the <pubDate> content from a feed and return a
// GregorianCalendar version of the date.
// If the element doesn't exist or otherwise can't be
// found, return a date of 0 to force a refresh.
private GregorianCalendar parseRssDocPubDate(String xml){
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(0);
String patt ="<[\\s]*pubDate[\\s]*>(.+?)</pubDate[\\s]*>";
Pattern p = Pattern.compile(patt);
Matcher m = p.matcher(xml);
try {
if(m.find()) {
mLogger.info("pubDate: " + m.group());
SimpleDateFormat pubDate = new SimpleDateFormat();
cal.setTime(pubDate.parse(m.group(1)));
}
} catch(ParseException ex) {
mLogger.warning("parseRssDocPubDate couldn't find a <pubDate> tag. Returning default value.");
}
return cal;
}
// Read the submitted RSS page.
String readRss(URL url){
String html = "<html><body><h2>No data</h2></body></html>";
try {
mLogger.info("URL is:" + url.toString());
BufferedReader inStream =
new BufferedReader(new InputStreamReader(url.openStream()),
1024);
String line;
StringBuilder rssFeed = new StringBuilder();
while ((line = inStream.readLine()) != null){
rssFeed.append(line);
}
html = rssFeed.toString();
} catch(IOException ex) {
mLogger.warning("Couldn't open an RSS stream");
}
return html;
}
//END_INCLUDE(5_2)
// Callback we send to ourself to requery all feeds.
public void run() {
queryIfPeriodicRefreshRequired();
}
// Required by Service. We won't implement it here, but need to
// include this basic code.
@Override
public IBinder onBind(Intent intent){
return mBinder;
}
// This is the object that receives RPC calls from clients.See
// RemoteService for a more complete example.
private final IBinder mBinder = new Binder() {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
return super.onTransact(code, data, reply, flags);
}
};
}