@Override
  public void handleItemList(IItemListHandler handler, long syncTime)
      throws IOException, ReaderException {
    // http://www.google.com/reader/api/0/stream/contents/user%2F-%2Fstate%2Fcom.google%2Fread?client=newsplus&output=json&ck=1276066665822&n=20&r=n
    Reader in = null;
    try {
      long startTime = handler.startTime();
      in = readStreamContents(syncTime, startTime, handler, null);

      String continuation = parseItemList(in, handler);

      int limit = handler.limit();
      int max = limit > SYNC_MAX_ITEMS ? SYNC_MAX_ITEMS : limit;
      int count = 1; // continuation count
      while ((limit == 0 || limit > max * count)
          && handler.continuation()
          && continuation != null
          && continuation.length() > 0) {
        in.close();
        in = readStreamContents(syncTime, startTime, handler, continuation);
        continuation = parseItemList(in, handler);
        count++;
      }
    } catch (JsonParseException e) {
      e.printStackTrace();
      throw new ReaderException("data parse error", e);
    } catch (RemoteException e) {
      e.printStackTrace();
      throw new ReaderException("remote connection error", e);
    } finally {
      if (in != null) in.close(); // ensure resources get cleaned up timely
    }
  }
  // NOTE: /api/0/stream/contents
  private Reader readStreamContents(
      long syncTime, long startTime, IItemListHandler handler, String continuation)
      throws IOException, ReaderException, RemoteException {
    initAuth();

    StringBuilder buff = new StringBuilder(URL_API_STREAM_CONTENTS.length() + 128);
    buff.append(URL_API_STREAM_CONTENTS);
    String stream = handler.stream();
    if (stream != null) {
      if (stream.equals(STATE_READING_LIST)) stream = STATE_GOOGLE_READING_LIST;
      else if (stream.equals(STATE_STARRED)) stream = STATE_GOOGLE_STARRED;
      buff.append("/").append(Utils.encode(stream));
    }
    buff.append("?client=newsplus&ck=").append(syncTime);
    if (handler.excludeRead()) {
      buff.append("&xt=").append(STATE_GOOGLE_READ);
    }
    if (continuation != null && continuation.length() > 0) {
      buff.append("&c=").append(continuation);
    }
    if (startTime > 0) {
      buff.append("&ot=").append(startTime);
    }
    int limit = handler.limit();
    limit = (limit > SYNC_MAX_ITEMS || limit == 0) ? SYNC_MAX_ITEMS : limit;
    if (limit > 0) {
      // google only allows max 1000 at once
      buff.append("&n=").append(limit > SYNC_MAX_ITEMS ? SYNC_MAX_ITEMS : limit);
    }
    buff.append("&r=").append(handler.newestFirst() ? "n" : "o");

    return doGetReader(buff.toString());
  }
  private String parseItemList(Reader in, IItemListHandler handler)
      throws JsonParseException, IOException, RemoteException {
    JsonFactory f = new JsonFactory();
    JsonParser jp = f.createParser(in);

    long length = 0;
    String currName;
    String mediaUrl = null;
    String mediaType = null;

    IItem entry = null;
    String continuation = null;
    List<IItem> itemList = new ArrayList<IItem>();

    List<String> excludedSubs = handler.excludedStreams(); // excluded subscriptions

    jp.nextToken(); // will return JsonToken.START_OBJECT (verify?)
    while (jp.nextToken() != JsonToken.END_OBJECT) {

      currName = jp.getCurrentName();
      jp.nextToken(); // move to value, or START_OBJECT/START_ARRAY
      if ("continuation".equals(currName)) {
        continuation = jp.getText();
      } else if ("items".equals(currName)) { // contains an object
        // start items
        while (jp.nextToken() != JsonToken.END_ARRAY) {
          // request stop
          //					if (handler.requestStop()) throw new JsonParseException(null, null);

          if (jp.getCurrentToken() == JsonToken.START_OBJECT) {
            entry = new IItem();
          } else if (jp.getCurrentToken() == JsonToken.END_OBJECT) {
            if (entry != null && entry.uid.length() > 0) {
              if (length + entry.getLength() > MAX_TRANSACTION_LENGTH) {
                handler.items(itemList, STRATEGY_INSERT_DEFAULT);
                itemList.clear();
                length = 0;
              }

              itemList.add(entry);
              length += entry.getLength();
            }

            if (itemList.size() % 200 == 0
                || length
                    > MAX_TRANSACTION_LENGTH) { // avoid TransactionTooLargeException, android only
              // allows 1mb
              handler.items(itemList, STRATEGY_INSERT_DEFAULT);
              itemList.clear();
              length = 0;
            }
            entry = null;
          }

          currName = jp.getCurrentName();
          if (currName == null || entry == null) continue;

          jp.nextToken(); // move to value
          if (currName.equals("id")) {
            entry.uid = stripItemUid(jp.getText());
          } else if (currName.equals("crawlTimeMsec")) {
            entry.updatedTime = Long.valueOf(jp.getText()) / 1000;
          } else if (currName.equals("title")) {
            entry.title = Utils.stripTags(unEscapeEntities(jp.getText()), true);
          } else if (currName.equals("categories")) {
            while (jp.nextToken() != JsonToken.END_ARRAY) {
              String category = jp.getText();
              if (category != null && addUserLabel(category, entry)) {
                entry.addTag(category);
              }
              if (category != null && category.endsWith("/state/com.google/read")) {
                entry.read = true;
              }
            }
          } else if (currName.equals("published")) {
            entry.publishedTime = jp.getLongValue();
          } else if (currName.equals("alternate")) {
            while (jp.nextToken() != JsonToken.END_ARRAY) {
              currName = jp.getCurrentName();
              if (currName == null) continue;
              jp.nextToken();
              if (currName.equals("href")) {
                entry.link = jp.getText();
              } else {
                jp.skipChildren();
              }
            }
          } else if (currName.equals("enclosure")) {
            while (jp.nextToken() != JsonToken.END_ARRAY) {
              currName = jp.getCurrentName();
              if (currName == null) continue;
              jp.nextToken();
              if (currName.equals("href")) {
                mediaUrl = jp.getText();
              } else if (currName.equals("type")) {
                mediaType = jp.getText();
                if (mediaType.startsWith("image")) {
                  entry.addImage(mediaUrl, mediaType);
                } else if (mediaType.startsWith("video")) {
                  entry.addVideo(mediaUrl, mediaType);
                } else if (mediaType.startsWith("audio")) {
                  entry.addAudio(mediaUrl, mediaType);
                }

                mediaUrl = null;
                mediaType = null;
              } else {
                jp.skipChildren();
              }
            }
          } else if (currName.equals("summary") || currName.equals("content")) {
            while (jp.nextToken() != JsonToken.END_OBJECT) {
              currName = jp.getCurrentName();
              if (currName == null) continue;
              jp.nextToken();
              if (currName.equals("content")) {
                entry.content = unEscapeEntities(jp.getText());
              } else {
                jp.skipChildren();
              }
            }
          } else if (currName.equals("author")) {
            entry.author = jp.getText();
          } else if (currName.equals("origin")) {
            while (jp.nextToken() != JsonToken.END_OBJECT) {
              currName = jp.getCurrentName();
              if (currName == null) continue;
              jp.nextToken();
              if (currName.equals("streamId")) {
                String streamId = jp.getText();
                if (streamId != null
                    && (excludedSubs == null || !excludedSubs.contains(streamId))) {
                  entry.subUid = streamId;
                } else entry = null;
              } else {
                jp.skipChildren();
              }
            }
          } else {
            jp.skipChildren();
          }
        }

        handler.items(itemList, STRATEGY_INSERT_DEFAULT);
        itemList.clear();

      } else {
        jp.skipChildren();
      }
    }

    return continuation;
  }