• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onInspectRangeChanged()72         public void onInspectRangeChanged();
onWarningChanged()73         public void onWarningChanged();
onLimitChanged()74         public void onLimitChanged();
requestWarningEdit()75         public void requestWarningEdit();
requestLimitEdit()76         public void requestLimitEdit();
77     }
78 
79     private DataUsageChartListener mListener;
80 
ChartDataUsageView(Context context)81     public ChartDataUsageView(Context context) {
82         this(context, null, 0);
83     }
84 
ChartDataUsageView(Context context, AttributeSet attrs)85     public ChartDataUsageView(Context context, AttributeSet attrs) {
86         this(context, attrs, 0);
87     }
88 
ChartDataUsageView(Context context, AttributeSet attrs, int defStyle)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
onFinishInflate()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 
setListener(DataUsageChartListener listener)158     public void setListener(DataUsageChartListener listener) {
159         mListener = listener;
160     }
161 
bindNetworkStats(NetworkStatsHistory stats)162     public void bindNetworkStats(NetworkStatsHistory stats) {
163         mSeries.bindNetworkStats(stats);
164         mHistory = stats;
165         updateVertAxisBounds(null);
166         updateEstimateVisible();
167         updatePrimaryRange();
168         requestLayout();
169     }
170 
bindDetailNetworkStats(NetworkStatsHistory stats)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 
bindNetworkPolicy(NetworkPolicy policy)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      */
updateVertAxisBounds(ChartSweepView activeSweep)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      */
updateEstimateVisible()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 
sendUpdateAxisDelayed(ChartSweepView sweep, boolean force)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 
clearUpdateAxisDelayed(ChartSweepView sweep)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
onTouchEvent(MotionEvent event)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 
getInspectStart()371     public long getInspectStart() {
372         return mSweepLeft.getValue();
373     }
374 
getInspectEnd()375     public long getInspectEnd() {
376         return mSweepRight.getValue();
377     }
378 
getWarningBytes()379     public long getWarningBytes() {
380         return mSweepWarning.getLabelValue();
381     }
382 
getLimitBytes()383     public long getLimitBytes() {
384         return mSweepLimit.getLabelValue();
385     }
386 
getHistoryStart()387     private long getHistoryStart() {
388         return mHistory != null ? mHistory.getStart() : Long.MAX_VALUE;
389     }
390 
getHistoryEnd()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      */
setVisibleRange(long visibleStart, long visibleEnd)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 
updatePrimaryRange()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 
TimeAxis()462         public TimeAxis() {
463             final long currentTime = System.currentTimeMillis();
464             setBounds(currentTime - DateUtils.DAY_IN_MILLIS * 30, currentTime);
465         }
466 
467         @Override
hashCode()468         public int hashCode() {
469             return Objects.hashCode(mMin, mMax, mSize);
470         }
471 
472         @Override
setBounds(long min, long max)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
setSize(float size)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
convertToPoint(long value)494         public float convertToPoint(long value) {
495             return (mSize * (value - mMin)) / (mMax - mMin);
496         }
497 
498         @Override
convertToValue(float point)499         public long convertToValue(float point) {
500             return (long) (mMin + ((point * (mMax - mMin)) / mSize));
501         }
502 
503         @Override
buildLabel(Resources res, SpannableStringBuilder builder, long value)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
getTickPoints()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
shouldAdjustAxis(long value)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
hashCode()550         public int hashCode() {
551             return Objects.hashCode(mMin, mMax, mSize);
552         }
553 
554         @Override
setBounds(long min, long max)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
setSize(float size)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
convertToPoint(long value)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
convertToValue(float point)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
buildLabel(Resources res, SpannableStringBuilder builder, long value)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
getTickPoints()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
shouldAdjustAxis(long value)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 
setText( SpannableStringBuilder builder, Object key, CharSequence text, String bootstrap)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 
roundUpToPowerOfTwo(long i)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