Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.net.NetworkPolicy;
     22 import android.net.NetworkStatsHistory;
     23 import android.os.Handler;
     24 import android.os.Message;
     25 import android.text.Spannable;
     26 import android.text.SpannableStringBuilder;
     27 import android.text.TextUtils;
     28 import android.text.format.DateUtils;
     29 import android.util.AttributeSet;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 
     33 import com.android.internal.util.Objects;
     34 import com.android.settings.R;
     35 import com.android.settings.widget.ChartSweepView.OnSweepListener;
     36 
     37 /**
     38  * Specific {@link ChartView} that displays {@link ChartNetworkSeriesView} along
     39  * with {@link ChartSweepView} for inspection ranges and warning/limits.
     40  */
     41 public class ChartDataUsageView extends ChartView {
     42 
     43     private static final long KB_IN_BYTES = 1024;
     44     private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
     45     private static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
     46 
     47     private static final int MSG_UPDATE_AXIS = 100;
     48     private static final long DELAY_MILLIS = 250;
     49 
     50     private static final boolean LIMIT_SWEEPS_TO_VALID_DATA = false;
     51 
     52     private ChartGridView mGrid;
     53     private ChartNetworkSeriesView mSeries;
     54     private ChartNetworkSeriesView mDetailSeries;
     55 
     56     private NetworkStatsHistory mHistory;
     57 
     58     private ChartSweepView mSweepLeft;
     59     private ChartSweepView mSweepRight;
     60     private ChartSweepView mSweepWarning;
     61     private ChartSweepView mSweepLimit;
     62 
     63     private Handler mHandler;
     64 
     65     /** Current maximum value of {@link #mVert}. */
     66     private long mVertMax;
     67 
     68     public interface DataUsageChartListener {
     69         public void onInspectRangeChanged();
     70         public void onWarningChanged();
     71         public void onLimitChanged();
     72         public void requestWarningEdit();
     73         public void requestLimitEdit();
     74     }
     75 
     76     private DataUsageChartListener mListener;
     77 
     78     public ChartDataUsageView(Context context) {
     79         this(context, null, 0);
     80     }
     81 
     82     public ChartDataUsageView(Context context, AttributeSet attrs) {
     83         this(context, attrs, 0);
     84     }
     85 
     86     public ChartDataUsageView(Context context, AttributeSet attrs, int defStyle) {
     87         super(context, attrs, defStyle);
     88         init(new TimeAxis(), new InvertedChartAxis(new DataAxis()));
     89 
     90         mHandler = new Handler() {
     91             @Override
     92             public void handleMessage(Message msg) {
     93                 final ChartSweepView sweep = (ChartSweepView) msg.obj;
     94                 updateVertAxisBounds(sweep);
     95                 updateEstimateVisible();
     96 
     97                 // we keep dispatching repeating updates until sweep is dropped
     98                 sendUpdateAxisDelayed(sweep, true);
     99             }
    100         };
    101     }
    102 
    103     @Override
    104     protected void onFinishInflate() {
    105         super.onFinishInflate();
    106 
    107         mGrid = (ChartGridView) findViewById(R.id.grid);
    108         mSeries = (ChartNetworkSeriesView) findViewById(R.id.series);
    109         mDetailSeries = (ChartNetworkSeriesView) findViewById(R.id.detail_series);
    110         mDetailSeries.setVisibility(View.GONE);
    111 
    112         mSweepLeft = (ChartSweepView) findViewById(R.id.sweep_left);
    113         mSweepRight = (ChartSweepView) findViewById(R.id.sweep_right);
    114         mSweepLimit = (ChartSweepView) findViewById(R.id.sweep_limit);
    115         mSweepWarning = (ChartSweepView) findViewById(R.id.sweep_warning);
    116 
    117         // prevent sweeps from crossing each other
    118         mSweepLeft.setValidRangeDynamic(null, mSweepRight);
    119         mSweepRight.setValidRangeDynamic(mSweepLeft, null);
    120         mSweepWarning.setValidRangeDynamic(null, mSweepLimit);
    121         mSweepLimit.setValidRangeDynamic(mSweepWarning, null);
    122 
    123         // mark neighbors for checking touch events against
    124         mSweepLeft.setNeighbors(mSweepRight);
    125         mSweepRight.setNeighbors(mSweepLeft);
    126         mSweepLimit.setNeighbors(mSweepWarning, mSweepLeft, mSweepRight);
    127         mSweepWarning.setNeighbors(mSweepLimit, mSweepLeft, mSweepRight);
    128 
    129         mSweepLeft.addOnSweepListener(mHorizListener);
    130         mSweepRight.addOnSweepListener(mHorizListener);
    131         mSweepWarning.addOnSweepListener(mVertListener);
    132         mSweepLimit.addOnSweepListener(mVertListener);
    133 
    134         mSweepWarning.setDragInterval(5 * MB_IN_BYTES);
    135         mSweepLimit.setDragInterval(5 * MB_IN_BYTES);
    136 
    137         // TODO: make time sweeps adjustable through dpad
    138         mSweepLeft.setClickable(false);
    139         mSweepLeft.setFocusable(false);
    140         mSweepRight.setClickable(false);
    141         mSweepRight.setFocusable(false);
    142 
    143         // tell everyone about our axis
    144         mGrid.init(mHoriz, mVert);
    145         mSeries.init(mHoriz, mVert);
    146         mDetailSeries.init(mHoriz, mVert);
    147         mSweepLeft.init(mHoriz);
    148         mSweepRight.init(mHoriz);
    149         mSweepWarning.init(mVert);
    150         mSweepLimit.init(mVert);
    151 
    152         setActivated(false);
    153     }
    154 
    155     public void setListener(DataUsageChartListener listener) {
    156         mListener = listener;
    157     }
    158 
    159     public void bindNetworkStats(NetworkStatsHistory stats) {
    160         mSeries.bindNetworkStats(stats);
    161         mHistory = stats;
    162         updateVertAxisBounds(null);
    163         updateEstimateVisible();
    164         updatePrimaryRange();
    165         requestLayout();
    166     }
    167 
    168     public void bindDetailNetworkStats(NetworkStatsHistory stats) {
    169         mDetailSeries.bindNetworkStats(stats);
    170         mDetailSeries.setVisibility(stats != null ? View.VISIBLE : View.GONE);
    171         if (mHistory != null) {
    172             mDetailSeries.setEndTime(mHistory.getEnd());
    173         }
    174         updateVertAxisBounds(null);
    175         updateEstimateVisible();
    176         updatePrimaryRange();
    177         requestLayout();
    178     }
    179 
    180     public void bindNetworkPolicy(NetworkPolicy policy) {
    181         if (policy == null) {
    182             mSweepLimit.setVisibility(View.INVISIBLE);
    183             mSweepLimit.setValue(-1);
    184             mSweepWarning.setVisibility(View.INVISIBLE);
    185             mSweepWarning.setValue(-1);
    186             return;
    187         }
    188 
    189         if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
    190             mSweepLimit.setVisibility(View.VISIBLE);
    191             mSweepLimit.setEnabled(true);
    192             mSweepLimit.setValue(policy.limitBytes);
    193         } else {
    194             mSweepLimit.setVisibility(View.VISIBLE);
    195             mSweepLimit.setEnabled(false);
    196             mSweepLimit.setValue(-1);
    197         }
    198 
    199         if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
    200             mSweepWarning.setVisibility(View.VISIBLE);
    201             mSweepWarning.setValue(policy.warningBytes);
    202         } else {
    203             mSweepWarning.setVisibility(View.INVISIBLE);
    204             mSweepWarning.setValue(-1);
    205         }
    206 
    207         updateVertAxisBounds(null);
    208         requestLayout();
    209         invalidate();
    210     }
    211 
    212     /**
    213      * Update {@link #mVert} to both show data from {@link NetworkStatsHistory}
    214      * and controls from {@link NetworkPolicy}.
    215      */
    216     private void updateVertAxisBounds(ChartSweepView activeSweep) {
    217         final long max = mVertMax;
    218 
    219         long newMax = 0;
    220         if (activeSweep != null) {
    221             final int adjustAxis = activeSweep.shouldAdjustAxis();
    222             if (adjustAxis > 0) {
    223                 // hovering around upper edge, grow axis
    224                 newMax = max * 11 / 10;
    225             } else if (adjustAxis < 0) {
    226                 // hovering around lower edge, shrink axis
    227                 newMax = max * 9 / 10;
    228             } else {
    229                 newMax = max;
    230             }
    231         }
    232 
    233         // always show known data and policy lines
    234         final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue());
    235         final long maxSeries = Math.max(mSeries.getMaxVisible(), mDetailSeries.getMaxVisible());
    236         final long maxVisible = Math.max(maxSeries, maxSweep) * 12 / 10;
    237         final long maxDefault = Math.max(maxVisible, 2 * GB_IN_BYTES);
    238         newMax = Math.max(maxDefault, newMax);
    239 
    240         // only invalidate when vertMax actually changed
    241         if (newMax != mVertMax) {
    242             mVertMax = newMax;
    243 
    244             final boolean changed = mVert.setBounds(0L, newMax);
    245             mSweepWarning.setValidRange(0L, newMax);
    246             mSweepLimit.setValidRange(0L, newMax);
    247 
    248             if (changed) {
    249                 mSeries.invalidatePath();
    250                 mDetailSeries.invalidatePath();
    251             }
    252 
    253             mGrid.invalidate();
    254 
    255             // since we just changed axis, make sweep recalculate its value
    256             if (activeSweep != null) {
    257                 activeSweep.updateValueFromPosition();
    258             }
    259 
    260             // layout other sweeps to match changed axis
    261             // TODO: find cleaner way of doing this, such as requesting full
    262             // layout and making activeSweep discard its tracking MotionEvent.
    263             if (mSweepLimit != activeSweep) {
    264                 layoutSweep(mSweepLimit);
    265             }
    266             if (mSweepWarning != activeSweep) {
    267                 layoutSweep(mSweepWarning);
    268             }
    269         }
    270     }
    271 
    272     /**
    273      * Control {@link ChartNetworkSeriesView#setEstimateVisible(boolean)} based
    274      * on how close estimate comes to {@link #mSweepWarning}.
    275      */
    276     private void updateEstimateVisible() {
    277         final long maxEstimate = mSeries.getMaxEstimate();
    278 
    279         // show estimate when near warning/limit
    280         long interestLine = Long.MAX_VALUE;
    281         if (mSweepWarning.isEnabled()) {
    282             interestLine = mSweepWarning.getValue();
    283         } else if (mSweepLimit.isEnabled()) {
    284             interestLine = mSweepLimit.getValue();
    285         }
    286 
    287         if (interestLine < 0) {
    288             interestLine = Long.MAX_VALUE;
    289         }
    290 
    291         final boolean estimateVisible = (maxEstimate >= interestLine * 7 / 10);
    292         mSeries.setEstimateVisible(estimateVisible);
    293     }
    294 
    295     private OnSweepListener mHorizListener = new OnSweepListener() {
    296         /** {@inheritDoc} */
    297         public void onSweep(ChartSweepView sweep, boolean sweepDone) {
    298             updatePrimaryRange();
    299 
    300             // update detail list only when done sweeping
    301             if (sweepDone && mListener != null) {
    302                 mListener.onInspectRangeChanged();
    303             }
    304         }
    305 
    306         /** {@inheritDoc} */
    307         public void requestEdit(ChartSweepView sweep) {
    308             // ignored
    309         }
    310     };
    311 
    312     private void sendUpdateAxisDelayed(ChartSweepView sweep, boolean force) {
    313         if (force || !mHandler.hasMessages(MSG_UPDATE_AXIS, sweep)) {
    314             mHandler.sendMessageDelayed(
    315                     mHandler.obtainMessage(MSG_UPDATE_AXIS, sweep), DELAY_MILLIS);
    316         }
    317     }
    318 
    319     private void clearUpdateAxisDelayed(ChartSweepView sweep) {
    320         mHandler.removeMessages(MSG_UPDATE_AXIS, sweep);
    321     }
    322 
    323     private OnSweepListener mVertListener = new OnSweepListener() {
    324         /** {@inheritDoc} */
    325         public void onSweep(ChartSweepView sweep, boolean sweepDone) {
    326             if (sweepDone) {
    327                 clearUpdateAxisDelayed(sweep);
    328                 updateEstimateVisible();
    329 
    330                 if (sweep == mSweepWarning && mListener != null) {
    331                     mListener.onWarningChanged();
    332                 } else if (sweep == mSweepLimit && mListener != null) {
    333                     mListener.onLimitChanged();
    334                 }
    335             } else {
    336                 // while moving, kick off delayed grow/shrink axis updates
    337                 sendUpdateAxisDelayed(sweep, false);
    338             }
    339         }
    340 
    341         /** {@inheritDoc} */
    342         public void requestEdit(ChartSweepView sweep) {
    343             if (sweep == mSweepWarning && mListener != null) {
    344                 mListener.requestWarningEdit();
    345             } else if (sweep == mSweepLimit && mListener != null) {
    346                 mListener.requestLimitEdit();
    347             }
    348         }
    349     };
    350 
    351     @Override
    352     public boolean onTouchEvent(MotionEvent event) {
    353         if (isActivated()) return false;
    354         switch (event.getAction()) {
    355             case MotionEvent.ACTION_DOWN: {
    356                 return true;
    357             }
    358             case MotionEvent.ACTION_UP: {
    359                 setActivated(true);
    360                 return true;
    361             }
    362             default: {
    363                 return false;
    364             }
    365         }
    366     }
    367 
    368     public long getInspectStart() {
    369         return mSweepLeft.getValue();
    370     }
    371 
    372     public long getInspectEnd() {
    373         return mSweepRight.getValue();
    374     }
    375 
    376     public long getWarningBytes() {
    377         return mSweepWarning.getLabelValue();
    378     }
    379 
    380     public long getLimitBytes() {
    381         return mSweepLimit.getLabelValue();
    382     }
    383 
    384     private long getHistoryStart() {
    385         return mHistory != null ? mHistory.getStart() : Long.MAX_VALUE;
    386     }
    387 
    388     private long getHistoryEnd() {
    389         return mHistory != null ? mHistory.getEnd() : Long.MIN_VALUE;
    390     }
    391 
    392     /**
    393      * Set the exact time range that should be displayed, updating how
    394      * {@link ChartNetworkSeriesView} paints. Moves inspection ranges to be the
    395      * last "week" of available data, without triggering listener events.
    396      */
    397     public void setVisibleRange(long visibleStart, long visibleEnd) {
    398         final boolean changed = mHoriz.setBounds(visibleStart, visibleEnd);
    399         mGrid.setBounds(visibleStart, visibleEnd);
    400         mSeries.setBounds(visibleStart, visibleEnd);
    401         mDetailSeries.setBounds(visibleStart, visibleEnd);
    402 
    403         final long historyStart = getHistoryStart();
    404         final long historyEnd = getHistoryEnd();
    405 
    406         final long validStart = historyStart == Long.MAX_VALUE ? visibleStart
    407                 : Math.max(visibleStart, historyStart);
    408         final long validEnd = historyEnd == Long.MIN_VALUE ? visibleEnd
    409                 : Math.min(visibleEnd, historyEnd);
    410 
    411         if (LIMIT_SWEEPS_TO_VALID_DATA) {
    412             // prevent time sweeps from leaving valid data
    413             mSweepLeft.setValidRange(validStart, validEnd);
    414             mSweepRight.setValidRange(validStart, validEnd);
    415         } else {
    416             mSweepLeft.setValidRange(visibleStart, visibleEnd);
    417             mSweepRight.setValidRange(visibleStart, visibleEnd);
    418         }
    419 
    420         // default sweeps to last week of data
    421         final long halfRange = (visibleEnd + visibleStart) / 2;
    422         final long sweepMax = validEnd;
    423         final long sweepMin = Math.max(visibleStart, (sweepMax - DateUtils.WEEK_IN_MILLIS));
    424 
    425         mSweepLeft.setValue(sweepMin);
    426         mSweepRight.setValue(sweepMax);
    427 
    428         requestLayout();
    429         if (changed) {
    430             mSeries.invalidatePath();
    431             mDetailSeries.invalidatePath();
    432         }
    433 
    434         updateVertAxisBounds(null);
    435         updateEstimateVisible();
    436         updatePrimaryRange();
    437     }
    438 
    439     private void updatePrimaryRange() {
    440         final long left = mSweepLeft.getValue();
    441         final long right = mSweepRight.getValue();
    442 
    443         // prefer showing primary range on detail series, when available
    444         if (mDetailSeries.getVisibility() == View.VISIBLE) {
    445             mDetailSeries.setPrimaryRange(left, right);
    446             mSeries.setPrimaryRange(0, 0);
    447         } else {
    448             mSeries.setPrimaryRange(left, right);
    449         }
    450     }
    451 
    452     public static class TimeAxis implements ChartAxis {
    453         private static final long TICK_INTERVAL = DateUtils.DAY_IN_MILLIS * 7;
    454 
    455         private long mMin;
    456         private long mMax;
    457         private float mSize;
    458 
    459         public TimeAxis() {
    460             final long currentTime = System.currentTimeMillis();
    461             setBounds(currentTime - DateUtils.DAY_IN_MILLIS * 30, currentTime);
    462         }
    463 
    464         @Override
    465         public int hashCode() {
    466             return Objects.hashCode(mMin, mMax, mSize);
    467         }
    468 
    469         /** {@inheritDoc} */
    470         public boolean setBounds(long min, long max) {
    471             if (mMin != min || mMax != max) {
    472                 mMin = min;
    473                 mMax = max;
    474                 return true;
    475             } else {
    476                 return false;
    477             }
    478         }
    479 
    480         /** {@inheritDoc} */
    481         public boolean setSize(float size) {
    482             if (mSize != size) {
    483                 mSize = size;
    484                 return true;
    485             } else {
    486                 return false;
    487             }
    488         }
    489 
    490         /** {@inheritDoc} */
    491         public float convertToPoint(long value) {
    492             return (mSize * (value - mMin)) / (mMax - mMin);
    493         }
    494 
    495         /** {@inheritDoc} */
    496         public long convertToValue(float point) {
    497             return (long) (mMin + ((point * (mMax - mMin)) / mSize));
    498         }
    499 
    500         /** {@inheritDoc} */
    501         public long buildLabel(Resources res, SpannableStringBuilder builder, long value) {
    502             // TODO: convert to better string
    503             builder.replace(0, builder.length(), Long.toString(value));
    504             return value;
    505         }
    506 
    507         /** {@inheritDoc} */
    508         public float[] getTickPoints() {
    509             // tick mark for every week
    510             final int tickCount = (int) ((mMax - mMin) / TICK_INTERVAL);
    511             final float[] tickPoints = new float[tickCount];
    512             for (int i = 0; i < tickCount; i++) {
    513                 tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * (i + 1)));
    514             }
    515             return tickPoints;
    516         }
    517 
    518         /** {@inheritDoc} */
    519         public int shouldAdjustAxis(long value) {
    520             // time axis never adjusts
    521             return 0;
    522         }
    523     }
    524 
    525     public static class DataAxis implements ChartAxis {
    526         private long mMin;
    527         private long mMax;
    528         private float mSize;
    529 
    530         @Override
    531         public int hashCode() {
    532             return Objects.hashCode(mMin, mMax, mSize);
    533         }
    534 
    535         /** {@inheritDoc} */
    536         public boolean setBounds(long min, long max) {
    537             if (mMin != min || mMax != max) {
    538                 mMin = min;
    539                 mMax = max;
    540                 return true;
    541             } else {
    542                 return false;
    543             }
    544         }
    545 
    546         /** {@inheritDoc} */
    547         public boolean setSize(float size) {
    548             if (mSize != size) {
    549                 mSize = size;
    550                 return true;
    551             } else {
    552                 return false;
    553             }
    554         }
    555 
    556         /** {@inheritDoc} */
    557         public float convertToPoint(long value) {
    558             // derived polynomial fit to make lower values more visible
    559             final double normalized = ((double) value - mMin) / (mMax - mMin);
    560             final double fraction = Math.pow(
    561                     10, 0.36884343106175121463 * Math.log10(normalized) + -0.04328199452018252624);
    562             return (float) (fraction * mSize);
    563         }
    564 
    565         /** {@inheritDoc} */
    566         public long convertToValue(float point) {
    567             final double normalized = point / mSize;
    568             final double fraction = 1.3102228476089056629
    569                     * Math.pow(normalized, 2.7111774693164631640);
    570             return (long) (mMin + (fraction * (mMax - mMin)));
    571         }
    572 
    573         private static final Object sSpanSize = new Object();
    574         private static final Object sSpanUnit = new Object();
    575 
    576         /** {@inheritDoc} */
    577         public long buildLabel(Resources res, SpannableStringBuilder builder, long value) {
    578 
    579             final CharSequence unit;
    580             final long unitFactor;
    581             if (value < 1000 * MB_IN_BYTES) {
    582                 unit = res.getText(com.android.internal.R.string.megabyteShort);
    583                 unitFactor = MB_IN_BYTES;
    584             } else {
    585                 unit = res.getText(com.android.internal.R.string.gigabyteShort);
    586                 unitFactor = GB_IN_BYTES;
    587             }
    588 
    589             final double result = (double) value / unitFactor;
    590             final double resultRounded;
    591             final CharSequence size;
    592 
    593             if (result < 10) {
    594                 size = String.format("%.1f", result);
    595                 resultRounded = (unitFactor * Math.round(result * 10)) / 10;
    596             } else {
    597                 size = String.format("%.0f", result);
    598                 resultRounded = unitFactor * Math.round(result);
    599             }
    600 
    601             final int[] sizeBounds = findOrCreateSpan(builder, sSpanSize, "^1");
    602             builder.replace(sizeBounds[0], sizeBounds[1], size);
    603             final int[] unitBounds = findOrCreateSpan(builder, sSpanUnit, "^2");
    604             builder.replace(unitBounds[0], unitBounds[1], unit);
    605 
    606             return (long) resultRounded;
    607         }
    608 
    609         /** {@inheritDoc} */
    610         public float[] getTickPoints() {
    611             final long range = mMax - mMin;
    612             final long tickJump;
    613             if (range < 6 * GB_IN_BYTES) {
    614                 tickJump = 256 * MB_IN_BYTES;
    615             } else if (range < 12 * GB_IN_BYTES) {
    616                 tickJump = 512 * MB_IN_BYTES;
    617             } else {
    618                 tickJump = 1 * GB_IN_BYTES;
    619             }
    620 
    621             final int tickCount = (int) (range / tickJump);
    622             final float[] tickPoints = new float[tickCount];
    623             long value = mMin;
    624             for (int i = 0; i < tickPoints.length; i++) {
    625                 tickPoints[i] = convertToPoint(value);
    626                 value += tickJump;
    627             }
    628 
    629             return tickPoints;
    630         }
    631 
    632         /** {@inheritDoc} */
    633         public int shouldAdjustAxis(long value) {
    634             final float point = convertToPoint(value);
    635             if (point < mSize * 0.1) {
    636                 return -1;
    637             } else if (point > mSize * 0.85) {
    638                 return 1;
    639             } else {
    640                 return 0;
    641             }
    642         }
    643     }
    644 
    645     private static int[] findOrCreateSpan(
    646             SpannableStringBuilder builder, Object key, CharSequence bootstrap) {
    647         int start = builder.getSpanStart(key);
    648         int end = builder.getSpanEnd(key);
    649         if (start == -1) {
    650             start = TextUtils.indexOf(builder, bootstrap);
    651             end = start + bootstrap.length();
    652             builder.setSpan(key, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
    653         }
    654         return new int[] { start, end };
    655     }
    656 
    657 }
    658