تحليل بيانات XML

اللغة الترميزية القابلة للامتداد (XML) هي مجموعة من القواعد لتشفير المستندات في ويمكن للآلة قراءته. XML هو تنسيق شائع لمشاركة البيانات على الإنترنت.

المواقع الإلكترونية التي تعدِّل محتواها بشكل متكرر، مثل المواقع الإخبارية أو المدونات وتوفر خلاصة XML حتى تتمكن البرامج الخارجية من مواكبة المحتوى التغييرات. يعد تحميل بيانات XML وتحليلها مهمة شائعة بالنسبة إلى الأجهزة المتصلة بالشبكة التطبيقات. يوضح هذا الموضوع كيفية تحليل مستندات XML واستخدام بياناتها.

لمزيد من المعلومات حول إنشاء محتوى مستند إلى الويب في تطبيق Android، يُرجى الاطّلاع على المحتوى المستنِد إلى الويب:

اختيار محلل لغوي

نقترح استخدام السمة XmlPullParser، وهي نظام فعّال يمكن المحافظة عليها لتحليل XML على Android. يحتوي Android على اثنين واستخدامات هذه الواجهة:

لا بأس في أي من الخيارين. تشير رسالة الأشكال البيانية المثال في هذا القسم يستخدم ExpatPullParser Xml.newPullParser()

تحليل الخلاصة

الخطوة الأولى في تحليل الخلاصة هي تحديد الحقول التي تهمّك. يستخرج المحلل اللغوي بيانات هذه الحقول ويتجاهل الباقي.

يمكنك الاطّلاع على المقتطف التالي من خلاصة محللة في نموذج التطبيق. على كل تظهر المشاركة على StackOverflow.com في الخلاصة كعلامة entry تحتوي على العديد من العلامات المدمَجة:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ...">
<title type="text">newest questions tagged android - Stack Overflow</title>
...
    <entry>
    ...
    </entry>
    <entry>
        <id>http://stackoverflow.com/q/9439999</id>
        <re:rank scheme="http://stackoverflow.com">0</re:rank>
        <title type="text">Where is my data file?</title>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
        <author>
            <name>cliff2310</name>
            <uri>http://stackoverflow.com/users/1128925</uri>
        </author>
        <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" />
        <published>2012-02-25T00:30:54Z</published>
        <updated>2012-02-25T00:30:54Z</updated>
        <summary type="html">
            <p>I have an Application that requires a data file...</p>

        </summary>
    </entry>
    <entry>
    ...
    </entry>
...
</feed>

نموذج التطبيق يستخرج البيانات الخاصة بالعلامة entry وعلاماتها المدمجة. title وlink وsummary

إنشاء مثيل للمحلل اللغوي

الخطوة التالية في تحليل الخلاصة هي إنشاء مثيل للمحلل وبدء عملية التحليل. هذا المقتطف تؤدي هذه السياسة إلى إعداد محلّل لغوي كي لا يعالج مساحات الاسم ويستخدم InputStream المُقدّم كإدخاله. فهي تبدأ عملية التحليل باستدعاء nextTag() ويستدعي readFeed(): تستخرج البيانات التي يستخدمها التطبيق وتعالجها. مهتم بـ:

Kotlin

// We don't use namespaces.
private val ns: String? = null

class StackOverflowXmlParser {

    @Throws(XmlPullParserException::class, IOException::class)
    fun parse(inputStream: InputStream): List<*> {
        inputStream.use { inputStream ->
            val parser: XmlPullParser = Xml.newPullParser()
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
            parser.setInput(inputStream, null)
            parser.nextTag()
            return readFeed(parser)
        }
    }
 ...
}

Java

public class StackOverflowXmlParser {
    // We don't use namespaces.
    private static final String ns = null;

    public List parse(InputStream in) throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
            parser.setInput(in, null);
            parser.nextTag();
            return readFeed(parser);
        } finally {
            in.close();
        }
    }
 ...
}

قراءة الخلاصة

تؤدي الطريقة readFeed() العمل الفعلي لمعالجة الخلاصة. يبحث عن عناصر تحمل علامة "إدخال" كنقطة انطلاق للتكرار جارٍ معالجة الخلاصة. وإذا لم تكن العلامة هي علامة entry، ستتخطّاها. بمجرد أن تكتمل معالجة الخلاصة بشكل متكرر، ويعرض readFeed() List التي تحتوي على الإدخالات (بما في ذلك أعضاء البيانات المتداخلة) التي المستخرجة من الخلاصة. يتم بعد ذلك إرجاع قيمة "List" هذه من خلال المحلل اللغوي.

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun readFeed(parser: XmlPullParser): List<Entry> {
    val entries = mutableListOf<Entry>()

    parser.require(XmlPullParser.START_TAG, ns, "feed")
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        // Starts by looking for the entry tag.
        if (parser.name == "entry") {
            entries.add(readEntry(parser))
        } else {
            skip(parser)
        }
    }
    return entries
}

Java

private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
    List entries = new ArrayList();

    parser.require(XmlPullParser.START_TAG, ns, "feed");
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        // Starts by looking for the entry tag.
        if (name.equals("entry")) {
            entries.add(readEntry(parser));
        } else {
            skip(parser);
        }
    }
    return entries;
}

تحليل XML

في ما يلي خطوات تحليل خلاصة XML:

  1. كما هو موضح في قسم تحليل الخلاصة، حدِّد العلامات التي تريد تضمينها في تطبيقك. هذا النمط يستخلص المثال بيانات العلامة entry وعلاماتها المتداخلة: title وlink وsummary
  2. أنشئ الطرق التالية:

    • "قراءة" لكل علامة تريد تضمينها، مثل "readEntry()" وreadTitle()" يقرأ المحلل اللغوي العلامات من مصدر البيانات المُدخَل. عندما تواجه علامة مسماة، في هذا المثال، entry، title، link أو summary، فإنها تستدعي الطريقة المناسبة لهذه العلامة. وبخلاف ذلك، يتخطى العلامة العلامة.
    • طرق استخراج البيانات لكل نوع مختلف من العلامات وتعزيز محلل لغوي إلى العلامة التالية. في هذا المثال، تكون الطرق ذات الصلة كما يلي:
      • بالنسبة إلى العلامتين title وsummary، يستدعي المحلل اللغوي readText() تستخرج هذه الطريقة بيانات هذه العلامات عن طريق استدعاء parser.getText()
      • بالنسبة إلى العلامة link، يستخرج المحلل اللغوي بيانات الروابط أولاً ما إذا كان الرابط هو نوع المحتوى التي يهتم بها. بعد ذلك، يستخدم parser.getAttributeValue() لإجراء ما يلي: لاستخراج قيمة الرابط
      • بالنسبة إلى العلامة entry، يستدعي المحلل اللغوي readEntry(). تُحلِّل هذه الطريقة العلامات المُدمَجة للإدخال وتعرض Entry يحتوي على أعضاء البيانات title وlink و summary
    • طريقة skip() مساعِدة متكررة. لمزيد من المناقشة حول هذا الموضوع، يُرجى الاطّلاع على تخطّي العلامات التي لا تهمّك.

يعرض هذا المقتطف طريقة تحليل المحلل للإدخالات والعناوين والروابط والملخصات.

Kotlin

data class Entry(val title: String?, val summary: String?, val link: String?)

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
@Throws(XmlPullParserException::class, IOException::class)
private fun readEntry(parser: XmlPullParser): Entry {
    parser.require(XmlPullParser.START_TAG, ns, "entry")
    var title: String? = null
    var summary: String? = null
    var link: String? = null
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        when (parser.name) {
            "title" -> title = readTitle(parser)
            "summary" -> summary = readSummary(parser)
            "link" -> link = readLink(parser)
            else -> skip(parser)
        }
    }
    return Entry(title, summary, link)
}

// Processes title tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readTitle(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "title")
    val title = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "title")
    return title
}

// Processes link tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readLink(parser: XmlPullParser): String {
    var link = ""
    parser.require(XmlPullParser.START_TAG, ns, "link")
    val tag = parser.name
    val relType = parser.getAttributeValue(null, "rel")
    if (tag == "link") {
        if (relType == "alternate") {
            link = parser.getAttributeValue(null, "href")
            parser.nextTag()
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link")
    return link
}

// Processes summary tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readSummary(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "summary")
    val summary = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "summary")
    return summary
}

// For the tags title and summary, extracts their text values.
@Throws(IOException::class, XmlPullParserException::class)
private fun readText(parser: XmlPullParser): String {
    var result = ""
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.text
        parser.nextTag()
    }
    return result
}
...

Java

public static class Entry {
    public final String title;
    public final String link;
    public final String summary;

    private Entry(String title, String summary, String link) {
        this.title = title;
        this.summary = summary;
        this.link = link;
    }
}

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, ns, "entry");
    String title = null;
    String summary = null;
    String link = null;
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        if (name.equals("title")) {
            title = readTitle(parser);
        } else if (name.equals("summary")) {
            summary = readSummary(parser);
        } else if (name.equals("link")) {
            link = readLink(parser);
        } else {
            skip(parser);
        }
    }
    return new Entry(title, summary, link);
}

// Processes title tags in the feed.
private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "title");
    String title = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "title");
    return title;
}

// Processes link tags in the feed.
private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {
    String link = "";
    parser.require(XmlPullParser.START_TAG, ns, "link");
    String tag = parser.getName();
    String relType = parser.getAttributeValue(null, "rel");
    if (tag.equals("link")) {
        if (relType.equals("alternate")){
            link = parser.getAttributeValue(null, "href");
            parser.nextTag();
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link");
    return link;
}

// Processes summary tags in the feed.
private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "summary");
    String summary = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "summary");
    return summary;
}

// For the tags title and summary, extracts their text values.
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
}
  ...
}

تخطي العلامات التي لا تهمّك

يحتاج المحلل اللغوي إلى تخطي العلامات التي لا يهتم بها. ها هي طريقة skip() للمحلل اللغوي:

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun skip(parser: XmlPullParser) {
    if (parser.eventType != XmlPullParser.START_TAG) {
        throw IllegalStateException()
    }
    var depth = 1
    while (depth != 0) {
        when (parser.next()) {
            XmlPullParser.END_TAG -> depth--
            XmlPullParser.START_TAG -> depth++
        }
    }
}

Java

private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
    int depth = 1;
    while (depth != 0) {
        switch (parser.next()) {
        case XmlPullParser.END_TAG:
            depth--;
            break;
        case XmlPullParser.START_TAG:
            depth++;
            break;
        }
    }
 }

إليك كيفية العمل:

  • يضع استثناءً إذا لم يكن الحدث الحالي START_TAG
  • يستهلك هذا الحقل START_TAG وجميع الأحداث بما يصل إلى END_TAG المطابقة.
  • ويتتبّع النظام عمق التداخل للتأكّد من أنّه يتوقف عند END_TAG الصحيح وليس عند أوّل علامة تظهر بعد علامة START_TAG الأصلية.

وبالتالي، إذا كان العنصر الحالي يتضمن عناصر متداخلة، فإن قيمة لن تكون قيمة depth بقيمة 0 حتى يستنفد المحلل اللغوي جميع الأحداث بين START_TAG الأصلي وEND_TAG المطابق له. بالنسبة لنفترض مثلاً أنّ المحلل يتخطّى العنصر <author> والذي يتكون من عنصرين مدمجين، <name> <uri>:

  • في المرة الأولى التي تمر فيها حلقة while، يستخدم المحلل اللغوي العلامة التالية المواجهات بعد <author> هو START_TAG <name> تزيد قيمة depth إلى 2-
  • في المرة الثانية خلال التكرار الحلقي while، يستخدم المحلل اللغوي العلامة التالية هي END_TAG </name>. القيمة لـ depth إنقاص إلى 1.
  • في المرة الثالثة خلال التكرار الحلقي while، سينتقل المحلل اللغوي للعلامة التالية هي START_TAG <uri>. القيمة لـ depth زيادات إلى 2.
  • في المرة الرابعة خلال التكرار الحلقي while، يستخدم المحلل اللغوي العلامة التالية هي END_TAG </uri>. قيمة depth ينقص إلى 1.
  • المرة الخامسة والأخيرة خلال حلقة while، هي المرة التالية العلامة التي يصادفها المحلل اللغوي هي علامة END_TAG </author> تقل قيمة depth إلى 0، ما يشير إلى أنّ العنصر <author> تم بنجاح. تم تخطيه.

استهلاك بيانات XML

يجلب مثال التطبيق خلاصة XML ويحلّلها بشكل غير متزامن. سيؤدي هذا إلى إزالة سلسلة التعليمات من واجهة المستخدم الرئيسية. فعندما اكتمال المعالجة، يقوم التطبيق بتحديث واجهة المستخدم في نشاطه الرئيسي، NetworkActivity

في المقتطف التالي، تؤدي طريقة loadPage() ما يلي:

  • لإعداد متغيّر سلسلة باستخدام عنوان URL لخلاصة XML.
  • لاستدعاء الإجراء downloadXml(url)، إذا كانت إعدادات المستخدم والشبكة الاتصال يسمح بذلك. تقوم هذه الطريقة بتنزيل الخلاصة وتحليلها وعرض نتيجة سلسلة لتكون المعروض في واجهة المستخدم.

Kotlin

class NetworkActivity : Activity() {

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"
        // Whether there is a Wi-Fi connection.
        private var wifiConnected = false
        // Whether there is a mobile connection.
        private var mobileConnected = false

        // Whether the display should be refreshed.
        var refreshDisplay = true
        // The user's current network preference setting.
        var sPref: String? = null
    }
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    fun loadPage() {

        if (sPref.equals(ANY) && (wifiConnected || mobileConnected)) {
            downloadXml(SO_URL)
        } else if (sPref.equals(WIFI) && wifiConnected) {
            downloadXml(SO_URL)
        } else {
            // Show error.
        }
    }
    ...
}

Java

public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false;
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true;
    public static String sPref = null;
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    public void loadPage() {

        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            downloadXml(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            downloadXml(URL);
        } else {
            // Show error.
        }
    }

تستدعي الطريقة downloadXml الطرق التالية في Kotlin:

  • lifecycleScope.launch(Dispatchers.IO)، التي تستخدم الكوروتينات بلغة Kotlin تشغيل الطريقة loadXmlFromNetwork() في سلسلة محادثات IO. يمرر عنوان URL للخلاصة على أنه . الطريقة التي تجلبها الطريقة loadXmlFromNetwork() وتعالجها الخلاصة. عند الانتهاء، تُمرر سلسلة نتيجة.
  • تستخدم الدالة withContext(Dispatchers.Main)، التي تستخدم الكوروتينات في لغة Kotlin للعودة إلى سلسلة التعليمات الرئيسية، وإرجاع السلسلة، ويعرضها في واجهة المستخدم.

في لغة برمجة Java، تكون العملية على النحو التالي:

  • يتم تنفيذ Executor. الطريقة loadXmlFromNetwork() على سلسلة محادثات في الخلفية. يمرر عنوان URL للخلاصة على أنه . الطريقة التي تجلبها الطريقة loadXmlFromNetwork() وتعالجها الخلاصة. عند الانتهاء، تُمرر سلسلة نتيجة.
  • Handler الاتصال بـ post للعودة إلى سلسلة التعليمات الرئيسية وإرجاع السلسلة، ويعرضها في واجهة المستخدم.

Kotlin

// Implementation of Kotlin coroutines used to download XML feed from stackoverflow.com.
private fun downloadXml(vararg urls: String) {
    var result: String? = null
    lifecycleScope.launch(Dispatchers.IO) {
        result = try {
            loadXmlFromNetwork(urls[0])
        } catch (e: IOException) {
            resources.getString(R.string.connection_error)
        } catch (e: XmlPullParserException) {
            resources.getString(R.string.xml_error)
        }
        withContext(Dispatchers.Main) {
            setContentView(R.layout.main)
            // Displays the HTML string in the UI via a WebView.
            findViewById<WebView>(R.id.webview)?.apply {
                loadData(result?: "", "text/html", null)
            }
        }
    }
}

Java

// Implementation of Executor and Handler used to download XML feed asynchronously from stackoverflow.com.
private void downloadXml(String... urls) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Handler handler = new Handler(Looper.getMainLooper());
    executor.execute(() -> {
        String result;
            try {
                result = loadXmlFromNetwork(urls[0]);
            } catch (IOException e) {
                result = getResources().getString(R.string.connection_error);
            } catch (XmlPullParserException e) {
                result = getResources().getString(R.string.xml_error);
            }
        String finalResult = result;
        handler.post(() -> {
            setContentView(R.layout.main);
            // Displays the HTML string in the UI via a WebView.
            WebView myWebView = (WebView) findViewById(R.id.webview);
            myWebView.loadData(finalResult, "text/html", null);
        });
    });
}

طريقة loadXmlFromNetwork() التي تم استدعاؤها من يتم عرض downloadXml في المقتطف التالي. ويؤديها إلى ما يلي:

  1. ينشئ مثيلاً لـ StackOverflowXmlParser. كما أنه ينشئ متغيرات List من إجمالي Entry عنصرًا (entries) ولـ title وurl وsummary، للاحتفاظ القيم المستخرجة من خلاصة XML لهذه الحقول.
  2. استدعاء downloadUrl()، الذي يجلب الخلاصة ويعرضها على النحو التالي InputStream.
  3. تستخدم StackOverflowXmlParser لتحليل InputStream. يملأ StackOverflowXmlParser List من entries مع بيانات من الخلاصة.
  4. معالجة List لـ entries وتدمج بيانات الخلاصة مع ترميز HTML
  5. إرجاع سلسلة HTML معروضة في النشاط الرئيسي واجهة مستخدم

Kotlin

// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
@Throws(XmlPullParserException::class, IOException::class)
private fun loadXmlFromNetwork(urlString: String): String {
    // Checks whether the user set the preference to include summary text.
    val pref: Boolean = PreferenceManager.getDefaultSharedPreferences(this)?.run {
        getBoolean("summaryPref", false)
    } ?: false

    val entries: List<Entry> = downloadUrl(urlString)?.use { stream ->
        // Instantiates the parser.
        StackOverflowXmlParser().parse(stream)
    } ?: emptyList()

    return StringBuilder().apply {
        append("<h3>${resources.getString(R.string.page_title)}</h3>")
        append("<em>${resources.getString(R.string.updated)} ")
        append("${formatter.format(rightNow.time)}</em>")
        // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
        // Each Entry object represents a single post in the XML feed.
        // This section processes the entries list to combine each entry with HTML markup.
        // Each entry is displayed in the UI as a link that optionally includes
        // a text summary.
        entries.forEach { entry ->
            append("<p><a href='")
            append(entry.link)
            append("'>" + entry.title + "</a></p>")
            // If the user set the preference to include summary text,
            // adds it to the display.
            if (pref) {
                append(entry.summary)
            }
        }
    }.toString()
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
@Throws(IOException::class)
private fun downloadUrl(urlString: String): InputStream? {
    val url = URL(urlString)
    return (url.openConnection() as? HttpURLConnection)?.run {
        readTimeout = 10000
        connectTimeout = 15000
        requestMethod = "GET"
        doInput = true
        // Starts the query.
        connect()
        inputStream
    }
}

Java

// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
    InputStream stream = null;
    // Instantiates the parser.
    StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();
    List<Entry> entries = null;
    String title = null;
    String url = null;
    String summary = null;
    Calendar rightNow = Calendar.getInstance();
    DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");

    // Checks whether the user set the preference to include summary text.
    SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    boolean pref = sharedPrefs.getBoolean("summaryPref", false);

    StringBuilder htmlString = new StringBuilder();
    htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");
    htmlString.append("<em>" + getResources().getString(R.string.updated) + " " +
            formatter.format(rightNow.getTime()) + "</em>");

    try {
        stream = downloadUrl(urlString);
        entries = stackOverflowXmlParser.parse(stream);
    // Makes sure that the InputStream is closed after the app is
    // finished using it.
    } finally {
        if (stream != null) {
            stream.close();
        }
     }

    // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
    // Each Entry object represents a single post in the XML feed.
    // This section processes the entries list to combine each entry with HTML markup.
    // Each entry is displayed in the UI as a link that optionally includes
    // a text summary.
    for (Entry entry : entries) {
        htmlString.append("<p><a href='");
        htmlString.append(entry.link);
        htmlString.append("'>" + entry.title + "</a></p>");
        // If the user set the preference to include summary text,
        // adds it to the display.
        if (pref) {
            htmlString.append(entry.summary);
        }
    }
    return htmlString.toString();
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
private InputStream downloadUrl(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setReadTimeout(10000 /* milliseconds */);
    conn.setConnectTimeout(15000 /* milliseconds */);
    conn.setRequestMethod("GET");
    conn.setDoInput(true);
    // Starts the query.
    conn.connect();
    return conn.getInputStream();
}