1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.codelab.rssexample; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.Service; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.os.Binder; 25 import android.os.IBinder; 26 import android.os.Parcel; 27 import android.os.Bundle; 28 import android.database.Cursor; 29 import android.content.ContentResolver; 30 import android.os.Handler; 31 import android.text.TextUtils; 32 import java.io.BufferedReader; 33 import java.net.URL; 34 import java.net.MalformedURLException; 35 import java.lang.StringBuilder; 36 import java.io.InputStreamReader; 37 import java.io.IOException; 38 import java.util.GregorianCalendar; 39 import java.text.SimpleDateFormat; 40 import java.util.logging.Logger; 41 import java.util.regex.Pattern; 42 import java.util.regex.Matcher; 43 import java.text.ParseException; 44 45 public class RssService extends Service implements Runnable{ 46 private Logger mLogger = Logger.getLogger(this.getPackageName()); 47 public static final String REQUERY_KEY = "Requery_All"; // Sent to tell us force a requery. 48 public static final String RSS_URL = "RSS_URL"; // Sent to tell us to requery a specific item. 49 private NotificationManager mNM; 50 private Cursor mCur; // RSS content provider cursor. 51 private GregorianCalendar mLastCheckedTime; // Time we last checked our feeds. 52 private final String LAST_CHECKED_PREFERENCE = "last_checked"; 53 static final int UPDATE_FREQUENCY_IN_MINUTES = 60; 54 private Handler mHandler; // Handler to trap our update reminders. 55 private final int NOTIFY_ID = 1; // Identifies our service icon in the icon tray. 56 57 @Override 58 protected void onCreate(){ 59 // Display an icon to show that the service is running. 60 mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 61 Intent clickIntent = new Intent(Intent.ACTION_MAIN); 62 clickIntent.setClassName(MyRssReader5.class.getName()); 63 Notification note = new Notification(this, R.drawable.rss_icon, "RSS Service", 64 clickIntent, null); 65 mNM.notify(NOTIFY_ID, note); 66 mHandler = new Handler(); 67 68 // Create the intent that will be launched if the user clicks the 69 // icon on the status bar. This will launch our RSS Reader app. 70 Intent intent = new Intent(MyRssReader.class); 71 72 // Get a cursor over the RSS items. 73 ContentResolver rslv = getContentResolver(); 74 mCur = rslv.query(RssContentProvider.CONTENT_URI, null, null, null, null); 75 76 // Load last updated value. 77 // We store last updated value in preferences. 78 SharedPreferences pref = getSharedPreferences("", 0); 79 mLastCheckedTime = new GregorianCalendar(); 80 mLastCheckedTime.setTimeInMillis(pref.getLong(LAST_CHECKED_PREFERENCE, 0)); 81 82 //BEGIN_INCLUDE(5_1) 83 // Need to run ourselves on a new thread, because 84 // we will be making resource-intensive HTTP calls. 85 // Our run() method will check whether we need to requery 86 // our sources. 87 Thread thr = new Thread(null, this, "rss_service_thread"); 88 thr.start(); 89 //END_INCLUDE(5_1) 90 mLogger.info("RssService created"); 91 } 92 93 //BEGIN_INCLUDE(5_3) 94 // A cheap way to pass a message to tell the service to requery. 95 @Override 96 protected void onStart(Intent intent, int startId){ 97 super.onStart(startId, arguments); 98 Bundle arguments = intent.getExtras(); 99 if(arguments != null) { 100 if(arguments.containsKey(REQUERY_KEY)) { 101 queryRssItems(); 102 } 103 if(arguments.containsKey(RSS_URL)) { 104 // Typically called after adding a new RSS feed to the list. 105 queryItem(arguments.getString(RSS_URL)); 106 } 107 } 108 } 109 //END_INCLUDE(5_3) 110 111 // When the service is destroyed, get rid of our persistent icon. 112 @Override 113 protected void onDestroy(){ 114 mNM.cancel(NOTIFY_ID); 115 } 116 117 // Determines whether the next scheduled check time has passed. 118 // Loads this value from a stored preference. If it has (or if no 119 // previous value has been stored), it will requery all RSS feeds; 120 // otherwise, it will post a delayed reminder to check again after 121 // now - next_check_time milliseconds. 122 public void queryIfPeriodicRefreshRequired() { 123 GregorianCalendar nextCheckTime = new GregorianCalendar(); 124 nextCheckTime = (GregorianCalendar) mLastCheckedTime.clone(); 125 nextCheckTime.add(GregorianCalendar.MINUTE, UPDATE_FREQUENCY_IN_MINUTES); 126 mLogger.info("last checked time:" + mLastCheckedTime.toString() + " Next checked time: " + nextCheckTime.toString()); 127 128 if(mLastCheckedTime.before(nextCheckTime)) { 129 queryRssItems(); 130 } else { 131 // Post a message to query again when we get to the next check time. 132 long timeTillNextUpdate = mLastCheckedTime.getTimeInMillis() - GregorianCalendar.getInstance().getTimeInMillis(); 133 mHandler.postDelayed(this, 1000 * 60 * UPDATE_FREQUENCY_IN_MINUTES); 134 } 135 136 } 137 138 // Query all feeds. If the new feed has a newer pubDate than the previous, 139 // then update it. 140 void queryRssItems(){ 141 mLogger.info("Querying Rss feeds..."); 142 143 // The cursor might have gone stale. Requery to be sure. 144 // We need to call next() after a requery to get to the 145 // first record. 146 mCur.requery(); 147 while (mCur.next()){ 148 // Get the URL for the feed from the cursor. 149 int urlColumnIndex = mCur.getColumnIndex(RssContentProvider.URL); 150 String url = mCur.getString(urlColumnIndex); 151 queryItem(url); 152 } 153 // Reset the global "last checked" time 154 mLastCheckedTime.setTimeInMillis(System.currentTimeMillis()); 155 156 // Post a message to query again in [update_frequency] minutes 157 mHandler.postDelayed(this, 1000 * 60 * UPDATE_FREQUENCY_IN_MINUTES); 158 } 159 160 161 // Query an individual RSS feed. Returns true if successful, false otherwise. 162 private boolean queryItem(String url) { 163 try { 164 URL wrappedUrl = new URL(url); 165 String rssFeed = readRss(wrappedUrl); 166 mLogger.info("RSS Feed " + url + ":\n " + rssFeed); 167 if(TextUtils.isEmpty(rssFeed)) { 168 return false; 169 } 170 171 // Parse out the feed update date, and compare to the current version. 172 // If feed update time is newer, or zero (if never updated, for new 173 // items), then update the content, date, and hasBeenRead fields. 174 // lastUpdated = <rss><channel><pubDate>value</pubDate></channel></rss>. 175 // If that value doesn't exist, the current date is used. 176 GregorianCalendar feedPubDate = parseRssDocPubDate(rssFeed); 177 GregorianCalendar lastUpdated = new GregorianCalendar(); 178 int lastUpdatedColumnIndex = mCur.getColumnIndex(RssContentProvider.LAST_UPDATED); 179 lastUpdated.setTimeInMillis(mCur.getLong(lastUpdatedColumnIndex)); 180 if(lastUpdated.getTimeInMillis() == 0 || 181 lastUpdated.before(feedPubDate) && !TextUtils.isEmpty(rssFeed)) { 182 // Get column indices. 183 int contentColumnIndex = mCur.getColumnIndex(RssContentProvider.CONTENT); 184 int updatedColumnIndex = mCur.getColumnIndex(RssContentProvider.HAS_BEEN_READ); 185 186 // Update values. 187 mCur.updateString(contentColumnIndex, rssFeed); 188 mCur.updateLong(lastUpdatedColumnIndex, feedPubDate.getTimeInMillis()); 189 mCur.updateInt(updatedColumnIndex, 0); 190 mCur.commitUpdates(); 191 } 192 } catch (MalformedURLException ex) { 193 mLogger.warning("Error in queryItem: Bad url"); 194 return false; 195 } 196 return true; 197 } 198 199 // BEGIN_INCLUDE(5_2) 200 // Get the <pubDate> content from a feed and return a 201 // GregorianCalendar version of the date. 202 // If the element doesn't exist or otherwise can't be 203 // found, return a date of 0 to force a refresh. 204 private GregorianCalendar parseRssDocPubDate(String xml){ 205 GregorianCalendar cal = new GregorianCalendar(); 206 cal.setTimeInMillis(0); 207 String patt ="<[\\s]*pubDate[\\s]*>(.+?)</pubDate[\\s]*>"; 208 Pattern p = Pattern.compile(patt); 209 Matcher m = p.matcher(xml); 210 try { 211 if(m.find()) { 212 mLogger.info("pubDate: " + m.group()); 213 SimpleDateFormat pubDate = new SimpleDateFormat(); 214 cal.setTime(pubDate.parse(m.group(1))); 215 } 216 } catch(ParseException ex) { 217 mLogger.warning("parseRssDocPubDate couldn't find a <pubDate> tag. Returning default value."); 218 } 219 return cal; 220 } 221 222 // Read the submitted RSS page. 223 String readRss(URL url){ 224 String html = "<html><body><h2>No data</h2></body></html>"; 225 try { 226 mLogger.info("URL is:" + url.toString()); 227 BufferedReader inStream = 228 new BufferedReader(new InputStreamReader(url.openStream()), 229 1024); 230 String line; 231 StringBuilder rssFeed = new StringBuilder(); 232 while ((line = inStream.readLine()) != null){ 233 rssFeed.append(line); 234 } 235 html = rssFeed.toString(); 236 } catch(IOException ex) { 237 mLogger.warning("Couldn't open an RSS stream"); 238 } 239 return html; 240 } 241 //END_INCLUDE(5_2) 242 243 // Callback we send to ourself to requery all feeds. 244 public void run() { 245 queryIfPeriodicRefreshRequired(); 246 } 247 248 // Required by Service. We won't implement it here, but need to 249 // include this basic code. 250 @Override 251 public IBinder onBind(Intent intent){ 252 return mBinder; 253 } 254 255 // This is the object that receives RPC calls from clients.See 256 // RemoteService for a more complete example. 257 private final IBinder mBinder = new Binder() { 258 @Override 259 protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) { 260 return super.onTransact(code, data, reply, flags); 261 } 262 }; 263 } 264