(getActivity(), R.layout.headline_item,
+ mHeadlinesList);
+ }
+
+ /**
+ * Sets the listener that should be notified of headline selection events.
+ * @param listener the listener to notify.
+ */
+ public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {
+ mHeadlineSelectedListener = listener;
+ }
+
+ /**
+ * Load and display the headlines for the given news category.
+ * @param categoryIndex the index of the news category to display.
+ */
+ public void loadCategory(int categoryIndex) {
+ mHeadlinesList.clear();
+ int i;
+ NewsCategory cat = NewsSource.getInstance().getCategory(categoryIndex);
+ for (i = 0; i < cat.getArticleCount(); i++) {
+ mHeadlinesList.add(cat.getArticle(i).getHeadline());
+ }
+ mListAdapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Handles a click on a headline.
+ *
+ * This causes the configured listener to be notified that a headline was selected.
+ */
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (null != mHeadlineSelectedListener) {
+ mHeadlineSelectedListener.onHeadlineSelected(position);
+ }
+ }
+
+ /** Sets choice mode for the list
+ *
+ * @param selectable whether list is to be selectable.
+ */
+ public void setSelectable(boolean selectable) {
+ if (selectable) {
+ getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ }
+ else {
+ getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
+ }
+ }
+}
diff --git a/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsArticle.java b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsArticle.java
new file mode 100644
index 000000000..9feaee265
--- /dev/null
+++ b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsArticle.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.newsreader;
+
+/**
+ * A news article.
+ *
+ * An article consists of a headline and a body. In this example app, article text is dynamically
+ * generated nonsense.
+ */
+public class NewsArticle {
+ // How many sentences in each paragraph?
+ final int SENTENCES_PER_PARAGRAPH = 20;
+
+ // How many paragraphs in each article?
+ final int PARAGRAPHS_PER_ARTICLE = 5;
+
+ // Headline and body
+ String mHeadline, mBody;
+
+ /**
+ * Create a news article with randomly generated text.
+ * @param ngen the nonsense generator to use.
+ */
+ public NewsArticle(NonsenseGenerator ngen) {
+ mHeadline = ngen.makeHeadline();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("");
+ sb.append("" + mHeadline + "
");
+ int i;
+ for (i = 0; i < PARAGRAPHS_PER_ARTICLE; i++) {
+ sb.append("").append(ngen.makeText(SENTENCES_PER_PARAGRAPH)).append("
");
+ }
+
+ sb.append("");
+ mBody = sb.toString();
+ }
+
+ /** Returns the headline. */
+ public String getHeadline() {
+ return mHeadline;
+ }
+
+ /** Returns the article body (HTML)*/
+ public String getBody() {
+ return mBody;
+ }
+}
diff --git a/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsCategory.java b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsCategory.java
new file mode 100644
index 000000000..75c7b223a
--- /dev/null
+++ b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsCategory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.newsreader;
+
+/**
+ * A news category (collection of articles).
+ */
+public class NewsCategory {
+ // how many articles?
+ final int ARTICLES_PER_CATEGORY = 20;
+
+ // array of our articles
+ NewsArticle[] mArticles;
+
+ /**
+ * Create a news category.
+ *
+ * The articles are dynamically generated with fun and random nonsense.
+ */
+ public NewsCategory() {
+ NonsenseGenerator ngen = new NonsenseGenerator();
+ mArticles = new NewsArticle[ARTICLES_PER_CATEGORY];
+ int i;
+ for (i = 0; i < mArticles.length; i++) {
+ mArticles[i] = new NewsArticle(ngen);
+ }
+ }
+
+ /** Returns how many articles exist in this category. */
+ public int getArticleCount() {
+ return mArticles.length;
+ }
+
+ /** Gets a particular article by index. */
+ public NewsArticle getArticle(int index) {
+ return mArticles[index];
+ }
+}
diff --git a/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsReaderActivity.java b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsReaderActivity.java
new file mode 100644
index 000000000..30ae0ae32
--- /dev/null
+++ b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsReaderActivity.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.newsreader;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.SpinnerAdapter;
+
+/**
+ * Main activity: shows headlines list and articles, if layout permits.
+ *
+ * This is the main activity of the application. It can have several different layouts depending
+ * on the SDK version, screen size and orientation. The configurations are divided in two large
+ * groups: single-pane layouts and dual-pane layouts.
+ *
+ * In single-pane mode, this activity shows a list of headlines using a {@link HeadlinesFragment}.
+ * When the user clicks on a headline, a separate activity (a {@link ArticleActivity}) is launched
+ * to show the news article.
+ *
+ * In dual-pane mode, this activity shows a {@HeadlinesFragment} on the left side and an
+ * {@ArticleFragment} on the right side. When the user selects a headline on the left, the
+ * corresponding article is shown on the right.
+ *
+ * If an Action Bar is available (large enough screen and SDK version 11 or up), navigation
+ * controls are shown in the Action Bar (whether to show tabs or a list depends on the layout).
+ * If an Action Bar is not available, a regular image and button are shown in the top area of
+ * the screen, emulating an Action Bar.
+ */
+public class NewsReaderActivity extends FragmentActivity
+ implements HeadlinesFragment.OnHeadlineSelectedListener,
+ CompatActionBarNavListener,
+ OnClickListener {
+
+ // Whether or not we are in dual-pane mode
+ boolean mIsDualPane = false;
+
+ // The fragment where the headlines are displayed
+ HeadlinesFragment mHeadlinesFragment;
+
+ // The fragment where the article is displayed (null if absent)
+ ArticleFragment mArticleFragment;
+
+ // The news category and article index currently being displayed
+ int mCatIndex = 0;
+ int mArtIndex = 0;
+ NewsCategory mCurrentCat;
+
+ // List of category titles
+ final String CATEGORIES[] = { "Top Stories", "Politics", "Economy", "Technology" };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_layout);
+
+ // find our fragments
+ mHeadlinesFragment = (HeadlinesFragment) getSupportFragmentManager().findFragmentById(
+ R.id.headlines);
+ mArticleFragment = (ArticleFragment) getSupportFragmentManager().findFragmentById(
+ R.id.article);
+
+ // Determine whether we are in single-pane or dual-pane mode by testing the visibility
+ // of the article view.
+ View articleView = findViewById(R.id.article);
+ mIsDualPane = articleView != null && articleView.getVisibility() == View.VISIBLE;
+
+ // Register ourselves as the listener for the headlines fragment events.
+ mHeadlinesFragment.setOnHeadlineSelectedListener(this);
+
+ // Set up the Action Bar (or not, if one is not available)
+ int catIndex = savedInstanceState == null ? 0 : savedInstanceState.getInt("catIndex", 0);
+ setUpActionBar(mIsDualPane, catIndex);
+
+ // Set up headlines fragment
+ mHeadlinesFragment.setSelectable(mIsDualPane);
+ restoreSelection(savedInstanceState);
+
+ // Set up the category button (shown if an Action Bar is not available)
+ Button catButton = (Button) findViewById(R.id.categorybutton);
+ if (catButton != null) {
+ catButton.setOnClickListener(this);
+ }
+ }
+
+ /** Restore category/article selection from saved state. */
+ void restoreSelection(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ setNewsCategory(savedInstanceState.getInt("catIndex", 0));
+ if (mIsDualPane) {
+ int artIndex = savedInstanceState.getInt("artIndex", 0);
+ mHeadlinesFragment.setSelection(artIndex);
+ onHeadlineSelected(artIndex);
+ }
+ }
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ restoreSelection(savedInstanceState);
+ }
+
+ /** Sets up Action Bar (if present).
+ *
+ * @param showTabs whether to show tabs (if false, will show list).
+ * @param selTab the selected tab or list item.
+ */
+ public void setUpActionBar(boolean showTabs, int selTab) {
+ if (Build.VERSION.SDK_INT < 11) {
+ // No action bar for you!
+ // But do not despair. In this case the layout includes a bar across the
+ // top that looks and feels like an action bar, but is made up of regular views.
+ return;
+ }
+
+ android.app.ActionBar actionBar = getActionBar();
+ actionBar.setDisplayShowTitleEnabled(false);
+
+ // Set up a CompatActionBarNavHandler to deliver us the Action Bar nagivation events
+ CompatActionBarNavHandler handler = new CompatActionBarNavHandler(this);
+ if (showTabs) {
+ actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_TABS);
+ int i;
+ for (i = 0; i < CATEGORIES.length; i++) {
+ actionBar.addTab(actionBar.newTab().setText(CATEGORIES[i]).setTabListener(handler));
+ }
+ actionBar.setSelectedNavigationItem(selTab);
+ }
+ else {
+ actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_LIST);
+ SpinnerAdapter adap = new ArrayAdapter(this, R.layout.actionbar_list_item,
+ CATEGORIES);
+ actionBar.setListNavigationCallbacks(adap, handler);
+ }
+
+ // Show logo instead of icon+title.
+ actionBar.setDisplayUseLogoEnabled(true);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setNewsCategory(0);
+ }
+
+ /** Sets the displayed news category.
+ *
+ * This causes the headlines fragment to be repopulated with the appropriate headlines.
+ */
+ void setNewsCategory(int categoryIndex) {
+ mCatIndex = categoryIndex;
+ mCurrentCat = NewsSource.getInstance().getCategory(categoryIndex);
+ mHeadlinesFragment.loadCategory(categoryIndex);
+
+ // If we are displaying the article on the right, we have to update that too
+ if (mIsDualPane) {
+ mArticleFragment.displayArticle(mCurrentCat.getArticle(0));
+ }
+
+ // If we are displaying a "category" button (on the ActionBar-less UI), we have to update
+ // its text to reflect the current category.
+ Button catButton = (Button) findViewById(R.id.categorybutton);
+ if (catButton != null) {
+ catButton.setText(CATEGORIES[mCatIndex]);
+ }
+ }
+
+ /** Called when a headline is selected.
+ *
+ * This is called by the HeadlinesFragment (via its listener interface) to notify us that a
+ * headline was selected in the Action Bar. The way we react depends on whether we are in
+ * single or dual-pane mode. In single-pane mode, we launch a new activity to display the
+ * selected article; in dual-pane mode we simply display it on the article fragment.
+ *
+ * @param index the index of the selected headline.
+ */
+ @Override
+ public void onHeadlineSelected(int index) {
+ mArtIndex = index;
+ if (mIsDualPane) {
+ // display it on the article fragment
+ mArticleFragment.displayArticle(mCurrentCat.getArticle(index));
+ }
+ else {
+ // use separate activity
+ Intent i = new Intent(this, ArticleActivity.class);
+ i.putExtra("catIndex", mCatIndex);
+ i.putExtra("artIndex", index);
+ startActivity(i);
+ }
+ }
+
+ /** Called when a news category is selected.
+ *
+ * This is called by our CompatActionBarNavHandler in response to the user selecting a
+ * news category in the Action Bar. We react by loading and displaying the headlines for
+ * that category.
+ *
+ * @param catIndex the index of the selected news category.
+ */
+ @Override
+ public void onCategorySelected(int catIndex) {
+ setNewsCategory(catIndex);
+ }
+
+ /** Save instance state. Saves current category/article index. */
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt("catIndex", mCatIndex);
+ outState.putInt("artIndex", mArtIndex);
+ super.onSaveInstanceState(outState);
+ }
+
+ /** Called when news category button is clicked.
+ *
+ * This is the button that we display on UIs that don't have an action bar. This button
+ * calls up a list of news categories and switches to the given category.
+ */
+ @Override
+ public void onClick(View v) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Select a Category");
+ builder.setItems(CATEGORIES, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ setNewsCategory(which);
+ }
+ });
+ AlertDialog d = builder.create();
+ d.show();
+ }
+}
diff --git a/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsSource.java b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsSource.java
new file mode 100644
index 000000000..18cbe8b37
--- /dev/null
+++ b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NewsSource.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.newsreader;
+
+/**
+ * Source of strange and wonderful news.
+ *
+ * This singleton functions as the repository for the news we display.
+ */
+public class NewsSource {
+ // the instance
+ static NewsSource instance = null;
+
+ // the category names
+ final String[] CATEGORIES = { "Top Stories", "US", "Politics", "Economy" };
+
+ // category objects, representing each category
+ NewsCategory[] mCategory;
+
+ /** Returns the singleton instance of this class. */
+ public static NewsSource getInstance() {
+ if (instance == null) {
+ instance = new NewsSource();
+ }
+ return instance;
+ }
+
+ public NewsSource() {
+ int i;
+ mCategory = new NewsCategory[CATEGORIES.length];
+ for (i = 0; i < CATEGORIES.length; i++) {
+ mCategory[i] = new NewsCategory();
+ }
+ }
+
+ /** Returns the list of news categories. */
+ public String[] getCategories() {
+ return CATEGORIES;
+ }
+
+ /** Returns a category by index. */
+ public NewsCategory getCategory(int categoryIndex) {
+ return mCategory[categoryIndex];
+ }
+}
diff --git a/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NonsenseGenerator.java b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NonsenseGenerator.java
new file mode 100644
index 000000000..e38b77769
--- /dev/null
+++ b/samples/training/multiscreen/newsreader/src/com/example/android/newsreader/NonsenseGenerator.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.newsreader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/** Generator of random news. More fun than "lorem ipsum", isn't it?
+ *
+ * This generator can construct headlines and news articles by randomly composing sentences.
+ * Any resemblance to actual events (or, actually, any resemblance to anything that makes sense)
+ * is merely coincidental!
+ */
+public class NonsenseGenerator {
+ Random mRandom;
+
+ static final String[] THINGS = { "bottle", "bowl", "brick", "building",
+ "bunny", "cake", "car", "cat", "cup", "desk", "dog", "duck",
+ "elephant", "engineer", "fork", "glass", "griffon", "hat", "key", "knife", "lawyer",
+ "llama", "manual", "meat", "monitor", "mouse", "tangerine", "paper", "pear", "pen",
+ "pencil", "phone", "physicist", "planet", "potato", "road", "salad", "shoe", "slipper",
+ "soup", "spoon", "star", "steak", "table", "terminal", "treehouse", "truck",
+ "watermelon", "window" };
+
+ static final String[] ADJECTIVES = { "red", "green", "yellow", "gray", "solid", "fierce",
+ "friendly", "cowardly", "convenient", "foreign", "national", "tall",
+ "short", "metallic", "golden", "silver", "sweet", "nationwide", "competitive",
+ "stable", "municipal", "famous" };
+
+ static final String[] VERBS_PAST = { "accused", "threatened", "warned", "spoke to",
+ "has met with",
+ "was seen in the company of", "advanced towards", "collapsed on",
+ "signed a partnership with", "was converted into", "became", "was authorized to sell",
+ "sold", "bought", "rented", "allegedly spoke to", "leased", "is now investing on",
+ "is expected to buy", "is expected to sell", "was reported to have met with",
+ "will work together with", "plans to cease fire against", "started a war with",
+ "signed a truce with", "is now managing", "is investigating" };
+
+ static final String[] VERBS_PRESENT = { "accuses", "threatens", "warns", "speaks to",
+ "meets with",
+ "seen with", "advances towards", "collapses on",
+ "signs partnership with", "converts into", "becomes", "is authorized to sell",
+ "sells", "buys", "rents", "allegedly speaks to", "leases", "invests on",
+ "expected to buy", "expected to sell", "reported to have met with",
+ "works together with", "plans cease fire against", "starts war with",
+ "signs truce with", "now manages" };
+
+ public NonsenseGenerator() {
+ mRandom = new Random();
+ }
+
+ /** Produces something that reads like a headline. */
+ public String makeHeadline() {
+ return makeSentence(true);
+ }
+
+ /** Produces a sentence.
+ *
+ * @param isHeadline whether the sentence should look like a headline or not.
+ * @return the generated sentence.
+ */
+ public String makeSentence(boolean isHeadline) {
+ List words = new ArrayList();
+ generateSentence(words, isHeadline);
+ words.set(0, String.valueOf(Character.toUpperCase(words.get(0).charAt(0))) +
+ words.get(0).substring(1));
+ return joinWords(words);
+ }
+
+ /** Produces news article text.
+ *
+ * @param numSentences how many sentences the text is to contain.
+ * @return the generated text.
+ */
+ public String makeText(int numSentences) {
+ StringBuilder sb = new StringBuilder();
+ while (numSentences-- > 0) {
+ sb.append(makeSentence(false) + ".");
+ if (numSentences > 0) {
+ sb.append(" ");
+ }
+ }
+ return sb.toString();
+ }
+
+ /** Generates a sentence.
+ *
+ * @param words the list of words to which the sentence will be appended.
+ * @param isHeadline whether the sentence must look like a headline or not.
+ */
+ private void generateSentence(List words, boolean isHeadline) {
+ if (!isHeadline && mRandom.nextInt(4) == 0)
+ generateTimeClause(words, isHeadline);
+ generateAgent(words, isHeadline);
+ generatePredicate(words, isHeadline);
+ }
+
+ private void generateTimeClause(List words, boolean isHeadline) {
+ if (mRandom.nextInt(2) == 0) {
+ words.add(pickOneOf("today", "yesterday", "this afternoon", "this morning",
+ "last evening"));
+ }
+ else {
+ words.add(pickOneOf("this", "last"));
+ words.add(pickOneOf("Monday", "Tuesday", "Wednesday", "Thursday"));
+ words.add(pickOneOf("morning", "afternoon", "evening"));
+ }
+ }
+
+ private void generateAgent(List words, boolean isHeadline) {
+ if (!isHeadline) {
+ words.add(pickOneOf("a", "the"));
+ }
+ if (mRandom.nextInt(3) != 0) {
+ words.add(pickOneOf(ADJECTIVES));
+ }
+ words.add(pickOneOf(THINGS));
+ }
+
+ private void generatePredicate(List words, boolean isHeadline) {
+ words.add(pickOneOf(isHeadline ? VERBS_PRESENT : VERBS_PAST));
+ if (!isHeadline)
+ words.add(pickOneOf("a", "the"));
+ if (mRandom.nextInt(3) != 0) {
+ words.add(pickOneOf(ADJECTIVES));
+ }
+ words.add(pickOneOf(THINGS));
+
+ if (mRandom.nextInt(3) == 0) {
+ words.add(isHeadline ? pickOneOf(", claims", ", says") :
+ pickOneOf(", claimed", ", said", ", reported"));
+ if (!isHeadline)
+ words.add(pickOneOf("a", "the"));
+ if (mRandom.nextInt(3) != 0) {
+ words.add(pickOneOf(ADJECTIVES));
+ }
+ words.add(pickOneOf(THINGS));
+ }
+ }
+
+ private String pickOneOf(String ... options) {
+ return options[mRandom.nextInt(options.length)];
+ }
+
+ private static String joinWords(List words) {
+ int i;
+ if (words.size() == 0) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append(words.get(0));
+ for (i = 1; i < words.size(); i++) {
+ if (!words.get(i).startsWith(",")) {
+ sb.append(" ");
+ }
+ sb.append(words.get(i));
+ }
+ return sb.toString();
+ }
+}