/* * Copyright 2009 Andrew Shu * * This file is part of "reddit is fun". * * "reddit is fun" is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * "reddit is fun" is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with "reddit is fun". If not, see <http://www.gnu.org/licenses/>. */ package com.andrewshu.android.reddit.reddits; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.ViewGroup; import android.view.Window; import android.webkit.CookieSyncManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import com.andrewshu.android.reddit.R; import com.andrewshu.android.reddit.common.CacheInfo; import com.andrewshu.android.reddit.common.Common; import com.andrewshu.android.reddit.common.Constants; import com.andrewshu.android.reddit.common.RedditIsFunHttpClientFactory; import com.andrewshu.android.reddit.common.util.CollectionUtils; import com.andrewshu.android.reddit.common.util.Util; import com.andrewshu.android.reddit.settings.RedditSettings; public final class PickSubredditActivity extends ListActivity { private static final String TAG = "PickSubredditActivity"; // Group 1: inner private final Pattern MY_SUBREDDITS_OUTER = Pattern.compile("your front page reddits.*?<ul>(.*?)</ul>", Pattern.CASE_INSENSITIVE); // Group 3: subreddit name. Repeat the matcher.find() until it fails. private final Pattern MY_SUBREDDITS_INNER = Pattern.compile("<a(.*?)/r/(.*?)>(.+?)</a>"); private RedditSettings mSettings = new RedditSettings(); private HttpClient mClient = RedditIsFunHttpClientFactory.getGzipHttpClient(); private PickSubredditAdapter mSubredditsAdapter; private ArrayList<String> mSubredditsList; private static final Object ADAPTER_LOCK = new Object(); private EditText mEt; private AsyncTask<?, ?, ?> mCurrentTask = null; private final Object mCurrentTaskLock = new Object(); public static final String[] DEFAULT_SUBREDDITS = { "pics", "funny", "politics", "gaming", "askreddit", "worldnews", "videos", "iama", "todayilearned", "wtf", "aww", "technology", "science", "music", "askscience", "movies", "bestof", "fffffffuuuuuuuuuuuu", "programming", "comics", "offbeat", "environment", "business", "entertainment", "economics", "trees", "linux", "android" }; // A list of special subreddits that can be viewed, but cannot be used for submissions. They inherit from the FakeSubreddit class // in the redditdev source, so we use the same naming here. Note: Should we add r/Random and r/Friends? public static final String[] FAKE_SUBREDDITS = { Constants.FRONTPAGE_STRING, "all" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CookieSyncManager.createInstance(getApplicationContext()); mSettings.loadRedditPreferences(this, mClient); setRequestedOrientation(mSettings.getRotation()); requestWindowFeature(Window.FEATURE_PROGRESS); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setTheme(mSettings.getTheme()); setContentView(R.layout.pick_subreddit_view); registerForContextMenu(getListView()); resetUI(null); mSubredditsList = cacheSubredditsList(mSubredditsList); if (CollectionUtils.isEmpty(mSubredditsList)) restoreLastNonConfigurationInstance(); if (CollectionUtils.isEmpty(mSubredditsList)) { new DownloadRedditsTask().execute(); } else { addFakeSubredditsUnlessSuppressed(); resetUI(new PickSubredditAdapter(this, mSubredditsList)); } } @Override public void onResume() { super.onResume(); CookieSyncManager.getInstance().startSync(); } @Override public void onPause() { super.onPause(); CookieSyncManager.getInstance().stopSync(); } @Override public Object onRetainNonConfigurationInstance() { // Avoid having to re-download and re-parse the subreddits list // when rotating or opening keyboard. return mSubredditsList; } @SuppressWarnings("unchecked") private void restoreLastNonConfigurationInstance() { mSubredditsList = (ArrayList<String>) getLastNonConfigurationInstance(); } void resetUI(PickSubredditAdapter adapter) { findViewById(R.id.loading_light).setVisibility(View.GONE); findViewById(R.id.loading_dark).setVisibility(View.GONE); synchronized (ADAPTER_LOCK) { if (adapter == null) { // Reset the list to be empty. mSubredditsList = new ArrayList<String>(); mSubredditsAdapter = new PickSubredditAdapter(this, mSubredditsList); } else { mSubredditsAdapter = adapter; } setListAdapter(mSubredditsAdapter); mSubredditsAdapter.mLoading = false; mSubredditsAdapter.notifyDataSetChanged(); // Just in case } Common.updateListDrawables(this, mSettings.getTheme()); // Set the EditText to do same thing as onListItemClick mEt = (EditText) findViewById(R.id.pick_subreddit_input); if (mEt != null) { mEt.setOnKeyListener(new OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { returnSubreddit(mEt.getText().toString().trim()); return true; } return false; } }); mEt.setFocusableInTouchMode(true); } Button goButton = (Button) findViewById(R.id.pick_subreddit_button); if (goButton != null) { goButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { returnSubreddit(mEt.getText().toString().trim()); } }); } getListView().requestFocus(); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { String item = mSubredditsAdapter.getItem(position); returnSubreddit(item); } private void returnSubreddit(String subreddit) { Intent intent = new Intent(); intent.setData(Util.createSubredditUri(subreddit)); setResult(RESULT_OK, intent); finish(); } private void enableLoadingScreen() { if (Util.isLightTheme(mSettings.getTheme())) { findViewById(R.id.loading_light).setVisibility(View.VISIBLE); findViewById(R.id.loading_dark).setVisibility(View.GONE); } else { findViewById(R.id.loading_light).setVisibility(View.GONE); findViewById(R.id.loading_dark).setVisibility(View.VISIBLE); } synchronized (ADAPTER_LOCK) { if (mSubredditsAdapter != null) mSubredditsAdapter.mLoading = true; } getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_START); } private void disableLoadingScreen() { resetUI(mSubredditsAdapter); getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_END); } class DownloadRedditsTask extends AsyncTask<Void, Void, ArrayList<String>> { @Override public ArrayList<String> doInBackground(Void... voidz) { ArrayList<String> reddits = null; HttpEntity entity = null; try { reddits = cacheSubredditsList(reddits); if (reddits == null) { reddits = new ArrayList<String>(); HttpGet request = new HttpGet(Constants.REDDIT_BASE_URL + "/reddits"); // Set timeout to 15 seconds HttpParams params = request.getParams(); HttpConnectionParams.setConnectionTimeout(params, 15000); HttpConnectionParams.setSoTimeout(params, 15000); HttpResponse response = mClient.execute(request); entity = response.getEntity(); BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent())); String line = in.readLine(); in.close(); entity.consumeContent(); Matcher outer = MY_SUBREDDITS_OUTER.matcher(line); if (outer.find()) { Matcher inner = MY_SUBREDDITS_INNER.matcher(outer.group(1)); while (inner.find()) { reddits.add(inner.group(3)); } } else { return null; } if (Constants.LOGGING) Log.d(TAG, "new subreddit list size: " + reddits.size()); if (Constants.USE_SUBREDDITS_CACHE) { try { CacheInfo.setCachedSubredditList(getApplicationContext(), reddits); if (Constants.LOGGING) Log.d(TAG, "wrote subreddit list to cache:" + reddits); } catch (IOException e) { if (Constants.LOGGING) Log.e(TAG, "error on setCachedSubredditList", e); } } } return reddits; } catch (Exception e) { if (Constants.LOGGING) Log.e(TAG, "failed", e); if (entity != null) { try { entity.consumeContent(); } catch (Exception e2) { // Ignore. } } } return null; } @Override public void onPreExecute() { synchronized (mCurrentTaskLock) { if (mCurrentTask != null) { this.cancel(true); return; } mCurrentTask = this; } resetUI(null); enableLoadingScreen(); } @Override public void onPostExecute(ArrayList<String> reddits) { synchronized (mCurrentTaskLock) { mCurrentTask = null; } disableLoadingScreen(); if (reddits == null || reddits.size() == 0) { // Need to make a copy because Arrays.asList returns List backed by original array mSubredditsList = new ArrayList<String>(); mSubredditsList.addAll(Arrays.asList(DEFAULT_SUBREDDITS)); } else { mSubredditsList = reddits; } addFakeSubredditsUnlessSuppressed(); resetUI(new PickSubredditAdapter(PickSubredditActivity.this, mSubredditsList)); } } private void addFakeSubredditsUnlessSuppressed() { // Insert special reddits (front page, all) into subreddits list, unless suppressed by Intent extras Bundle extras = getIntent().getExtras(); boolean addFakeSubreddits = false; if (extras != null) { boolean shouldHideFakeSubreddits = extras.getBoolean(Constants.EXTRA_HIDE_FAKE_SUBREDDITS_STRING, false); if (!shouldHideFakeSubreddits) { addFakeSubreddits = true; } } else { addFakeSubreddits = true; } if (addFakeSubreddits) { mSubredditsList.addAll(0, Arrays.asList(FAKE_SUBREDDITS)); } } private final class PickSubredditAdapter extends ArrayAdapter<String> { private LayoutInflater mInflater; private boolean mLoading = true; private int mFrequentSeparatorPos = ListView.INVALID_POSITION; public PickSubredditAdapter(Context context, List<String> objects) { super(context, 0, objects); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public boolean isEmpty() { if (mLoading) { // We don't want the empty state to show when loading. return false; } else { return super.isEmpty(); } } @Override public int getItemViewType(int position) { if (position == mFrequentSeparatorPos) { // We don't want the separator view to be recycled. return IGNORE_ITEM_VIEW_TYPE; } return super.getItemViewType(position); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view; // Here view may be passed in for re-use, or we make a new one. if (convertView == null) { view = mInflater.inflate(android.R.layout.simple_list_item_1, null); } else { view = convertView; } TextView text = (TextView) view.findViewById(android.R.id.text1); text.setText(mSubredditsAdapter.getItem(position)); return view; } } @Override protected Dialog onCreateDialog(int id) { Dialog dialog; ProgressDialog pdialog; switch (id) { // "Please wait" case Constants.DIALOG_LOADING_REDDITS_LIST: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Loading your reddits..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; default: throw new IllegalArgumentException("Unexpected dialog id "+id); } return dialog; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: Common.goHome(this); break; default: throw new IllegalArgumentException("Unexpected action value "+item.getItemId()); } return true; } @Override protected void onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); final int[] myDialogs = { Constants.DIALOG_LOADING_REDDITS_LIST, }; for (int dialog : myDialogs) { try { removeDialog(dialog); } catch (IllegalArgumentException e) { // Ignore. } } } protected ArrayList<String> cacheSubredditsList(ArrayList<String> reddits){ if (Constants.USE_SUBREDDITS_CACHE) { if (CacheInfo.checkFreshSubredditListCache(getApplicationContext())) { reddits = CacheInfo.getCachedSubredditList(getApplicationContext()); if (Constants.LOGGING) Log.d(TAG, "cached subreddit list:" + reddits); } } return reddits; } }