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
onCreate()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
onStart(Intent intent, int startId)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
onDestroy()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.
queryIfPeriodicRefreshRequired()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.
queryRssItems()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.
queryItem(String url)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.
parseRssDocPubDate(String xml)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.
readRss(URL url)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.
run()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
onBind(Intent intent)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