Android Open Source - PkRSS Pk R S S






From Project

Back to project page PkRSS.

License

The source code is released under:

Apache License

If you think the Android project PkRSS listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.pkmmte.pkrss;
//  w  w  w.j a  va  2  s . c om
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
import android.util.SparseBooleanArray;
import com.pkmmte.pkrss.downloader.DefaultDownloader;
import com.pkmmte.pkrss.downloader.Downloader;
import com.pkmmte.pkrss.downloader.OkHttpDownloader;
import com.pkmmte.pkrss.parser.Parser;
import com.pkmmte.pkrss.parser.Rss2Parser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A powerful RSS feed manager for Android
 * <p>
 * Use {@link #with(android.content.Context)} for the global singleton instance or construct your
 * own instance with {@link PkRSS.Builder}.
 */
public class PkRSS {
  // General public constant keys
  public static final String KEY_ARTICLE = "ARTICLE";
  public static final String KEY_ARTICLE_ID = "ARTICLE ID";
  public static final String KEY_ARTICLE_URL = "ARTICLE URL";
  public static final String KEY_FEED_URL = "FEED URL";
  public static final String KEY_CATEGORY_NAME = "CATEGORY NAME";
  public static final String KEY_CATEGORY = "CATEGORY";
  public static final String KEY_SEARCH = "SEARCH TERM";
  public static final String KEY_READ_ARRAY = "READ ARRAY";
  public static final String KEY_FAVORITES = "FAVORITES";

  // Global singleton instance
  private static PkRSS singleton = null;

  // For issue tracking purposes
  private volatile boolean loggingEnabled;
  protected static final String TAG = "PkRSS";

  // Callback Thread Handler
  protected final CallbackHandler handler;

  // Context is always useful for some reason.
  private final Context mContext;

  // For storing & reading read data
  private final SharedPreferences mPrefs;

  // Our handy client for getting XML feed data
  private final Downloader downloader;

  // Reusable XML Parser
  private final Parser parser;

  // List of stored articles
  private final Map<String, List<Article>> articleMap = new HashMap<String, List<Article>>();

  // Keep track of pages already loaded on specific feeds
  private final Map<String, Integer> pageTracker = new HashMap<String, Integer>();

  // Persistent SparseArray for checking an article's read state
  private final SparseBooleanArray readList = new SparseBooleanArray();

  // Database storing all articles marked as favorite
  private final FavoriteDatabase favoriteDatabase;

  /**
   * The global default {@link PkRSS} instance.
   * <p>
   * This instance is automatically initialized with defaults that are suitable to most
   * implementations.
   * <p>
   * If these settings do not meet the requirements of your application, you can construct your own
   * instance with full control over the configuration by using {@link PkRSS.Builder}.
   * You may also use {@link PkRSS.Builder#buildSingleton()} to build the singleton or
   * {@link PkRSS.Builder#replaceSingleton()} to replace the existing singleton if it differs.
   */
  public static PkRSS with(Context context) {
    if(singleton == null)
      singleton = new Builder(context).build();
    return singleton;
  }

  /**
   * Returns the global singleton instance. It may be null so use wisely!
   * @return Global singleton instance. May be null.
   */
  protected static PkRSS getInstance() {
    return singleton;
  }

  PkRSS(Context context, CallbackHandler handler, Downloader downloader, Parser parser, boolean loggingEnabled) {
    this.mContext = context;
    this.handler = handler;
    this.downloader = downloader;
    this.downloader.attachInstance(this);
    this.parser = parser;
    this.parser.attachInstance(this);
    this.loggingEnabled = loggingEnabled;
    this.mPrefs = context.getSharedPreferences(TAG, Context.MODE_PRIVATE);
    getRead();
    favoriteDatabase = new FavoriteDatabase(context);
  }

  /**
   * Toggle whether debug logging is enabled.
   */
  public void setLoggingEnabled(boolean enabled) {
    loggingEnabled = enabled;
    Log.d(TAG, "Logging is now " + (enabled ? "enabled" : "disabled"));
  }

  /**
   * {@code true} if debug logging is enabled.
   */
  public boolean isLoggingEnabled() {
    return loggingEnabled;
  }

  /**
   * Starts a request with the specified URL.
   * <p>
   * URLs may be anything you wish as long as the {@link Parser} and/or
   * {@link Downloader} supports it. See {@link PkRSS.Builder} for using
   * your own custom Downloaders/Parsers. If necessary, you may use a custom
   * parser for this specific request via {@link RequestCreator#parser(Parser)}.
   * @param url URL to load feed from.
   * @return A chained Request.
   */
  public RequestCreator load(String url) {
    return new RequestCreator(this, url);
  }

  /**
   * Handles the specified {@link Request}. May throw an {@link IOException} for
   * mishandled URLs or timeouts.
   * @param request Request to execute.
   * @throws IOException
   */
  protected void load(final Request request) throws IOException {
    // Don't load if URL is the favorites key
    if(request.url.equals(KEY_FAVORITES)) {
      log("Favorites URL detected, skipping load...");
      return;
    }

    // Call the "OnPreload" code, if available
    if(request.callback != null && request.handler == null)
      handler.onPreload(request.callback);
    else if(request.callback != null)
      request.handler.onPreload(request.callback);

    // Create safe url for pagination/indexing purposes
    String safeUrl = request.downloader == null ? downloader.toSafeUrl(request) : request.downloader.toSafeUrl(request);

    // Put the page index into the request's HashMap
    pageTracker.put(safeUrl, request.page);

    // Get response from this request
    String response = request.downloader == null ? downloader.execute(request) : request.downloader.execute(request);

    // Parse articles from response and inset into global list
    List<Article> newArticles = request.parser == null ? parser.parse(response) : request.parser.parse(response);
    insert(safeUrl, newArticles);

    // Call the "OnLoaded" code, if available
    if(request.callback != null && request.handler == null)
      handler.onLoaded(request.callback, newArticles);
    else if(request.callback != null)
      request.handler.onLoaded(request.callback, newArticles);
  }

  /**
   * Returns a {@link HashMap<String, List<Article>} containing all loaded
   * Article objects. The map key being the safe url.
   * @return
   */
  public Map<String, List<Article>> get() {
    return articleMap;
  }

  /**
   * Looks up the specified URL String from the saved HashMap.
   * @param url Safe URL to look up loaded articles from. May also be {@link PkRSS#KEY_FAVORITES}.
   * @return A {@link List} containing all loaded articles associated with that
   * URL. May be null if no such URL has yet been loaded.
   */
  public List<Article> get(String url) {
    if(url.equals(KEY_FAVORITES))
      return getFavorites();

    return articleMap.get(url);
  }

  /**
   * Similar to {@link PkRSS#get(String)} but also looks for the search term.
   * @param url Safe URL to look up loaded articles from.
   * @param search Search term.
   * @return A {@link List} containing all loaded articles associated with that
   * URL and query. May be null if no such URL has yet been loaded.
   */
  public List<Article> get(String url, String search) {
    if(search == null)
      return articleMap.get(url);

    return articleMap.get(url + "?s=" + Uri.encode(search));
  }

  /**
   * Returns an {@link Article} object associated with the specified id.
   * @param id ID belonging to such article.
   * @return The Article associated with the specified id. May return null if
   * no such article was found with that ID.
   */
  public Article get(int id) {
    long time = System.currentTimeMillis();

    // Look for an article with this id in the Article HashMap
    for(List<Article> articleList : articleMap.values()) {
      for(Article article : articleList) {
        if(article.getId() == id) {
          log("get(" + id + ") took " + (System.currentTimeMillis() - time) + "ms");
          return article;
        }
      }
    }

    // If none was found, try searching in the favorites database
    for(Article article : favoriteDatabase.getAll()) {
      if(article.getId() == id) {
        log("get(" + id + ") took " + (System.currentTimeMillis() - time) + "ms");
        return article;
      }
    }

    log("Could not find Article with id " + id, Log.WARN);
    log("get(" + id + ") took " + (System.currentTimeMillis() - time) + "ms");
    return null;
  }

  /**
   * Retrieves an ArrayList of articles from the Favorite Database.
   * @return Either an Article List or null if database wasn't properly started.
   */
  public List<Article> getFavorites() {
    return favoriteDatabase == null ? null : favoriteDatabase.getAll();
  }

  /**
   * Marks/Unmarks all loaded articles as read. This includes those loaded from
   * all URLs and those stored in the favorites database. Those loaded after this
   * was called will still be unread.
   * @param read
   */
  public void markAllRead(boolean read) {
    long time = System.currentTimeMillis();

    // Just clear the array and return, if marking as unread
    if(!read) {
      readList.clear();
      writeRead();
      log("markAllRead(" + String.valueOf(read) + ") took " + (System.currentTimeMillis() - time) + "ms");
      return;
    }

    // Look for an article with this id in the Article HashMap
    for(List<Article> articleList : articleMap.values()) {
      for(Article article : articleList)
        readList.put(article.getId(), read);
    }

    // If none was found, try searching in the favorites database
    for(Article article : favoriteDatabase.getAll()) {
      readList.put(article.getId(), read);
    }

    writeRead();
    log("markAllRead(" + String.valueOf(read) + ") took " + (System.currentTimeMillis() - time) + "ms");
  }

  /**
   * Marks an article id as read.
   * @param id Article id to store its read state.
   * @param read Whether or not to mark this id as read.
   */
  public void markRead(int id, boolean read) {
    readList.put(id, read);
    writeRead();
  }

  /**
   * @param id Article ID to check for its read state.
   * @return {@code true} if such id was previously marked as read,
   * {@code false} if it has not yet been marked as read.
   */
  public boolean isRead(int id) {
    return readList.get(id, false);
  }

  /**
   * Saves an {@link Article} object to the favorites database.
   * <p>
   * If possible, use the Article object itself instead to increase performance!
   * @param id ID of the article to save.
   * @return {@code true} if successful, {@code false} if otherwise.
   */
  public boolean saveFavorite(int id) {
    return saveFavorite(get(id), true);
  }

  /**
   * Saves/Deletes an {@link Article} object to the favorites database.
   * <p>
   * If possible, use the Article object itself instead to increase performance!
   * @param id ID of the article to save.
   * @param favorite Whether to save or delete. {@code true} to save; {@code false} to delete.
   * @return {@code true} if successful, {@code false} if otherwise.
   */
  public boolean saveFavorite(int id, boolean favorite) {
    return saveFavorite(get(id), favorite);
  }

  /**
   * Saves an {@link Article} object to the favorites database.
   * @param article Article object to save.
   * @return {@code true} if successful, {@code false} if otherwise.
   */
  public boolean saveFavorite(Article article) {
    return saveFavorite(article, true);
  }

  /**
   * Saves/Deletes an {@link Article} object to the favorites database.
   * @param article Article object to save.
   * @param favorite Whether to save or delete. {@code true} to save; {@code false} to delete.
   * @return {@code true} if successful, {@code false} if otherwise.
   */
  public boolean saveFavorite(Article article, boolean favorite) {
    long time = System.currentTimeMillis();
    log("Adding article " + article.getId() + " to favorites...");
    try {
      if (favorite)
        favoriteDatabase.add(article);
      else
        favoriteDatabase.delete(article);
    }
    catch (Exception e) {
      log("Error " + (favorite ? "saving article to" : "deleting article from") + " favorites database.", Log.ERROR);
    }

    log("Saving article " + article.getId() + " to favorites took " + (System.currentTimeMillis() - time) + "ms");
    return true;
  }

  /**
   * Clears the favorites database.
   */
  public void deleteAllFavorites() {
    long time = System.currentTimeMillis();
    log("Deleting all favorites...");
    favoriteDatabase.deleteAll();
    log("Deleting all favorites took " + (System.currentTimeMillis() - time) + "ms");
  }

  /**
   * Searches the FavoriteDatabase for the specified ID.
   * @param id Article ID which to search for.
   * @return {@code true} if database contains it or {@code false} if otherwise
   * or database not yet started.
   */
  public boolean containsFavorite(int id) {
    if(favoriteDatabase == null)
      return false;

    return favoriteDatabase.contains(id);
  }

  /**
   * Clears {@link Downloader} cache.
   * @return {@code true} if successfully cleared or {@code false} if otherwise.
   */
  public boolean clearCache() {
    return downloader.clearCache();
  }

  /**
   * Clears all PkRSS data including Downloader cache, all articles in Favorite
   * Database, and all articles marked as read.
   * @return {@code true} if successfully cleared or {@code false} if otherwise.
   */
  public boolean clearData() {
    if (!downloader.clearCache())
      return false;
    deleteAllFavorites();
    markAllRead(false);
    return true;
  }

  /**
   * @return The {@link Map} used for storing page states.
   */
  protected Map<String, Integer> getPageTracker() {
    return pageTracker;
  }

  /**
   * Inserts the passed list into the article map database.
   * This will be cleared once the instance dies.
   * @param url URL to associate this list with.
   * @param newArticles Article list to store.
   */
  private void insert(String url, List<Article> newArticles) {
    if(!articleMap.containsKey(url))
      articleMap.put(url, new ArrayList<Article>());

    List<Article> articleList = articleMap.get(url);
    articleList.addAll(newArticles);

    log("New size for " + url + " is " + articleList.size());
  }

  /**
   * Asynchronously loads read data.
   */
  private void getRead() {
    // Execute on background thread as we don't know how large this is
    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(Void... params) {
        int size = mPrefs.getInt("READ_ARRAY_SIZE", 0);
        boolean value;
        if(size < 1)
          return null;

        for(int i = 0, key; i < size; i++) {
          key = mPrefs.getInt("READ_ARRAY_KEY_" + i, 0);
          value = mPrefs.getBoolean("READ_ARRAY_VALUE_" + i, false);

          readList.put(key, value);
        }
        return null;
      }
    }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
  }

  /**
   * Asynchronously saves read data.
   */
  private void writeRead() {
    // Execute on background thread as we don't know how large this is
    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(Void... params) {
        // Get editor & basic variables
        SharedPreferences.Editor editor = mPrefs.edit();
        int size = readList.size();
        boolean value;

        editor.putInt("READ_ARRAY_SIZE", size);
        for(int i = 0, key; i < size; i++) {
          key = readList.keyAt(i);
          value = readList.get(key);

          editor.putInt("READ_ARRAY_KEY_" + i, key);
          editor.putBoolean("READ_ARRAY_VALUE_" + i, value);
        }
        editor.commit();

        return null;
      }
    }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    PkRSS pkRSS = (PkRSS) o;

    if (loggingEnabled != pkRSS.loggingEnabled) return false;
    if (!handler.equals(pkRSS.handler)) return false;
    if (!downloader.equals(pkRSS.downloader)) return false;
    if (!parser.equals(pkRSS.parser)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = (loggingEnabled ? 1 : 0);
    result = 31 * result + handler.hashCode();
    result = 31 * result + downloader.hashCode();
    result = 31 * result + parser.hashCode();
    return result;
  }

  protected final void log(String message) {
    log(TAG, message, Log.DEBUG);
  }

  protected final void log(String tag, String message) {
    log(tag, message, Log.DEBUG);
  }

  protected final void log(String message, int type) {
    log(TAG, message, type);
  }

  protected final void log(String tag, String message, int type) {
    if(!loggingEnabled)
      return;

    switch(type) {
      case Log.VERBOSE:
        Log.v(tag, message);
        break;
      case Log.DEBUG:
        Log.d(tag, message);
        break;
      case Log.INFO:
        Log.i(tag, message);
        break;
      case Log.WARN:
        Log.w(tag, message);
        break;
      case Log.ERROR:
        Log.e(tag, message);
        break;
      case Log.ASSERT:
      default:
        Log.wtf(tag, message);
        break;
    }
  }

  /** Fluent API for creating {@link PkRSS} instances. */
  public static class Builder {
    private final Context context;
    private CallbackHandler handler;
    private Downloader downloader;
    private Parser parser;
    private boolean loggingEnabled;

    /**
     * Start building a new {@link PkRSS} instance.
     */
    public Builder(Context context) {
      if (context == null)
        throw new IllegalArgumentException("Context must not be null!");
      this.context = context.getApplicationContext();
    }

    /**
     * Specifies the thread for which to execute callbacks on.
     * Setting this to null executes callbacks on the same thread in which the request was called. <br />
     * <b>Default: </b> {@code null}
     */
    public Builder handler(Handler handler) {
      this.handler = new CallbackHandler(handler);
      return this;
    }

    /**
     * Specifies a custom {@link Downloader} Object for which to load data with. <br />
     * <b>Default: </b> Either {@link DefaultDownloader} or {@link OkHttpDownloader} (See more... {@link Utils#createDefaultDownloader(Context)})
     */
    public Builder downloader(Downloader downloader) {
      this.downloader = downloader;
      return this;
    }

    /**
     * Specifies a custom {@link Parser} Object for which to parse data with. <br />
     * <b>Default: </b> {@link Rss2Parser}
     */
    public Builder parser(Parser parser) {
      this.parser = parser;
      return this;
    }

    /**
     * Toggle whether debug logging is enabled.
     * <b>Default: </b> {@code false}
     */
    public Builder loggingEnabled(boolean enabled) {
      this.loggingEnabled = enabled;
      return this;
    }

    /**
     * Creates a {@link PkRSS} instance.
     */
    public PkRSS build() {
      if(parser == null)
        parser = new Rss2Parser();

      if(downloader == null)
        downloader = Utils.createDefaultDownloader(context);

      if(handler == null)
        handler = new CallbackHandler();

      return new PkRSS(context, handler, downloader, parser, loggingEnabled);
    }

    /**
     * Creates a {@link PkRSS} instance and assigns it to the
     * global {@link PkRSS#singleton} instance if it doesn't already exists.
     */
    public void buildSingleton() {
      if(singleton == null)
        singleton = build();
      else
        Log.e(TAG, "Cannot buildSingleton(), a singleton already exists! Ignoring request...");
    }

    /**
     * <b><i>Use wisely!</i></b>
     * <p>
     * Creates a {@link PkRSS} instance and replaces the existing
     * global {@link PkRSS#singleton} instance if the hash codes differ.
     * <p>
     * <i>Do NOT use this unless you really have to!</i>
     */
    public void replaceSingleton() {
      PkRSS newInstance = build();
      if(singleton == null || !newInstance.equals(singleton)) {
        singleton = newInstance;
        Log.d(TAG, "Replaced global singleton instance.");
      }
      else
        Log.e(TAG, "Could not replace singleton instance. Same instance already exists.");
    }
  }
}




Java Source Code List

com.pkmmte.pkrss.ApplicationTest.java
com.pkmmte.pkrss.Article.java
com.pkmmte.pkrss.CallbackHandler.java
com.pkmmte.pkrss.Callback.java
com.pkmmte.pkrss.Category.java
com.pkmmte.pkrss.Enclosure.java
com.pkmmte.pkrss.FavoriteDatabase.java
com.pkmmte.pkrss.PkRSS.java
com.pkmmte.pkrss.RequestCreator.java
com.pkmmte.pkrss.Request.java
com.pkmmte.pkrss.Utils.java
com.pkmmte.pkrss.downloader.DefaultDownloader.java
com.pkmmte.pkrss.downloader.Downloader.java
com.pkmmte.pkrss.downloader.OkHttpDownloader.java
com.pkmmte.pkrss.parser.AtomParser.java
com.pkmmte.pkrss.parser.Parser.java
com.pkmmte.pkrss.parser.Rss2Parser.java