Home | History | Annotate | Download | only in rssexample
      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