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