Add histogram metrics for video playback freezes

Bug: 234833109
Test: atest VideoRenderQualityTracker_test
Change-Id: I5e3a8b02336bbcbe639aad37f0914ba0f98d8d44
diff --git a/media/libmediametrics/MediaMetrics.cpp b/media/libmediametrics/MediaMetrics.cpp
index a3c2f1a..2240223 100644
--- a/media/libmediametrics/MediaMetrics.cpp
+++ b/media/libmediametrics/MediaMetrics.cpp
@@ -86,6 +86,11 @@
     if (item != NULL) item->setRate(attr, count, duration);
 }
 
+void mediametrics_setString(mediametrics_handle_t handle, attr_t attr,
+                                 const std::string &string) {
+    mediametrics_setCString(handle, attr, string.c_str());
+}
+
 void mediametrics_setCString(mediametrics_handle_t handle, attr_t attr,
                                  const char *value) {
     Item *item = (Item *) handle;
@@ -152,6 +157,14 @@
     return item->getRate(attr, count, duration, rate);
 }
 
+bool mediametrics_getString(mediametrics_handle_t handle, attr_t attr,
+                                 std::string *string) {
+    Item *item = (Item *) handle;
+    if (item == NULL) return false;
+
+    return item->getString(attr, string);
+}
+
 // NB: caller owns the string that comes back, is responsible for freeing it
 bool mediametrics_getCString(mediametrics_handle_t handle, attr_t attr,
                                  char **value) {
diff --git a/media/libmediametrics/include/media/MediaMetrics.h b/media/libmediametrics/include/media/MediaMetrics.h
index 76abe86..58612a3 100644
--- a/media/libmediametrics/include/media/MediaMetrics.h
+++ b/media/libmediametrics/include/media/MediaMetrics.h
@@ -50,7 +50,7 @@
 void mediametrics_setRate(mediametrics_handle_t handle, attr_t attr,
                           int64_t count, int64_t duration);
 void mediametrics_setCString(mediametrics_handle_t handle, attr_t attr,
-                            const char * value);
+                             const char * value);
 
 // fused get/add/set; if attr wasn't there, it's a simple set.
 // these do not provide atomicity or mutual exclusion, only simpler code sequences.
@@ -95,4 +95,11 @@
 
 __END_DECLS
 
+#ifdef __cplusplus
+#include <string>
+void mediametrics_setString(mediametrics_handle_t handle, attr_t attr,
+                            const std::string &value);
+bool mediametrics_getString(mediametrics_handle_t handle, attr_t attr, std::string *value);
+#endif // __cplusplus
+
 #endif
diff --git a/media/libmediametrics/include/media/MediaMetricsItem.h b/media/libmediametrics/include/media/MediaMetricsItem.h
index de56665..03834d4 100644
--- a/media/libmediametrics/include/media/MediaMetricsItem.h
+++ b/media/libmediametrics/include/media/MediaMetricsItem.h
@@ -1048,6 +1048,9 @@
         }
         return true;
     }
+    bool getString(const char *key, std::string *value) const {
+        return get(key, value);
+    }
     // Caller owns the returned string
     bool getCString(const char *key, char **value) const {
         std::string s;
@@ -1057,9 +1060,6 @@
         }
         return false;
     }
-    bool getString(const char *key, std::string *value) const {
-        return get(key, value);
-    }
 
     const Prop::Elem* get(const char *key) const {
         const Prop *prop = findProp(key);
diff --git a/media/libstagefright/MediaCodec.cpp b/media/libstagefright/MediaCodec.cpp
index 435f547..939f6d6 100644
--- a/media/libstagefright/MediaCodec.cpp
+++ b/media/libstagefright/MediaCodec.cpp
@@ -206,6 +206,15 @@
 static const char *kCodecFramerateDesired = "android.media.mediacodec.framerate.desired";
 static const char *kCodecFramerateActual = "android.media.mediacodec.framerate.actual";
 
+static const char *kCodecFreezeCount = "android.media.mediacodec.freeze.count";
+static const char *kCodecFreezeDurationAverage = "android.media.mediacodec.freeze.duration.average";
+static const char *kCodecFreezeDurationMax = "android.media.mediacodec.freeze.duration.max";
+static const char *kCodecFreezeDurationHistogram =
+        "android.media.mediacodec.freeze.duration.histogram";
+static const char *kCodecFreezeDistanceAverage = "android.media.mediacodec.freeze.distance.average";
+static const char *kCodecFreezeDistanceHistogram =
+        "android.media.mediacodec.freeze.distance.histogram";
+
 /* -1: shaper disabled
    >=0: number of fields changed */
 static const char *kCodecShapingEnhanced = "android.media.mediacodec.shaped";
@@ -1114,6 +1123,18 @@
             mediametrics_setDouble(mMetricsHandle, kCodecFramerateDesired, m.desiredFrameRate);
             mediametrics_setDouble(mMetricsHandle, kCodecFramerateActual, m.actualFrameRate);
         }
+        if (m.freezeDurationMsHistogram.getCount() >= 1) {
+            const MediaHistogram &histogram = m.freezeDurationMsHistogram;
+            mediametrics_setInt64(mMetricsHandle, kCodecFreezeCount, histogram.getCount());
+            mediametrics_setInt64(mMetricsHandle, kCodecFreezeDurationAverage, histogram.getAvg());
+            mediametrics_setInt64(mMetricsHandle, kCodecFreezeDurationMax, histogram.getMax());
+            mediametrics_setString(mMetricsHandle, kCodecFreezeDurationHistogram, histogram.emit());
+        }
+        if (m.freezeDistanceMsHistogram.getCount() >= 1) {
+            const MediaHistogram &histogram = m.freezeDistanceMsHistogram;
+            mediametrics_setInt64(mMetricsHandle, kCodecFreezeDistanceAverage, histogram.getAvg());
+            mediametrics_setString(mMetricsHandle, kCodecFreezeDistanceHistogram, histogram.emit());
+        }
     }
 
     if (mLatencyHist.getCount() != 0 ) {
diff --git a/media/libstagefright/MediaHistogram.cpp b/media/libstagefright/MediaHistogram.cpp
index 396d90e..3dd67b8 100644
--- a/media/libstagefright/MediaHistogram.cpp
+++ b/media/libstagefright/MediaHistogram.cpp
@@ -21,57 +21,104 @@
 
 #include <assert.h>
 #include <inttypes.h>
-#include <stdint.h>
+#include <sstream>
+#include <stdio.h>
 
 namespace android {
 
+
+MediaHistogram::MediaHistogram() {
+    mBuckets = nullptr;
+    mBucketLimits = nullptr;
+    mBucketCount = 0;
+    mFloor = mCeiling = mWidth = 0;
+    mBelow = 0;
+    mAbove = 0;
+    mSum = 0;
+    mCount = 0;
+    mMin = INT64_MAX;
+    mMax = INT64_MIN;
+}
+
+void MediaHistogram::clear() {
+    if (mBuckets != nullptr) {
+        free(mBuckets);
+        mBuckets = nullptr;
+    }
+    if (mBucketLimits != nullptr) {
+        free(mBucketLimits);
+        mBucketLimits = nullptr;
+    }
+    mBucketCount = 0;
+}
+
 bool MediaHistogram::setup(int bucketCount, int64_t width, int64_t floor)
 {
     if (bucketCount <= 0 || width <= 0) {
         return false;
     }
-
-    // get histogram buckets
-    if (bucketCount == mBucketCount && mBuckets != NULL) {
-        // reuse our existing buffer
-        memset(mBuckets, 0, sizeof(*mBuckets) * mBucketCount);
-    } else {
-        // get a new pre-zeroed buffer
-        int64_t *newbuckets = (int64_t *)calloc(bucketCount, sizeof (*mBuckets));
-        if (newbuckets == NULL) {
-            goto bad;
-        }
-        if (mBuckets != NULL)
-            free(mBuckets);
-        mBuckets = newbuckets;
+    if (!allocate(bucketCount, false)) {
+        return false;
     }
-
     mWidth = width;
     mFloor = floor;
     mCeiling = floor + bucketCount * width;
-    mBucketCount = bucketCount;
-
     mMin = INT64_MAX;
     mMax = INT64_MIN;
     mSum = 0;
     mCount = 0;
     mBelow = mAbove = 0;
-
     return true;
+}
 
-  bad:
-    if (mBuckets != NULL) {
-        free(mBuckets);
-        mBuckets = NULL;
+bool MediaHistogram::setup(const std::vector<int64_t> &bucketLimits) {
+    if (bucketLimits.size() <= 1) {
+        return false;
+    }
+    int bucketCount = bucketLimits.size() - 1;
+    if (!allocate(bucketCount, true)) {
+        return false;
     }
 
-    return false;
+    mWidth = -1;
+    mFloor = bucketLimits[0];
+    for (int i = 0; i < bucketCount; ++i) {
+        mBucketLimits[i] = bucketLimits[i + 1];
+    }
+    mCeiling = bucketLimits[bucketCount];
+    mMin = INT64_MAX;
+    mMax = INT64_MIN;
+    mSum = 0;
+    mCount = 0;
+    mBelow = mAbove = 0;
+    return true;
+}
+
+bool MediaHistogram::allocate(int bucketCount, bool withBucketLimits) {
+    assert(bucketCount > 0);
+    if (bucketCount != mBucketCount) {
+        clear();
+        mBuckets = (int64_t *) calloc(bucketCount, sizeof(*mBuckets));
+        if (mBuckets == nullptr) {
+            return false;
+        }
+    }
+    if (withBucketLimits && mBucketLimits == nullptr) {
+        mBucketLimits = (int64_t *) calloc(bucketCount, sizeof(*mBucketLimits));
+        if (mBucketLimits == nullptr) {
+            clear();
+            return false;
+        }
+    }
+    mBucketCount = bucketCount;
+    memset(mBuckets, 0, sizeof(*mBuckets) * mBucketCount);
+    return true;
 }
 
 void MediaHistogram::insert(int64_t sample)
 {
     // histogram is not set up
-    if (mBuckets == NULL) {
+    if (mBuckets == nullptr) {
         return;
     }
 
@@ -84,35 +131,43 @@
         mBelow++;
     } else if (sample >= mCeiling) {
         mAbove++;
-    } else {
+    } else if (mBucketLimits == nullptr) {
         int64_t slot = (sample - mFloor) / mWidth;
         assert(slot < mBucketCount);
         mBuckets[slot]++;
+    } else {
+        // A binary search might be more efficient for large number of buckets, but it is expected
+        // that there will never be a large amount of buckets, so keep the code simple.
+        for (int slot = 0; slot < mBucketCount; ++slot) {
+            if (sample < mBucketLimits[slot]) {
+                mBuckets[slot]++;
+                break;
+            }
+        }
     }
     return;
 }
 
-std::string MediaHistogram::emit()
+std::string MediaHistogram::emit() const
 {
-    std::string value;
-    char buffer[64];
-
-    // emits:  width,Below{bucket0,bucket1,...., bucketN}above
-    // unconfigured will emit: 0,0{}0
+    // emits:  floor,width,below{bucket0,bucket1,...., bucketN}above
+    // or.. emits:  below{bucket0,bucket1,...., bucketN}above
+    // unconfigured will emit: 0,0,0{}0
     // XXX: is this best representation?
-    snprintf(buffer, sizeof(buffer), "%" PRId64 ",%" PRId64 ",%" PRId64 "{",
-             mFloor, mWidth, mBelow);
-    value = buffer;
+    std::stringstream ss;
+    if (mBucketLimits == nullptr) {
+        ss << mFloor << "," << mWidth << "," << mBelow << "{";
+    } else {
+        ss << mBelow << "{";
+    }
     for (int i = 0; i < mBucketCount; i++) {
         if (i != 0) {
-            value = value + ",";
+            ss << ",";
         }
-        snprintf(buffer, sizeof(buffer), "%" PRId64, mBuckets[i]);
-        value = value + buffer;
+        ss << mBuckets[i];
     }
-    snprintf(buffer, sizeof(buffer), "}%" PRId64 , mAbove);
-    value = value + buffer;
-    return value;
+    ss << "}" << mAbove;
+    return ss.str();
 }
 
 } // android
diff --git a/media/libstagefright/VideoRenderQualityTracker.cpp b/media/libstagefright/VideoRenderQualityTracker.cpp
index 7224269..5f0e969 100644
--- a/media/libstagefright/VideoRenderQualityTracker.cpp
+++ b/media/libstagefright/VideoRenderQualityTracker.cpp
@@ -50,15 +50,21 @@
     // Allow for a tolerance of 200 milliseconds for determining if we moved forward in content time
     // because of frame drops for live content, or because the user is seeking.
     contentTimeAdvancedForLiveContentToleranceUs = 200 * 1000;
+
+    // Freeze configuration
+    freezeDurationMsHistogramBuckets = {1, 20, 40, 60, 80, 100, 120, 150, 175, 225, 300, 400, 500};
+    freezeDistanceMsHistogramBuckets = {0, 20, 100, 400, 1000, 2000, 3000, 4000, 8000, 15000, 30000,
+                                        60000};
 }
 
-VideoRenderQualityTracker::VideoRenderQualityTracker() :
-        mConfiguration(Configuration()) {
+VideoRenderQualityTracker::VideoRenderQualityTracker() : mConfiguration(Configuration()) {
+    configureHistograms(mMetrics, mConfiguration);
     resetForDiscontinuity();
 }
 
 VideoRenderQualityTracker::VideoRenderQualityTracker(const Configuration &configuration) :
         mConfiguration(configuration) {
+    configureHistograms(mMetrics, mConfiguration);
     resetForDiscontinuity();
 }
 
@@ -128,6 +134,7 @@
 void VideoRenderQualityTracker::resetForDiscontinuity() {
     mLastContentTimeUs = -1;
     mLastRenderTimeUs = -1;
+    mLastFreezeEndTimeUs = -1;
 
     // Don't worry about tracking frame rendering times from now up until playback catches up to the
     // discontinuity. While stuttering or freezing could be found in the next few frames, the impact
@@ -226,6 +233,29 @@
     updateFrameRate(mMetrics.contentFrameRate, mContentFrameDurationUs, mConfiguration);
     updateFrameRate(mMetrics.desiredFrameRate, mDesiredFrameDurationUs, mConfiguration);
     updateFrameRate(mMetrics.actualFrameRate, mActualFrameDurationUs, mConfiguration);
+
+    // If the previous frame was dropped, there was a freeze if we've already rendered a frame
+    if (mActualFrameDurationUs[1] == -1 && mLastRenderTimeUs != -1) {
+        processFreeze(actualRenderTimeUs, mLastRenderTimeUs, mLastFreezeEndTimeUs, mMetrics);
+        mLastFreezeEndTimeUs = actualRenderTimeUs;
+    }
+}
+
+void VideoRenderQualityTracker::processFreeze(int64_t actualRenderTimeUs, int64_t lastRenderTimeUs,
+                                              int64_t lastFreezeEndTimeUs,
+                                              VideoRenderQualityMetrics &m) {
+    int64_t freezeDurationMs = (actualRenderTimeUs - lastRenderTimeUs) / 1000;
+    m.freezeDurationMsHistogram.insert(freezeDurationMs);
+    if (lastFreezeEndTimeUs != -1) {
+        int64_t distanceSinceLastFreezeMs = (lastRenderTimeUs - lastFreezeEndTimeUs) / 1000;
+        m.freezeDistanceMsHistogram.insert(distanceSinceLastFreezeMs);
+    }
+}
+
+void VideoRenderQualityTracker::configureHistograms(VideoRenderQualityMetrics &m,
+                                                    const Configuration &c) {
+    m.freezeDurationMsHistogram.setup(c.freezeDurationMsHistogramBuckets);
+    m.freezeDistanceMsHistogram.setup(c.freezeDistanceMsHistogramBuckets);
 }
 
 int64_t VideoRenderQualityTracker::nowUs() {
diff --git a/media/libstagefright/include/media/stagefright/MediaHistogram.h b/media/libstagefright/include/media/stagefright/MediaHistogram.h
index 4883203..3c12901 100644
--- a/media/libstagefright/include/media/stagefright/MediaHistogram.h
+++ b/media/libstagefright/include/media/stagefright/MediaHistogram.h
@@ -18,31 +18,36 @@
 #define MEDIA_HISTOGRAM_H_
 
 #include <string>
+#include <vector>
 
 namespace android {
 
 class MediaHistogram {
     public:
-    MediaHistogram() : mFloor(0), mWidth(0), mBelow(0), mAbove(0),
-                    mMin(INT64_MAX), mMax(INT64_MIN), mSum(0), mCount(0),
-                    mBucketCount(0), mBuckets(NULL) {};
+    MediaHistogram();
     ~MediaHistogram() { clear(); };
-    void clear() { if (mBuckets != NULL) free(mBuckets); mBuckets = NULL; };
+    void clear();
     bool setup(int bucketCount, int64_t width, int64_t floor = 0);
+    bool setup(const std::vector<int64_t> &bucketLimits);
     void insert(int64_t sample);
     int64_t getMin() const { return mMin; }
     int64_t getMax() const { return mMax; }
     int64_t getCount() const { return mCount; }
     int64_t getSum() const { return mSum; }
     int64_t getAvg() const { return mSum / (mCount == 0 ? 1 : mCount); }
-    std::string emit();
+    std::string emit() const;
 private:
+    MediaHistogram(const MediaHistogram &); // disallow
+
+    bool allocate(int bucketCount, bool withBucketLimits);
+
     int64_t mFloor, mCeiling, mWidth;
     int64_t mBelow, mAbove;
     int64_t mMin, mMax, mSum, mCount;
 
     int mBucketCount;
     int64_t *mBuckets;
+    int64_t *mBucketLimits;
 };
 
 } // android
diff --git a/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h b/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
index bcec783..45c822a 100644
--- a/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
+++ b/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
@@ -22,6 +22,8 @@
 #include <list>
 #include <queue>
 
+#include <media/stagefright/MediaHistogram.h>
+
 namespace android {
 
 static const float FRAME_RATE_UNDETERMINED = -1.0f;
@@ -55,6 +57,12 @@
     // The frame rate as detected by looking at the actual render time, as returned by the system
     // post-render.
     float actualFrameRate;
+
+    // A histogram of the durations of freezes due to dropped/skipped frames.
+    MediaHistogram freezeDurationMsHistogram;
+
+    // A histogram of the durations between each freeze.
+    MediaHistogram freezeDistanceMsHistogram;
 };
 
 ///////////////////////////////////////////////////////
@@ -101,6 +109,10 @@
         // short, but the content frame duration is large, it is assumed the app is intentionally
         // seeking forward.
         int32_t contentTimeAdvancedForLiveContentToleranceUs;
+
+        // Freeze configuration
+        std::vector<int64_t> freezeDurationMsHistogramBuckets;
+        std::vector<int64_t> freezeDistanceMsHistogramBuckets;
     };
 
     VideoRenderQualityTracker();
@@ -160,6 +172,9 @@
         int64_t priorTimestampUs;
     };
 
+    // Configure histograms for the metrics.
+    static void configureHistograms(VideoRenderQualityMetrics &m, const Configuration &c);
+
     // The current time in microseconds.
     static int64_t nowUs();
 
@@ -178,6 +193,10 @@
     // occurring.
     static bool is32pulldown(const FrameDurationUs &durationUs, const Configuration &c);
 
+    // Process a frame freeze.
+    static void processFreeze(int64_t actualRenderTimeUs, int64_t lastRenderTimeUs,
+                              int64_t lastFreezeEndTimeUs, VideoRenderQualityMetrics &m);
+
     // Check to see if a discontinuity has occurred by examining the content time and the
     // app-desired render time. If so, reset some internal state.
     bool resetIfDiscontinuity(int64_t contentTimeUs, int64_t desiredRenderTimeUs);
@@ -204,6 +223,9 @@
     // The most recently processed timestamp referring to the wall clock time a frame was rendered.
     int64_t mLastRenderTimeUs;
 
+    // The most recent timestamp of the first frame rendered after the freeze.
+    int64_t mLastFreezeEndTimeUs;
+
     // Frames skipped at the end of playback shouldn't really be considered skipped, therefore keep
     // a list of the frames, and process them as skipped frames the next time a frame is rendered.
     std::list<int64_t> mPendingSkippedFrameContentTimeUsList;
diff --git a/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp b/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
index 56ec788..2249a8b 100644
--- a/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
+++ b/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
@@ -50,6 +50,15 @@
         }
     }
 
+    void render(int numFrames) {
+        for (int i = 0; i < numFrames; ++i) {
+            mVideoRenderQualityTracker.onFrameReleased(mMediaTimeUs);
+            mVideoRenderQualityTracker.onFrameRendered(mMediaTimeUs, mClockTimeNs);
+            mMediaTimeUs += mContentFrameDurationUs;
+            mClockTimeNs += mContentFrameDurationUs * 1000;
+        }
+    }
+
     void skip(int numFrames) {
         for (int i = 0; i < numFrames; ++i) {
             mVideoRenderQualityTracker.onFrameSkipped(mMediaTimeUs);
@@ -218,4 +227,68 @@
     EXPECT_EQ(h.getMetrics().actualFrameRate, FRAME_RATE_UNDETERMINED);
 }
 
+TEST_F(VideoRenderQualityTrackerTest, capturesFreezeDurationHistogram) {
+    Configuration c;
+    // +17 because freeze durations include the render time of the previous frame
+    c.freezeDurationMsHistogramBuckets = {2 * 17 + 17, 3 * 17 + 17, 6 * 17 + 17};
+    Helper h(17, c);
+    h.render(1);
+    h.drop(1); // below
+    h.render(1);
+    h.drop(3); // bucket 1
+    h.render(1);
+    h.drop(2); // bucket 0
+    h.render(1);
+    h.drop(4); // bucket 1
+    h.render(1);
+    h.drop(2); // bucket 0
+    h.render(1);
+    h.drop(5); // bucket 1
+    h.render(1);
+    h.drop(10); // above
+    h.render(1);
+    h.drop(15); // above
+    h.render(1);
+    EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.emit(), "1{2,3}2");
+    EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getCount(), 8);
+    // the smallest frame drop was 1, +17 because it includes the previous frame render time
+    EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getMin(), 1 * 17 + 17);
+    // the largest frame drop was 10, +17 because it includes the previous frame render time
+    EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getMax(), 15 * 17 + 17);
+    // total frame drop count, multiplied by 17, plus 17 for each occurrence, divided by occurrences
+    EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getAvg(), ((1 + 3 + 2 + 4 + 2 + 5 + 10 + 15)
+                                                                   * 17 + 8 * 17) / 8);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, capturesFreezeDistanceHistogram) {
+    Configuration c;
+    c.freezeDistanceMsHistogramBuckets = {1 * 17, 5 * 17, 6 * 17};
+    Helper h(17, c);
+    h.render(1);
+    h.drop(1);
+    h.render(5); // bucket 0
+    h.drop(3);
+    h.render(3); // bucket 0
+    h.drop(2);
+    h.render(9); // above
+    h.drop(5);
+    h.render(1); // below
+    h.drop(2);
+    h.render(6); // bucket 1
+    h.drop(4);
+    h.render(12); // above
+    h.drop(2);
+    h.render(1);
+    EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.emit(), "1{2,1}2");
+    EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getCount(), 6);
+    // the smallest render between drops was 1, -17 because the last frame rendered also froze
+    EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getMin(), 1 * 17 - 17);
+    // the largest render between drops was 12, -17 because the last frame rendered also froze
+    EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getMax(), 12 * 17 - 17);
+    // total render count between, multiplied by 17, minus 17 for each occurrence, divided by
+    // occurrences
+    EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getAvg(), ((5 + 3 + 9 + 1 + 6 + 12) * 17 -
+                                                                  6 * 17) / 6);
+}
+
 } // android