public long getMaxVisible() {
   final long maxVisible = mEstimateVisible ? mMaxEstimate : mMax;
   if (maxVisible <= 0 && mStats != null) {
     // haven't generated path yet; fall back to raw data
     final NetworkStatsHistory.Entry entry = mStats.getValues(mStart, mEnd, null);
     return entry.rxBytes + entry.txBytes;
   } else {
     return maxVisible;
   }
 }
 private static void assertValues(
     NetworkStatsHistory stats,
     long start,
     long end,
     long rxBytes,
     long rxPackets,
     long txBytes,
     long txPackets,
     int operations) {
   final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null);
   assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
   assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
   assertEquals("unexpected txBytes", txBytes, entry.txBytes);
   assertEquals("unexpected txPackets", txPackets, entry.txPackets);
   assertEquals("unexpected operations", operations, entry.operations);
 }
  /**
   * Erase any existing {@link Path} and generate series outline based on currently bound {@link
   * NetworkStatsHistory} data.
   */
  private void generatePath() {
    if (LOGD) Log.d(TAG, "generatePath()");

    mMax = 0;
    mPathStroke.reset();
    mPathFill.reset();
    mPathEstimate.reset();
    mPathValid = true;

    // bail when not enough stats to render
    if (mStats == null || mStats.size() < 2) {
      return;
    }

    final int width = getWidth();
    final int height = getHeight();

    boolean started = false;
    float lastX = 0;
    float lastY = height;
    long lastTime = mHoriz.convertToValue(lastX);

    // move into starting position
    mPathStroke.moveTo(lastX, lastY);
    mPathFill.moveTo(lastX, lastY);

    // TODO: count fractional data from first bucket crossing start;
    // currently it only accepts first full bucket.

    long totalData = 0;

    NetworkStatsHistory.Entry entry = null;

    final int start = mStats.getIndexBefore(mStart);
    final int end = mStats.getIndexAfter(mEnd);
    for (int i = start; i <= end; i++) {
      entry = mStats.getValues(i, entry);

      final long startTime = entry.bucketStart;
      final long endTime = startTime + entry.bucketDuration;

      final float startX = mHoriz.convertToPoint(startTime);
      final float endX = mHoriz.convertToPoint(endTime);

      // skip until we find first stats on screen
      if (endX < 0) continue;

      // increment by current bucket total
      totalData += entry.rxBytes + entry.txBytes;

      final float startY = lastY;
      final float endY = mVert.convertToPoint(totalData);

      if (lastTime != startTime) {
        // gap in buckets; line to start of current bucket
        mPathStroke.lineTo(startX, startY);
        mPathFill.lineTo(startX, startY);
      }

      // always draw to end of current bucket
      mPathStroke.lineTo(endX, endY);
      mPathFill.lineTo(endX, endY);

      lastX = endX;
      lastY = endY;
      lastTime = endTime;
    }

    // when data falls short, extend to requested end time
    if (lastTime < mEndTime) {
      lastX = mHoriz.convertToPoint(mEndTime);

      mPathStroke.lineTo(lastX, lastY);
      mPathFill.lineTo(lastX, lastY);
    }

    if (LOGD) {
      final RectF bounds = new RectF();
      mPathFill.computeBounds(bounds, true);
      Log.d(
          TAG,
          "onLayout() rendered with bounds=" + bounds.toString() + " and totalData=" + totalData);
    }

    // drop to bottom of graph from current location
    mPathFill.lineTo(lastX, height);
    mPathFill.lineTo(0, height);

    mMax = totalData;

    // build estimated data
    mPathEstimate.moveTo(lastX, lastY);

    final long now = System.currentTimeMillis();
    final long bucketDuration = mStats.getBucketDuration();

    // long window is average over two weeks
    entry = mStats.getValues(lastTime - WEEK_IN_MILLIS * 2, lastTime, now, entry);
    final long longWindow = (entry.rxBytes + entry.txBytes) * bucketDuration / entry.bucketDuration;

    long futureTime = 0;
    while (lastX < width) {
      futureTime += bucketDuration;

      // short window is day average last week
      final long lastWeekTime = lastTime - WEEK_IN_MILLIS + (futureTime % WEEK_IN_MILLIS);
      entry = mStats.getValues(lastWeekTime - DAY_IN_MILLIS, lastWeekTime, now, entry);
      final long shortWindow =
          (entry.rxBytes + entry.txBytes) * bucketDuration / entry.bucketDuration;

      totalData += (longWindow * 7 + shortWindow * 3) / 10;

      lastX = mHoriz.convertToPoint(lastTime + futureTime);
      lastY = mVert.convertToPoint(totalData);

      mPathEstimate.lineTo(lastX, lastY);
    }

    mMaxEstimate = totalData;

    invalidate();
  }