Data usage: precise editing, restrict help, D-pad.
Introduce dialogs for precise editing of network policy warning/limit
values, triggered by click on sweep labels. Show up to 999MB before
rounding to GB, and round to nearest 5MB value when dragging. Partial
D-pad navigation around chart controls. Fix jumping when relayout
during drag, and fix sweep overlap bug.
When restricting data without limited networks, show dialog help to
guide user towards network limit. When reloading chart data, try
restoring to nearest cycle.
Bug: 5289641, 5111701, 5226078
Change-Id: Ic59dee6496c480a64dc56f8534acf4d81b50bca7
diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java
index 0e08075..bbc3606 100644
--- a/src/com/android/settings/DataUsageSummary.java
+++ b/src/com/android/settings/DataUsageSummary.java
@@ -20,6 +20,7 @@
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_WIMAX;
import static android.net.NetworkPolicy.LIMIT_DISABLED;
+import static android.net.NetworkPolicy.WARNING_DISABLED;
import static android.net.NetworkPolicyManager.EXTRA_NETWORK_TEMPLATE;
import static android.net.NetworkPolicyManager.POLICY_NONE;
import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
@@ -129,6 +130,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import java.util.Locale;
import libcore.util.Objects;
@@ -156,7 +158,10 @@
private static final String TAG_CONFIRM_DATA_ROAMING = "confirmDataRoaming";
private static final String TAG_CONFIRM_LIMIT = "confirmLimit";
private static final String TAG_CYCLE_EDITOR = "cycleEditor";
+ private static final String TAG_WARNING_EDITOR = "warningEditor";
+ private static final String TAG_LIMIT_EDITOR = "limitEditor";
private static final String TAG_CONFIRM_RESTRICT = "confirmRestrict";
+ private static final String TAG_DENIED_RESTRICT = "deniedRestrict";
private static final String TAG_CONFIRM_APP_RESTRICT = "confirmAppRestrict";
private static final String TAG_APP_DETAILS = "appDetails";
@@ -295,7 +300,10 @@
mTabHost.setOnTabChangedListener(mTabListener);
mHeader = (ViewGroup) inflater.inflate(R.layout.data_usage_header, mListView, false);
- mListView.addHeaderView(mHeader, null, false);
+ mHeader.setClickable(true);
+
+ mListView.addHeaderView(mHeader, null, true);
+ mListView.setItemsCanFocus(true);
if (mInsetSide > 0) {
// inset selector and divider drawables
@@ -316,7 +324,10 @@
mDisableAtLimit = new CheckBox(inflater.getContext());
mDisableAtLimit.setClickable(false);
+ mDisableAtLimit.setFocusable(false);
mDisableAtLimitView = inflatePreference(inflater, mNetworkSwitches, mDisableAtLimit);
+ mDisableAtLimitView.setClickable(true);
+ mDisableAtLimitView.setFocusable(true);
mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener);
mNetworkSwitches.addView(mDisableAtLimitView);
}
@@ -346,7 +357,10 @@
mAppRestrict = new CheckBox(inflater.getContext());
mAppRestrict.setClickable(false);
+ mAppRestrict.setFocusable(false);
mAppRestrictView = inflatePreference(inflater, mAppSwitches, mAppRestrict);
+ mAppRestrictView.setClickable(true);
+ mAppRestrictView.setFocusable(true);
mAppRestrictView.setOnClickListener(mAppRestrictListener);
mAppSwitches.addView(mAppRestrictView);
}
@@ -456,7 +470,11 @@
case R.id.data_usage_menu_restrict_background: {
final boolean restrictBackground = !item.isChecked();
if (restrictBackground) {
- ConfirmRestrictFragment.show(this);
+ if (hasLimitedNetworks()) {
+ ConfirmRestrictFragment.show(this);
+ } else {
+ DeniedRestrictFragment.show(this);
+ }
} else {
// no confirmation to drop restriction
setRestrictBackground(false);
@@ -619,7 +637,6 @@
if (LOGD) Log.d(TAG, "updateBody() with currentTab=" + currentTab);
mDataEnabledView.setVisibility(View.VISIBLE);
- mDisableAtLimitView.setVisibility(View.VISIBLE);
if (TAB_MOBILE.equals(currentTab)) {
setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_mobile);
@@ -735,7 +752,7 @@
setPreferenceTitle(mAppRestrictView, R.string.data_usage_app_restrict_background);
setPreferenceSummary(mAppRestrictView,
getString(R.string.data_usage_app_restrict_background_summary,
- buildLimitedNetworksList()));
+ buildLimitedNetworksString()));
mAppRestrictView.setVisibility(View.VISIBLE);
mAppRestrict.setChecked(getAppRestrictBackground());
@@ -745,12 +762,6 @@
}
}
- private void setPolicyCycleDay(int cycleDay) {
- if (LOGD) Log.d(TAG, "setPolicyCycleDay()");
- mPolicyEditor.setPolicyCycleDay(mTemplate, cycleDay);
- updatePolicy(true);
- }
-
private void setPolicyWarningBytes(long warningBytes) {
if (LOGD) Log.d(TAG, "setPolicyWarningBytes()");
mPolicyEditor.setPolicyWarningBytes(mTemplate, warningBytes);
@@ -863,15 +874,8 @@
private void updatePolicy(boolean refreshCycle) {
if (isAppDetailMode()) {
mNetworkSwitches.setVisibility(View.GONE);
- // we fall through to update cycle list for detail mode
} else {
mNetworkSwitches.setVisibility(View.VISIBLE);
-
- // when heading back to summary without cycle refresh, kick details
- // update to repopulate list.
- if (!refreshCycle) {
- updateDetailData();
- }
}
// TODO: move enabled state directly into policy
@@ -907,6 +911,8 @@
* item, updating the inspection range on {@link #mChart}.
*/
private void updateCycleList(NetworkPolicy policy) {
+ // stash away currently selected cycle to try restoring below
+ final CycleItem previousItem = (CycleItem) mCycleSpinner.getSelectedItem();
mCycleAdapter.clear();
final Context context = mCycleSpinner.getContext();
@@ -954,8 +960,18 @@
// force pick the current cycle (first item)
if (mCycleAdapter.getCount() > 0) {
- mCycleSpinner.setSelection(0);
- mCycleListener.onItemSelected(mCycleSpinner, null, 0, 0);
+ final int position = mCycleAdapter.findNearestPosition(previousItem);
+ mCycleSpinner.setSelection(position);
+
+ // only force-update cycle when changed; skipping preserves any
+ // user-defined inspection region.
+ final CycleItem selectedItem = mCycleAdapter.getItem(position);
+ if (!Objects.equal(selectedItem, previousItem)) {
+ mCycleListener.onItemSelected(mCycleSpinner, null, position, 0);
+ } else {
+ // but still kick off loader for detailed list
+ updateDetailData();
+ }
} else {
updateDetailData();
}
@@ -1002,9 +1018,16 @@
final boolean restrictBackground = !mAppRestrict.isChecked();
if (restrictBackground) {
- // enabling restriction; show confirmation dialog which
- // eventually calls setRestrictBackground() once user confirms.
- ConfirmAppRestrictFragment.show(DataUsageSummary.this);
+ if (hasLimitedNetworks()) {
+ // enabling restriction; show confirmation dialog which
+ // eventually calls setRestrictBackground() once user
+ // confirms.
+ ConfirmAppRestrictFragment.show(DataUsageSummary.this);
+ } else {
+ // no limited networks; show dialog to guide user towards
+ // setting a network limit. doesn't mutate restrict state.
+ DeniedRestrictFragment.show(DataUsageSummary.this);
+ }
} else {
setAppRestrictBackground(false);
}
@@ -1211,13 +1234,22 @@
public void onLimitChanged() {
setPolicyLimitBytes(mChart.getLimitBytes());
}
- };
+ /** {@inheritDoc} */
+ public void requestWarningEdit() {
+ WarningEditorFragment.show(DataUsageSummary.this);
+ }
+
+ /** {@inheritDoc} */
+ public void requestLimitEdit() {
+ LimitEditorFragment.show(DataUsageSummary.this);
+ }
+ };
/**
* List item that reflects a specific data usage cycle.
*/
- public static class CycleItem {
+ public static class CycleItem implements Comparable<CycleItem> {
public CharSequence label;
public long start;
public long end;
@@ -1236,6 +1268,20 @@
public String toString() {
return label.toString();
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof CycleItem) {
+ final CycleItem another = (CycleItem) o;
+ return start == another.start && end == another.end;
+ }
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public int compareTo(CycleItem another) {
+ return Long.compare(start, another.start);
+ }
}
private static final StringBuilder sBuilder = new StringBuilder(50);
@@ -1291,6 +1337,25 @@
add(mChangeItem);
}
}
+
+ /**
+ * Find position of {@link CycleItem} in this adapter which is nearest
+ * the given {@link CycleItem}.
+ */
+ public int findNearestPosition(CycleItem target) {
+ if (target != null) {
+ final int count = getCount();
+ for (int i = count - 1; i >= 0; i--) {
+ final CycleItem item = getItem(i);
+ if (item instanceof CycleChangeItem) {
+ continue;
+ } else if (item.compareTo(target) >= 0) {
+ return i;
+ }
+ }
+ }
+ return 0;
+ }
}
private static class AppUsageItem implements Comparable<AppUsageItem> {
@@ -1528,12 +1593,11 @@
* Dialog to edit {@link NetworkPolicy#cycleDay}.
*/
public static class CycleEditorFragment extends DialogFragment {
- private static final String EXTRA_CYCLE_DAY = "cycleDay";
+ private static final String EXTRA_TEMPLATE = "template";
public static void show(DataUsageSummary parent) {
- final NetworkPolicy policy = parent.mPolicyEditor.getPolicy(parent.mTemplate);
final Bundle args = new Bundle();
- args.putInt(CycleEditorFragment.EXTRA_CYCLE_DAY, policy.cycleDay);
+ args.putParcelable(EXTRA_TEMPLATE, parent.mTemplate);
final CycleEditorFragment dialog = new CycleEditorFragment();
dialog.setArguments(args);
@@ -1544,6 +1608,8 @@
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Context context = getActivity();
+ final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
+ final NetworkPolicyEditor editor = target.mPolicyEditor;
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
@@ -1551,11 +1617,12 @@
final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false);
final NumberPicker cycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day);
- final int oldCycleDay = getArguments().getInt(EXTRA_CYCLE_DAY, 1);
+ final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
+ final int cycleDay = editor.getPolicyCycleDay(template);
cycleDayPicker.setMinValue(1);
cycleDayPicker.setMaxValue(31);
- cycleDayPicker.setValue(oldCycleDay);
+ cycleDayPicker.setValue(cycleDay);
cycleDayPicker.setWrapSelectorWheel(true);
builder.setTitle(R.string.data_usage_cycle_editor_title);
@@ -1565,10 +1632,8 @@
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
final int cycleDay = cycleDayPicker.getValue();
- final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
- if (target != null) {
- target.setPolicyCycleDay(cycleDay);
- }
+ editor.setPolicyCycleDay(template, cycleDay);
+ target.updatePolicy(true);
}
});
@@ -1577,6 +1642,125 @@
}
/**
+ * Dialog to edit {@link NetworkPolicy#warningBytes}.
+ */
+ public static class WarningEditorFragment extends DialogFragment {
+ private static final String EXTRA_TEMPLATE = "template";
+
+ public static void show(DataUsageSummary parent) {
+ final Bundle args = new Bundle();
+ args.putParcelable(EXTRA_TEMPLATE, parent.mTemplate);
+
+ final WarningEditorFragment dialog = new WarningEditorFragment();
+ dialog.setArguments(args);
+ dialog.setTargetFragment(parent, 0);
+ dialog.show(parent.getFragmentManager(), TAG_WARNING_EDITOR);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getActivity();
+ final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
+ final NetworkPolicyEditor editor = target.mPolicyEditor;
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
+
+ final View view = dialogInflater.inflate(R.layout.data_usage_bytes_editor, null, false);
+ final NumberPicker bytesPicker = (NumberPicker) view.findViewById(R.id.bytes);
+
+ final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
+ final long warningBytes = editor.getPolicyWarningBytes(template);
+ final long limitBytes = editor.getPolicyLimitBytes(template);
+
+ bytesPicker.setMinValue(0);
+ if (limitBytes != LIMIT_DISABLED) {
+ bytesPicker.setMaxValue((int) (limitBytes / MB_IN_BYTES) - 1);
+ } else {
+ bytesPicker.setMaxValue(Integer.MAX_VALUE);
+ }
+ bytesPicker.setValue((int) (warningBytes / MB_IN_BYTES));
+ bytesPicker.setWrapSelectorWheel(false);
+
+ builder.setTitle(R.string.data_usage_warning_editor_title);
+ builder.setView(view);
+
+ builder.setPositiveButton(R.string.data_usage_cycle_editor_positive,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // clear focus to finish pending text edits
+ bytesPicker.clearFocus();
+
+ final long bytes = bytesPicker.getValue() * MB_IN_BYTES;
+ editor.setPolicyWarningBytes(template, bytes);
+ target.updatePolicy(false);
+ }
+ });
+
+ return builder.create();
+ }
+ }
+
+ /**
+ * Dialog to edit {@link NetworkPolicy#limitBytes}.
+ */
+ public static class LimitEditorFragment extends DialogFragment {
+ private static final String EXTRA_TEMPLATE = "template";
+
+ public static void show(DataUsageSummary parent) {
+ final Bundle args = new Bundle();
+ args.putParcelable(EXTRA_TEMPLATE, parent.mTemplate);
+
+ final LimitEditorFragment dialog = new LimitEditorFragment();
+ dialog.setArguments(args);
+ dialog.setTargetFragment(parent, 0);
+ dialog.show(parent.getFragmentManager(), TAG_LIMIT_EDITOR);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getActivity();
+ final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
+ final NetworkPolicyEditor editor = target.mPolicyEditor;
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
+
+ final View view = dialogInflater.inflate(R.layout.data_usage_bytes_editor, null, false);
+ final NumberPicker bytesPicker = (NumberPicker) view.findViewById(R.id.bytes);
+
+ final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
+ final long warningBytes = editor.getPolicyWarningBytes(template);
+ final long limitBytes = editor.getPolicyLimitBytes(template);
+
+ bytesPicker.setMaxValue(Integer.MAX_VALUE);
+ if (warningBytes != WARNING_DISABLED) {
+ bytesPicker.setMinValue((int) (warningBytes / MB_IN_BYTES) + 1);
+ } else {
+ bytesPicker.setMinValue(0);
+ }
+ bytesPicker.setValue((int) (limitBytes / MB_IN_BYTES));
+ bytesPicker.setWrapSelectorWheel(false);
+
+ builder.setTitle(R.string.data_usage_limit_editor_title);
+ builder.setView(view);
+
+ builder.setPositiveButton(R.string.data_usage_cycle_editor_positive,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // clear focus to finish pending text edits
+ bytesPicker.clearFocus();
+
+ final long bytes = bytesPicker.getValue() * MB_IN_BYTES;
+ editor.setPolicyLimitBytes(template, bytes);
+ target.updatePolicy(false);
+ }
+ });
+
+ return builder.create();
+ }
+ }
+ /**
* Dialog to request user confirmation before disabling data.
*/
public static class ConfirmDataDisableFragment extends DialogFragment {
@@ -1661,7 +1845,7 @@
final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
if (target != null) {
- final CharSequence limitedNetworks = target.buildLimitedNetworksList();
+ final CharSequence limitedNetworks = target.buildLimitedNetworksString();
builder.setMessage(
getString(R.string.data_usage_restrict_background, limitedNetworks));
}
@@ -1681,6 +1865,31 @@
}
/**
+ * Dialog to inform user that {@link #POLICY_REJECT_METERED_BACKGROUND}
+ * change has been denied, usually based on
+ * {@link DataUsageSummary#hasLimitedNetworks()}.
+ */
+ public static class DeniedRestrictFragment extends DialogFragment {
+ public static void show(DataUsageSummary parent) {
+ final DeniedRestrictFragment dialog = new DeniedRestrictFragment();
+ dialog.setTargetFragment(parent, 0);
+ dialog.show(parent.getFragmentManager(), TAG_DENIED_RESTRICT);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getActivity();
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.data_usage_app_restrict_background);
+ builder.setMessage(R.string.data_usage_restrict_denied_dialog);
+ builder.setPositiveButton(android.R.string.ok, null);
+
+ return builder.create();
+ }
+ }
+
+ /**
* Dialog to request user confirmation before setting
* {@link #POLICY_REJECT_METERED_BACKGROUND}.
*/
@@ -1879,10 +2088,32 @@
}
/**
+ * Test if any networks are currently limited.
+ */
+ private boolean hasLimitedNetworks() {
+ return !buildLimitedNetworksList().isEmpty();
+ }
+
+ /**
* Build string describing currently limited networks, which defines when
* background data is restricted.
*/
- private CharSequence buildLimitedNetworksList() {
+ private CharSequence buildLimitedNetworksString() {
+ final List<CharSequence> limited = buildLimitedNetworksList();
+
+ // handle case where no networks limited
+ if (limited.isEmpty()) {
+ limited.add(getText(R.string.data_usage_list_none));
+ }
+
+ return TextUtils.join(limited);
+ }
+
+ /**
+ * Build list of currently limited networks, which defines when background
+ * data is restricted.
+ */
+ private List<CharSequence> buildLimitedNetworksList() {
final Context context = getActivity();
final String subscriberId = getActiveSubscriberId(context);
@@ -1904,12 +2135,7 @@
limited.add(getText(R.string.data_usage_tab_ethernet));
}
- // handle case where no networks limited
- if (limited.isEmpty()) {
- limited.add(getText(R.string.data_usage_list_none));
- }
-
- return TextUtils.join(limited);
+ return limited;
}
/**
diff --git a/src/com/android/settings/net/NetworkPolicyEditor.java b/src/com/android/settings/net/NetworkPolicyEditor.java
index bb5a2c3..5ba8ca4 100644
--- a/src/com/android/settings/net/NetworkPolicyEditor.java
+++ b/src/com/android/settings/net/NetworkPolicyEditor.java
@@ -149,6 +149,10 @@
template, cycleDay, WARNING_DISABLED, LIMIT_DISABLED, SNOOZE_NEVER);
}
+ public int getPolicyCycleDay(NetworkTemplate template) {
+ return getPolicy(template).cycleDay;
+ }
+
public void setPolicyCycleDay(NetworkTemplate template, int cycleDay) {
final NetworkPolicy policy = getOrCreatePolicy(template);
policy.cycleDay = cycleDay;
@@ -156,6 +160,10 @@
writeAsync();
}
+ public long getPolicyWarningBytes(NetworkTemplate template) {
+ return getPolicy(template).warningBytes;
+ }
+
public void setPolicyWarningBytes(NetworkTemplate template, long warningBytes) {
final NetworkPolicy policy = getOrCreatePolicy(template);
policy.warningBytes = warningBytes;
@@ -163,6 +171,10 @@
writeAsync();
}
+ public long getPolicyLimitBytes(NetworkTemplate template) {
+ return getPolicy(template).limitBytes;
+ }
+
public void setPolicyLimitBytes(NetworkTemplate template, long limitBytes) {
final NetworkPolicy policy = getOrCreatePolicy(template);
policy.limitBytes = limitBytes;
diff --git a/src/com/android/settings/widget/ChartDataUsageView.java b/src/com/android/settings/widget/ChartDataUsageView.java
index e831cc1..43ce97c 100644
--- a/src/com/android/settings/widget/ChartDataUsageView.java
+++ b/src/com/android/settings/widget/ChartDataUsageView.java
@@ -69,6 +69,8 @@
public void onInspectRangeChanged();
public void onWarningChanged();
public void onLimitChanged();
+ public void requestWarningEdit();
+ public void requestLimitEdit();
}
private DataUsageChartListener mListener;
@@ -123,6 +125,15 @@
mSweepWarning.addOnSweepListener(mVertListener);
mSweepLimit.addOnSweepListener(mVertListener);
+ mSweepWarning.setDragInterval(5 * MB_IN_BYTES);
+ mSweepLimit.setDragInterval(5 * MB_IN_BYTES);
+
+ // TODO: make time sweeps adjustable through dpad
+ mSweepLeft.setClickable(false);
+ mSweepLeft.setFocusable(false);
+ mSweepRight.setClickable(false);
+ mSweepRight.setFocusable(false);
+
// tell everyone about our axis
mGrid.init(mHoriz, mVert);
mSeries.init(mHoriz, mVert);
@@ -276,6 +287,7 @@
}
private OnSweepListener mHorizListener = new OnSweepListener() {
+ /** {@inheritDoc} */
public void onSweep(ChartSweepView sweep, boolean sweepDone) {
updatePrimaryRange();
@@ -284,6 +296,11 @@
mListener.onInspectRangeChanged();
}
}
+
+ /** {@inheritDoc} */
+ public void requestEdit(ChartSweepView sweep) {
+ // ignored
+ }
};
private void sendUpdateAxisDelayed(ChartSweepView sweep, boolean force) {
@@ -298,6 +315,7 @@
}
private OnSweepListener mVertListener = new OnSweepListener() {
+ /** {@inheritDoc} */
public void onSweep(ChartSweepView sweep, boolean sweepDone) {
if (sweepDone) {
clearUpdateAxisDelayed(sweep);
@@ -313,6 +331,15 @@
sendUpdateAxisDelayed(sweep, false);
}
}
+
+ /** {@inheritDoc} */
+ public void requestEdit(ChartSweepView sweep) {
+ if (sweep == mSweepWarning && mListener != null) {
+ mListener.requestWarningEdit();
+ } else if (sweep == mSweepLimit && mListener != null) {
+ mListener.requestLimitEdit();
+ }
+ }
};
@Override
@@ -540,7 +567,7 @@
final CharSequence unit;
final long unitFactor;
- if (value <= 100 * MB_IN_BYTES) {
+ if (value < 1000 * MB_IN_BYTES) {
unit = res.getText(com.android.internal.R.string.megabyteShort);
unitFactor = MB_IN_BYTES;
} else {
@@ -551,6 +578,7 @@
final double result = (double) value / unitFactor;
final double resultRounded;
final CharSequence size;
+
if (result < 10) {
size = String.format("%.1f", result);
resultRounded = (unitFactor * Math.round(result * 10)) / 10;
diff --git a/src/com/android/settings/widget/ChartSweepView.java b/src/com/android/settings/widget/ChartSweepView.java
index 2190588..22a6478 100644
--- a/src/com/android/settings/widget/ChartSweepView.java
+++ b/src/com/android/settings/widget/ChartSweepView.java
@@ -83,11 +83,22 @@
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
+ private int mTouchMode = MODE_NONE;
+
+ private static final int MODE_NONE = 0;
+ private static final int MODE_DRAG = 1;
+ private static final int MODE_LABEL = 2;
+
+ private long mDragInterval = 1;
+
public interface OnSweepListener {
public void onSweep(ChartSweepView sweep, boolean sweepDone);
+ public void requestEdit(ChartSweepView sweep);
}
private OnSweepListener mListener;
+
+ private float mTrackingStart;
private MotionEvent mTracking;
public ChartSweepView(Context context) {
@@ -112,15 +123,28 @@
setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE));
+ // TODO: moved focused state directly into assets
+ setBackgroundResource(R.drawable.data_usage_sweep_background);
+
mOutlinePaint.setColor(Color.RED);
mOutlinePaint.setStrokeWidth(1f);
mOutlinePaint.setStyle(Style.STROKE);
a.recycle();
+ setClickable(true);
+ setFocusable(true);
+ setOnClickListener(mClickListener);
+
setWillNotDraw(false);
}
+ private OnClickListener mClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ dispatchRequestEdit();
+ }
+ };
+
void init(ChartAxis axis) {
mAxis = Preconditions.checkNotNull(axis, "missing axis");
}
@@ -133,6 +157,10 @@
return mMargins;
}
+ public void setDragInterval(long dragInterval) {
+ mDragInterval = dragInterval;
+ }
+
/**
* Return the number of pixels that the "target" area is inset from the
* {@link View} edge, along the current {@link #setFollowAxis(int)}.
@@ -159,9 +187,16 @@
}
}
+ private void dispatchRequestEdit() {
+ if (mListener != null) {
+ mListener.requestEdit(this);
+ }
+ }
+
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
+ setFocusable(enabled);
requestLayout();
}
@@ -232,6 +267,7 @@
private void invalidateLabel() {
if (mLabelTemplate != null && mAxis != null) {
mLabelValue = mAxis.buildLabel(getResources(), mLabelTemplate, mValue);
+ setContentDescription(mLabelTemplate);
invalidateLabelOffset();
invalidate();
} else {
@@ -369,11 +405,16 @@
case MotionEvent.ACTION_DOWN: {
// only start tracking when in sweet spot
- final boolean accept;
+ final boolean acceptDrag;
+ final boolean acceptLabel;
if (mFollowAxis == VERTICAL) {
- accept = event.getX() > getWidth() - (mSweepPadding.right * 8);
+ acceptDrag = event.getX() > getWidth() - (mSweepPadding.right * 8);
+ acceptLabel = mLabelLayout != null ? event.getX() < mLabelLayout.getWidth()
+ : false;
} else {
- accept = event.getY() > getHeight() - (mSweepPadding.bottom * 8);
+ acceptDrag = event.getY() > getHeight() - (mSweepPadding.bottom * 8);
+ acceptLabel = mLabelLayout != null ? event.getY() < mLabelLayout.getHeight()
+ : false;
}
final MotionEvent eventInParent = event.copy();
@@ -385,8 +426,14 @@
return false;
}
- if (accept) {
+ if (acceptDrag) {
+ if (mFollowAxis == VERTICAL) {
+ mTrackingStart = getTop() - mMargins.top;
+ } else {
+ mTrackingStart = getLeft() - mMargins.left;
+ }
mTracking = event.copy();
+ mTouchMode = MODE_DRAG;
// starting drag should activate entire chart
if (!parent.isActivated()) {
@@ -394,47 +441,68 @@
}
return true;
+ } else if (acceptLabel) {
+ mTouchMode = MODE_LABEL;
+ return true;
} else {
+ mTouchMode = MODE_NONE;
return false;
}
}
case MotionEvent.ACTION_MOVE: {
+ if (mTouchMode == MODE_LABEL) {
+ return true;
+ }
+
getParent().requestDisallowInterceptTouchEvent(true);
// content area of parent
final Rect parentContent = getParentContentRect();
final Rect clampRect = computeClampRect(parentContent);
+ if (clampRect.isEmpty()) return true;
+ long value;
if (mFollowAxis == VERTICAL) {
final float currentTargetY = getTop() - mMargins.top;
- final float requestedTargetY = currentTargetY
+ final float requestedTargetY = mTrackingStart
+ (event.getRawY() - mTracking.getRawY());
final float clampedTargetY = MathUtils.constrain(
requestedTargetY, clampRect.top, clampRect.bottom);
setTranslationY(clampedTargetY - currentTargetY);
- setValue(mAxis.convertToValue(clampedTargetY - parentContent.top));
+ value = mAxis.convertToValue(clampedTargetY - parentContent.top);
} else {
final float currentTargetX = getLeft() - mMargins.left;
- final float requestedTargetX = currentTargetX
+ final float requestedTargetX = mTrackingStart
+ (event.getRawX() - mTracking.getRawX());
final float clampedTargetX = MathUtils.constrain(
requestedTargetX, clampRect.left, clampRect.right);
setTranslationX(clampedTargetX - currentTargetX);
- setValue(mAxis.convertToValue(clampedTargetX - parentContent.left));
+ value = mAxis.convertToValue(clampedTargetX - parentContent.left);
}
+ // round value from drag to nearest increment
+ value -= value % mDragInterval;
+ setValue(value);
+
dispatchOnSweep(false);
return true;
}
case MotionEvent.ACTION_UP: {
- mTracking = null;
- mValue = mLabelValue;
- dispatchOnSweep(true);
- setTranslationX(0);
- setTranslationY(0);
- requestLayout();
+ if (mTouchMode == MODE_LABEL) {
+ performClick();
+ } else if (mTouchMode == MODE_DRAG) {
+ mTrackingStart = 0;
+ mTracking = null;
+ mValue = mLabelValue;
+ dispatchOnSweep(true);
+ setTranslationX(0);
+ setTranslationY(0);
+ requestLayout();
+ }
+
+ mTouchMode = MODE_NONE;
return true;
}
default: {
@@ -501,7 +569,9 @@
final Rect dynamicRect = buildClampRect(
parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin);
- rect.intersect(dynamicRect);
+ if (!rect.intersect(dynamicRect)) {
+ rect.setEmpty();
+ }
return rect;
}
@@ -587,7 +657,7 @@
mContentOffset.bottom -= offset;
mMargins.bottom += offset;
} else {
- final int heightAfter = heightBefore * 3;
+ final int heightAfter = heightBefore * 2;
setMeasuredDimension(widthBefore, heightAfter);
mContentOffset.offset(0, (heightAfter - heightBefore) / 2);
@@ -608,13 +678,11 @@
@Override
protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
final int width = getWidth();
final int height = getHeight();
- if (DRAW_OUTLINE) {
- canvas.drawRect(0, 0, width, height, mOutlinePaint);
- }
-
final int labelSize;
if (isEnabled() && mLabelLayout != null) {
final int count = canvas.save();
@@ -637,6 +705,11 @@
}
mSweep.draw(canvas);
+
+ if (DRAW_OUTLINE) {
+ mOutlinePaint.setColor(Color.RED);
+ canvas.drawRect(0, 0, width, height, mOutlinePaint);
+ }
}
public static float getLabelTop(ChartSweepView view) {