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
17package com.example.codelab.rssexample;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.Service;
22import android.content.Intent;
23import android.content.SharedPreferences;
24import android.os.Binder;
25import android.os.IBinder;
26import android.os.Parcel;
27import android.os.Bundle;
28import android.database.Cursor;
29import android.content.ContentResolver;
30import android.os.Handler;
31import android.text.TextUtils;
32import java.io.BufferedReader;
33import java.net.URL;
34import java.net.MalformedURLException;
35import java.lang.StringBuilder;
36import java.io.InputStreamReader;
37import java.io.IOException;
38import java.util.GregorianCalendar;
39import java.text.SimpleDateFormat;
40import java.util.logging.Logger;
41import java.util.regex.Pattern;
42import java.util.regex.Matcher;
43import java.text.ParseException;
44
45public 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