public static boolean followFeedByUrl(
      final String feedUrl, final boolean isAskingToFollow, final ActionListener actionListener) {
    if (TextUtils.isEmpty(feedUrl)) {
      if (actionListener != null) {
        actionListener.onActionResult(false);
      }
      return false;
    }

    ReaderBlog blogInfo = ReaderBlogTable.getFeedInfo(ReaderBlogTable.getFeedIdFromUrl(feedUrl));
    if (blogInfo != null) {
      return internalFollowFeed(
          blogInfo.feedId, blogInfo.getFeedUrl(), isAskingToFollow, actionListener);
    }

    updateFeedInfo(
        0,
        feedUrl,
        new UpdateBlogInfoListener() {
          @Override
          public void onResult(ReaderBlog blogInfo) {
            if (blogInfo != null) {
              internalFollowFeed(
                  blogInfo.feedId, blogInfo.getFeedUrl(), isAskingToFollow, actionListener);
            } else if (actionListener != null) {
              actionListener.onActionResult(false);
            }
          }
        });

    return true;
  }
  /*
   * follow editText entry as a url
   */
  private void addAsUrl(final String entry) {
    if (TextUtils.isEmpty(entry)) {
      return;
    }

    // normalize the url and prepend protocol if not supplied
    final String normUrl;
    if (!entry.contains("://")) {
      normUrl = UrlUtils.normalizeUrl("http://" + entry);
    } else {
      normUrl = UrlUtils.normalizeUrl(entry);
    }

    // if this isn't a valid URL, add original entry as a tag
    if (!URLUtil.isNetworkUrl(normUrl)) {
      addAsTag(entry);
      return;
    }

    // make sure it isn't already followed
    if (ReaderBlogTable.isFollowedBlogUrl(normUrl) || ReaderBlogTable.isFollowedFeedUrl(normUrl)) {
      ToastUtils.showToast(this, R.string.reader_toast_err_already_follow_blog);
      return;
    }

    // URL is valid, so follow it
    performAddUrl(normUrl);
  }
  public static boolean followFeedById(
      final long feedId, final boolean isAskingToFollow, final ActionListener actionListener) {
    ReaderBlog blogInfo = ReaderBlogTable.getFeedInfo(feedId);
    if (blogInfo != null) {
      return internalFollowFeed(
          blogInfo.feedId, blogInfo.getFeedUrl(), isAskingToFollow, actionListener);
    }

    updateFeedInfo(
        feedId,
        null,
        new UpdateBlogInfoListener() {
          @Override
          public void onResult(ReaderBlog blogInfo) {
            if (blogInfo != null) {
              internalFollowFeed(
                  blogInfo.feedId, blogInfo.getFeedUrl(), isAskingToFollow, actionListener);
            } else if (actionListener != null) {
              actionListener.onActionResult(false);
            }
          }
        });

    return true;
  }
  public static boolean followBlogById(
      final long blogId, final boolean isAskingToFollow, final ActionListener actionListener) {
    if (blogId == 0) {
      if (actionListener != null) {
        actionListener.onActionResult(false);
      }
      return false;
    }

    ReaderBlogTable.setIsFollowedBlogId(blogId, isAskingToFollow);
    ReaderPostTable.setFollowStatusForPostsInBlog(blogId, isAskingToFollow);

    if (isAskingToFollow) {
      AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.READER_BLOG_FOLLOWED, blogId);
    } else {
      AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.READER_BLOG_UNFOLLOWED, blogId);
    }

    final String actionName = (isAskingToFollow ? "follow" : "unfollow");
    final String path =
        "sites/" + blogId + "/follows/" + (isAskingToFollow ? "new" : "mine/delete");

    com.wordpress.rest.RestRequest.Listener listener =
        new RestRequest.Listener() {
          @Override
          public void onResponse(JSONObject jsonObject) {
            boolean success = isFollowActionSuccessful(jsonObject, isAskingToFollow);
            if (success) {
              AppLog.d(T.READER, "blog " + actionName + " succeeded");
            } else {
              AppLog.w(
                  T.READER,
                  "blog " + actionName + " failed - " + jsonToString(jsonObject) + " - " + path);
              localRevertFollowBlogId(blogId, isAskingToFollow);
            }
            if (actionListener != null) {
              actionListener.onActionResult(success);
            }
          }
        };
    RestRequest.ErrorListener errorListener =
        new RestRequest.ErrorListener() {
          @Override
          public void onErrorResponse(VolleyError volleyError) {
            AppLog.w(T.READER, "blog " + actionName + " failed with error");
            AppLog.e(T.READER, volleyError);
            localRevertFollowBlogId(blogId, isAskingToFollow);
            if (actionListener != null) {
              actionListener.onActionResult(false);
            }
          }
        };
    WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);

    return true;
  }
  /*
   * block a blog - result includes the list of posts that were deleted by the block so they
   * can be restored if the user undoes the block
   */
  public static BlockedBlogResult blockBlogFromReader(
      final long blogId, final ActionListener actionListener) {
    final BlockedBlogResult blockResult = new BlockedBlogResult();
    blockResult.blogId = blogId;
    blockResult.deletedPosts = ReaderPostTable.getPostsInBlog(blogId, 0, false);
    blockResult.wasFollowing = ReaderBlogTable.isFollowedBlog(blogId);

    ReaderPostTable.deletePostsInBlog(blogId);
    ReaderBlogTable.setIsFollowedBlogId(blogId, false);

    com.wordpress.rest.RestRequest.Listener listener =
        new RestRequest.Listener() {
          @Override
          public void onResponse(JSONObject jsonObject) {
            if (actionListener != null) {
              actionListener.onActionResult(true);
            }
          }
        };
    RestRequest.ErrorListener errorListener =
        new RestRequest.ErrorListener() {
          @Override
          public void onErrorResponse(VolleyError volleyError) {
            AppLog.e(T.READER, volleyError);
            ReaderPostTable.addOrUpdatePosts(null, blockResult.deletedPosts);
            if (blockResult.wasFollowing) {
              ReaderBlogTable.setIsFollowedBlogId(blogId, true);
            }
            if (actionListener != null) {
              actionListener.onActionResult(false);
            }
          }
        };

    AppLog.i(T.READER, "blocking blog " + blogId);
    String path = "me/block/sites/" + Long.toString(blogId) + "/new";
    WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);

    return blockResult;
  }
    @Override
    protected Boolean doInBackground(Void... params) {
      switch (getBlogType()) {
        case RECOMMENDED:
          // get recommended blogs using this offset, then start over with no offset
          // if there aren't any with this offset,
          int limit = ReaderConstants.READER_MAX_RECOMMENDED_TO_DISPLAY;
          int offset = UserPrefs.getReaderRecommendedBlogOffset();
          tmpRecommendedBlogs = ReaderBlogTable.getRecommendedBlogs(limit, offset);
          if (tmpRecommendedBlogs.size() == 0 && offset > 0) {
            UserPrefs.setReaderRecommendedBlogOffset(0);
            tmpRecommendedBlogs = ReaderBlogTable.getRecommendedBlogs(limit, 0);
          }
          return !mRecommendedBlogs.isSameList(tmpRecommendedBlogs);

        case FOLLOWED:
          tmpFollowedBlogs = ReaderBlogTable.getFollowedBlogs();
          return !mFollowedBlogs.isSameList(tmpFollowedBlogs);

        default:
          return false;
      }
    }
  private static void handleUpdateBlogInfoResponse(
      JSONObject jsonObject, UpdateBlogInfoListener infoListener) {
    if (jsonObject == null) {
      if (infoListener != null) {
        infoListener.onResult(null);
      }
      return;
    }

    ReaderBlog blogInfo = ReaderBlog.fromJson(jsonObject);
    ReaderBlogTable.addOrUpdateBlog(blogInfo);

    if (infoListener != null) {
      infoListener.onResult(blogInfo);
    }
  }
  @Override
  public View getView(final int position, View convertView, final ViewGroup parent) {
    final BlogViewHolder holder;
    if (convertView == null || !(convertView.getTag() instanceof BlogViewHolder)) {
      convertView = mInflater.inflate(R.layout.reader_listitem_blog, parent, false);
      holder = new BlogViewHolder(convertView);
      convertView.setTag(holder);
    } else {
      holder = (BlogViewHolder) convertView.getTag();
    }

    final long blogId;
    final String blogUrl;
    final boolean isFollowing;
    switch (getBlogType()) {
      case RECOMMENDED:
        final ReaderRecommendedBlog blog = (ReaderRecommendedBlog) getItem(position);
        blogId = blog.blogId;
        blogUrl = blog.getBlogUrl();
        isFollowing = ReaderBlogTable.isFollowedBlog(blogId, blogUrl);
        holder.txtTitle.setText(blog.getTitle());
        holder.txtDescription.setText(blog.getReason());
        holder.txtUrl.setText(UrlUtils.getDomainFromUrl(blogUrl));
        holder.imgBlog.setImageUrl(blog.getImageUrl(), WPNetworkImageView.ImageType.AVATAR);
        break;

      case FOLLOWED:
        final ReaderBlog blogInfo = (ReaderBlog) getItem(position);
        blogId = blogInfo.blogId;
        blogUrl = blogInfo.getUrl();
        isFollowing = blogInfo.isFollowing;
        String domain = UrlUtils.getDomainFromUrl(blogUrl);
        if (blogInfo.hasName()) {
          holder.txtTitle.setText(blogInfo.getName());
        } else {
          holder.txtTitle.setText(domain);
        }
        holder.txtUrl.setText(domain);
        break;

      default:
        blogId = 0;
        blogUrl = null;
        isFollowing = false;
        break;
    }

    // show the correct following status
    ReaderUtils.showFollowStatus(holder.txtFollow, isFollowing);
    holder.txtFollow.setOnClickListener(
        new View.OnClickListener() {
          @Override
          public void onClick(View v) {
            AniUtils.zoomAction(holder.txtFollow);
            changeFollowStatus(holder.txtFollow, position, !isFollowing);
          }
        });

    // show blog preview when view is clicked
    convertView.setOnClickListener(
        new View.OnClickListener() {
          @Override
          public void onClick(View v) {
            // make sure we have either the blog id or url
            if (blogId != 0 || !TextUtils.isEmpty(blogUrl)) {
              ReaderActivityLauncher.showReaderBlogPreview(getContext(), blogId, blogUrl);
            }
          }
        });

    return convertView;
  }
 private static void localRevertFollowFeedId(long feedId, boolean isAskingToFollow) {
   ReaderBlogTable.setIsFollowedFeedId(feedId, !isAskingToFollow);
   ReaderPostTable.setFollowStatusForPostsInFeed(feedId, !isAskingToFollow);
 }
 /*
  * called when a follow/unfollow fails, restores local data to previous state
  */
 private static void localRevertFollowBlogId(long blogId, boolean isAskingToFollow) {
   ReaderBlogTable.setIsFollowedBlogId(blogId, !isAskingToFollow);
   ReaderPostTable.setFollowStatusForPostsInBlog(blogId, !isAskingToFollow);
 }
  private static boolean internalFollowFeed(
      final long feedId,
      final String feedUrl,
      final boolean isAskingToFollow,
      final ActionListener actionListener) {
    // feedUrl is required
    if (TextUtils.isEmpty(feedUrl)) {
      if (actionListener != null) {
        actionListener.onActionResult(false);
      }
      return false;
    }

    if (feedId != 0) {
      ReaderBlogTable.setIsFollowedFeedId(feedId, isAskingToFollow);
      ReaderPostTable.setFollowStatusForPostsInFeed(feedId, isAskingToFollow);
    }

    if (isAskingToFollow) {
      AnalyticsTracker.track(AnalyticsTracker.Stat.READER_BLOG_FOLLOWED);
    } else {
      AnalyticsTracker.track(AnalyticsTracker.Stat.READER_BLOG_UNFOLLOWED);
    }

    final String actionName = (isAskingToFollow ? "follow" : "unfollow");
    final String path =
        "read/following/mine/"
            + (isAskingToFollow ? "new" : "delete")
            + "?url="
            + UrlUtils.urlEncode(feedUrl);

    com.wordpress.rest.RestRequest.Listener listener =
        new RestRequest.Listener() {
          @Override
          public void onResponse(JSONObject jsonObject) {
            boolean success = isFollowActionSuccessful(jsonObject, isAskingToFollow);
            if (success) {
              AppLog.d(T.READER, "feed " + actionName + " succeeded");
            } else {
              AppLog.w(
                  T.READER,
                  "feed " + actionName + " failed - " + jsonToString(jsonObject) + " - " + path);
              localRevertFollowFeedId(feedId, isAskingToFollow);
            }
            if (actionListener != null) {
              actionListener.onActionResult(success);
            }
          }
        };
    RestRequest.ErrorListener errorListener =
        new RestRequest.ErrorListener() {
          @Override
          public void onErrorResponse(VolleyError volleyError) {
            AppLog.w(T.READER, "feed " + actionName + " failed with error");
            AppLog.e(T.READER, volleyError);
            localRevertFollowFeedId(feedId, isAskingToFollow);
            if (actionListener != null) {
              actionListener.onActionResult(false);
            }
          }
        };
    WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);

    return true;
  }