Статьи

Службы Android, часть 2

Вступление

 В первой части этой серии мы рассмотрели, как создать базовый сервис Android. Мы изучили класс Service и как правильно расширить этот класс, чтобы обеспечить функциональность, необходимую для нашего приложения. По пути мы также узнали, как запускать и останавливать службы из приложения Android. Во второй части этой серии мы еще немного расширим RSS-сервис и добавим функциональность. Я покажу вам еще несколько приемов, которые вы можете использовать в своих приложениях, сервисах и т. Д.

Продление службы

 Нам нужно еще больше расширить наш базовый сервис, чтобы обеспечить осмысленную реализацию. Прежде чем мы это сделаем, нам нужно изучить некоторые вспомогательные классы Эти классы будут инкапсулировать RSSFeed, а также абстрагировать фактическое чтение и анализ канала. Начнем с разбора классов. Имеет смысл использовать шаблон интерпретатора для разбора канала. Давайте посмотрим на интерфейс, который предоставляет эту функциональность.

 

package com.demo.service;

import java.util.List;

public interface RSSFeedParser {
	List<RSSMessage> parse();
}

 
Этот простой интерфейс реализует классический шаблон интерпретатора с помощью метода parse. Этот метод возвращает список объектов RSSMessage, которые были найдены в ленте. Давайте посмотрим на этот класс сейчас.

 

package com.demo.service;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import android.util.Log;

public class RSSMessage implements Comparable<RSSMessage> {

	static SimpleDateFormat DateFormatter 
		= new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
	
	static String TAG = "RSSMessage";
	
	private String title;
	private URL link;
	private String description;
	private Date date;
	
	@Override
	public int compareTo(RSSMessage another) {
		if( another == null ) return 1;
		
		// sort descending, most recent first
		return another.date.compareTo(this.date);
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getTitle() {
		return title;
	}

	public String getUrl() {
		return this.link.toString();
	}
	
	public void setLink(String link) {
		try {
			this.link = new URL(link);
		} catch (MalformedURLException e) {
			Log.e(TAG, e.getMessage());
			
			throw new RuntimeException(e);
		}
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public String getDescription() {
		return description;
	}

	public void setDate(String date) {
		// pad the date if necessary
		while(!date.endsWith("00")) {
			date += "0";
		}
		
		try {
			this.date = DateFormatter.parse(date.trim());
		} catch (ParseException e) {
			Log.e(TAG, e.getLocalizedMessage());
			
			throw new RuntimeException(e);
		}
	}

	public String getDate() {
		return DateFormatter.format(this.date);
	}

}

 Этот класс является простым приложением, которое инкапсулирует элементы RSSFeed.

Теперь давайте взглянем на базовую реализацию нашего интерфейса RSSFeedParser. В этом примере я использую базовый класс для предоставления базовой информации и функциональности анализатора каналов, но не выполняю фактический анализ. Это до расширенного класса, чтобы обеспечить эту функцию. Давайте посмотрим на базовый класс.

 

package com.demo.service;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

import android.util.Log;

public abstract class BaseFeedParser implements RSSFeedParser {
	static String TAG = "BaseFeedParser";
	
	// names of the XML tags
	static final String CHANNEL = "channel";
	static final String PUB_DATE = "pubDate";
	static final String DESCRIPTION = "description";
	static final String LINK = "link";
	static final String TITLE = "title";
	static final String ITEM = "item";
	
	final URL feedUrl;
	
	public BaseFeedParser(String feedUrl) {
		try {
			this.feedUrl = new URL(feedUrl);
		} catch (MalformedURLException e) {
			Log.e(TAG, e.getLocalizedMessage());
	
			throw new RuntimeException(e);
		}
	}
	
	protected InputStream getInputStream() {
		
		try {
			return feedUrl.openConnection().getInputStream();
		} catch (IOException e) {
			Log.e(TAG, e.getLocalizedMessage());
	
			throw new RuntimeException(e);
		}
	}

}

 

Класс начинается с реализации интерфейса RSSFeedParser, который мы рассматривали ранее. Класс является абстрактным, поскольку он не реализует метод разбора. BaseFeedParser содержит определения имен полей RSS, которые нас интересуют. Конструктор принимает параметр String, который указывает URL-адрес канала. Класс также предоставляет простую функцию для инкапсуляции чтения входного потока из соединения с сокетом по URL, указанному в базовом конструкторе.

Давайте теперь посмотрим на конкретную реализацию этого класса.

package com.demo.service;

import java.util.ArrayList;
import java.util.List;

import org.xmlpull.v1.XmlPullParser;

import android.util.Xml;

public class XmlPullFeedParser extends BaseFeedParser {
    public XmlPullFeedParser(String feedUrl) {
        super(feedUrl);
    }
    public List<RSSMessage> parse() {
        List<RSSMessage> messages = null;
        XmlPullParser parser = Xml.newPullParser();
        try {
            // auto-detect the encoding from the stream
            parser.setInput(this.getInputStream(), null);
            int eventType = parser.getEventType();
            RSSMessage currentMessage = null;
            boolean done = false;
            while (eventType != XmlPullParser.END_DOCUMENT && !done){
                String name = null;
                switch (eventType){
                    case XmlPullParser.START_DOCUMENT:
                        messages = new ArrayList<RSSMessage>();
                        break;
                    case XmlPullParser.START_TAG:
                        name = parser.getName();
                        if (name.equalsIgnoreCase(ITEM)){
                            currentMessage = new RSSMessage();
                        } else if (currentMessage != null){
                            if (name.equalsIgnoreCase(LINK)){
                                currentMessage.setLink(parser.nextText());
                            } else if (name.equalsIgnoreCase(DESCRIPTION)){
                                currentMessage.setDescription(parser.nextText());
                            } else if (name.equalsIgnoreCase(PUB_DATE)){
                                currentMessage.setDate(parser.nextText());
                            } else if (name.equalsIgnoreCase(TITLE)){
                                currentMessage.setTitle(parser.nextText());
                            }    
                        }
                        break;
                    case XmlPullParser.END_TAG:
                        name = parser.getName();
                        if (name.equalsIgnoreCase(ITEM) && currentMessage != null){
                            messages.add(currentMessage);
                        } else if (name.equalsIgnoreCase(CHANNEL)){
                            done = true;
                        }
                        break;
                }
                eventType = parser.next();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return messages;
    }
}

Это важный класс, поэтому давайте внимательно рассмотрим его. Этот класс обеспечивает функциональность синтаксического анализа с использованием XmlPullParser. Парсеры Pull отличаются от DOM и парсеров push тем, что пользователь api контролирует разбор потока xml, отсюда и слово pull. В push-парсерах разбор остается вне контроля пользователей API. Push-парсеры используют обратные вызовы. Когда требуется любой тип состояния, работать с push-парсерами может быть очень сложно. 

Парсеры DOM страдают от раздувания памяти. С меньшими наборами XML это не такая проблема, но с ростом размера XML анализаторы DOM становятся все менее и менее эффективными, что делает их не очень масштабируемыми с точки зрения роста вашего XML. Синтаксические анализаторы — это удачная среда, поскольку они позволяют вам контролировать синтаксический анализ, устраняя тем самым любой вид сложного управления состоянием, поскольку состояние всегда известно, и они не страдают от переполнения памяти DOM-анализаторами.

В этом примере вы можете видеть, что этот класс использует значения констант базового класса, чтобы определить, на каком узле он работает. Здесь вы можете увидеть, что при обнаружении начала документа функция создает новый список RSSMessages. Когда начальный тег найден, имя сравнивается с именем узла Item. Если узел Item найден, создается новое сообщение RSSMessage. Это текущее RSSMessage. Если найдено любое другое допустимое имя узла, оно считается полем RSSMessage и добавляется к текущему активному сообщению. При обнаружении конечного тега имя сравнивается с именем узла Item и именем узла Channel. Если это узел Item, то если RSSMessage был запущен ранее, он добавляется в список сообщений.Если имя конечного тега является именем узла канала, функция сигнализирует, что синтаксический анализ завершен, и основной цикл завершается.

Теперь, когда у нас есть функциональный парсер, пришло время пересмотреть сервис, который мы создали в первой статье. Это был простой сервис, который создавал таймер обновления, который запускал функцию refreshFeed. Вот этот код для повышения квалификации.

 

package com.demo.service;

import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.widget.Toast;

public class RSSService extends Service {

	private Timer updateTimer;
	private Date lastRead = new Date(0L);

	@Override
	public IBinder onBind(Intent arg0) {
		return null;
	}
	
	@Override
	public void onCreate() {
		
		updateTimer = new Timer("RSSServiceUpdateTimer");
	}
	
	@Override
	public void onStart(Intent intent, int startId) {
		
		Toast.makeText(this, "RSSService Started", Toast.LENGTH_LONG).show();
		
		// TODO: Read from user preferences
		int period = 10; 
		
		// cancel the current timer
		updateTimer.cancel();
		
		// create a new timer
		updateTimer = new Timer("RSSServiceUpdateTimer");
		updateTimer.scheduleAtFixedRate(
				new TimerTask() {

					@Override
					public void run() {
						RSSService.this.refreshFeed();
					}
					
				}, 0, period*60*1000);
	}
	
	protected void refreshFeed() {
	}
	
	private void announceNewFeed(RSSMessage feed) {
		
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		
		Toast.makeText(this, "RSSService Stopped", Toast.LENGTH_LONG).show();		
	}

}

Интересующая нас функция — refreshFeed. Давайте продолжим и уточним это сейчас, когда у нас есть анализатор каналов, и мы действительно можем сделать что-то значимое с помощью канала RSS. Вот этот код.

 

	protected void refreshFeed() {
		// perform http lookup for new feeds
		RSSFeedParser parser 
			= new XmlPullFeedParser("http://rss.news.yahoo.com/rss/topstories");

		Date maxMessageDate = new Date(0L);		

		List<RSSMessage> messages = parser.parse();
		// anything older than lastRead we announce
		for(int i=0; i<messages.size(); i++) {
			String dateString = messages.get(i).getDate();
			
			Date messageDate = new Date(dateString);
			if(messageDate.compareTo(lastRead) > 0) {
				if( maxMessageDate.compareTo(messageDate) < 0 ) {
					maxMessageDate = messageDate;
				}
				announceNewFeed(messages.get(i));
			}
			
		}

		lastRead = maxMessageDate;
	}

В этой функции вы можете увидеть знакомый XmlPullFeedParser, а также наш оригинальный интерфейс RSSFeedParser, который реализует шаблон интерпретатора. В этом случае я использую жестко запрограммированный URL, но в реальном приложении вы, скорее всего, предоставите его из пользовательского ввода или файла настроек / предпочтений. Я использую фид самых популярных историй от Yahoo, так как этот фид меняется довольно часто, и в нем много разных типов медиа, которые вы можете использовать для создания богатого пользовательского опыта.

Первое, что делает функция, это разбирает ленту в список классов RSSMessage. Затем он перебирает каждое сообщение RSSMessage в списке. В этом случае мы хотим объявить только новые каналы, поэтому мы сравниваем с датой последнего обновления канала. Если дата больше, чем дата последнего обновления, мы вызываем announceNewFeed с этим сообщением. В этом примере announceNewFeed ничего не делает, но в следующей статье мы будем использовать это для подключения к поставщику данных, который мы затем можем использовать в наших приложениях. После обработки всех сообщений в поле lastRead присваивается наибольшая дата, прочитанная из канала.

Вывод

 В этой статье мы расширили наш сервисный пример из первой статьи. Мы создали pojo для представления сообщения RSS-ленты и создали XML-парсер для анализа канала с использованием шаблона интерпретатора. Наконец, мы подключили все это к нашей функции refreshFeed, которая вызывается из таймера обновления, который мы рассмотрели в первой статье. Мы предоставили ловушку для объявления новых каналов из метода refreshFeed и рассмотрим это в следующей статье. Расширив базовую структуру, мы смогли создать богатый сервис данных для RSS-каналов. В этой статье нет ограничений на то, что вы можете делать со службами Android.