1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.settings.datausage; 16 17 import static android.net.NetworkPolicy.CYCLE_NONE; 18 import static android.net.NetworkPolicy.LIMIT_DISABLED; 19 import static android.net.NetworkPolicy.WARNING_DISABLED; 20 21 import android.app.AlertDialog; 22 import android.app.Dialog; 23 import android.app.Fragment; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.res.Resources; 27 import android.net.NetworkPolicy; 28 import android.net.NetworkTemplate; 29 import android.os.Bundle; 30 import android.support.v14.preference.SwitchPreference; 31 import android.support.v7.preference.Preference; 32 import android.text.format.Time; 33 import android.util.FeatureFlagUtils; 34 import android.util.Log; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.widget.EditText; 38 import android.widget.NumberPicker; 39 import android.widget.Spinner; 40 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 43 import com.android.settings.R; 44 import com.android.settings.core.FeatureFlags; 45 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 46 import com.android.settingslib.NetworkPolicyEditor; 47 import com.android.settingslib.net.DataUsageController; 48 49 public class BillingCycleSettings extends DataUsageBase implements 50 Preference.OnPreferenceChangeListener, DataUsageEditController { 51 52 private static final String TAG = "BillingCycleSettings"; 53 private static final boolean LOGD = false; 54 public static final long MIB_IN_BYTES = 1024 * 1024; 55 public static final long GIB_IN_BYTES = MIB_IN_BYTES * 1024; 56 57 private static final long MAX_DATA_LIMIT_BYTES = 50000 * GIB_IN_BYTES; 58 59 private static final String TAG_CONFIRM_LIMIT = "confirmLimit"; 60 private static final String TAG_CYCLE_EDITOR = "cycleEditor"; 61 private static final String TAG_WARNING_EDITOR = "warningEditor"; 62 63 private static final String KEY_BILLING_CYCLE = "billing_cycle"; 64 private static final String KEY_SET_DATA_WARNING = "set_data_warning"; 65 private static final String KEY_DATA_WARNING = "data_warning"; 66 @VisibleForTesting static final String KEY_SET_DATA_LIMIT = "set_data_limit"; 67 private static final String KEY_DATA_LIMIT = "data_limit"; 68 69 private NetworkTemplate mNetworkTemplate; 70 private Preference mBillingCycle; 71 private Preference mDataWarning; 72 private SwitchPreference mEnableDataWarning; 73 private SwitchPreference mEnableDataLimit; 74 private Preference mDataLimit; 75 private DataUsageController mDataUsageController; 76 77 @VisibleForTesting setUpForTest(NetworkPolicyEditor policyEditor, Preference billingCycle, Preference dataLimit, Preference dataWarning, SwitchPreference enableLimit, SwitchPreference enableWarning)78 void setUpForTest(NetworkPolicyEditor policyEditor, 79 Preference billingCycle, 80 Preference dataLimit, 81 Preference dataWarning, 82 SwitchPreference enableLimit, 83 SwitchPreference enableWarning) { 84 services.mPolicyEditor = policyEditor; 85 mBillingCycle = billingCycle; 86 mDataLimit = dataLimit; 87 mDataWarning = dataWarning; 88 mEnableDataLimit = enableLimit; 89 mEnableDataWarning = enableWarning; 90 } 91 92 @Override onCreate(Bundle icicle)93 public void onCreate(Bundle icicle) { 94 super.onCreate(icicle); 95 96 mDataUsageController = new DataUsageController(getContext()); 97 98 Bundle args = getArguments(); 99 mNetworkTemplate = args.getParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE); 100 101 addPreferencesFromResource(R.xml.billing_cycle); 102 mBillingCycle = findPreference(KEY_BILLING_CYCLE); 103 mEnableDataWarning = (SwitchPreference) findPreference(KEY_SET_DATA_WARNING); 104 mEnableDataWarning.setOnPreferenceChangeListener(this); 105 mDataWarning = findPreference(KEY_DATA_WARNING); 106 mEnableDataLimit = (SwitchPreference) findPreference(KEY_SET_DATA_LIMIT); 107 mEnableDataLimit.setOnPreferenceChangeListener(this); 108 mDataLimit = findPreference(KEY_DATA_LIMIT); 109 110 mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.data_warning_footnote); 111 } 112 113 @Override onResume()114 public void onResume() { 115 super.onResume(); 116 updatePrefs(); 117 } 118 119 @VisibleForTesting updatePrefs()120 void updatePrefs() { 121 final int cycleDay = services.mPolicyEditor.getPolicyCycleDay(mNetworkTemplate); 122 if (FeatureFlagUtils.isEnabled(getContext(), FeatureFlags.DATA_USAGE_SETTINGS_V2)) { 123 mBillingCycle.setSummary(null); 124 } else if (cycleDay != CYCLE_NONE) { 125 mBillingCycle.setSummary(getString(R.string.billing_cycle_fragment_summary, cycleDay)); 126 } else { 127 mBillingCycle.setSummary(null); 128 } 129 final long warningBytes = services.mPolicyEditor.getPolicyWarningBytes(mNetworkTemplate); 130 if (warningBytes != WARNING_DISABLED) { 131 mDataWarning.setSummary(DataUsageUtils.formatDataUsage(getContext(), warningBytes)); 132 mDataWarning.setEnabled(true); 133 mEnableDataWarning.setChecked(true); 134 } else { 135 mDataWarning.setSummary(null); 136 mDataWarning.setEnabled(false); 137 mEnableDataWarning.setChecked(false); 138 } 139 final long limitBytes = services.mPolicyEditor.getPolicyLimitBytes(mNetworkTemplate); 140 if (limitBytes != LIMIT_DISABLED) { 141 mDataLimit.setSummary(DataUsageUtils.formatDataUsage(getContext(), limitBytes)); 142 mDataLimit.setEnabled(true); 143 mEnableDataLimit.setChecked(true); 144 } else { 145 mDataLimit.setSummary(null); 146 mDataLimit.setEnabled(false); 147 mEnableDataLimit.setChecked(false); 148 } 149 } 150 151 @Override onPreferenceTreeClick(Preference preference)152 public boolean onPreferenceTreeClick(Preference preference) { 153 if (preference == mBillingCycle) { 154 CycleEditorFragment.show(this); 155 return true; 156 } else if (preference == mDataWarning) { 157 BytesEditorFragment.show(this, false); 158 return true; 159 } else if (preference == mDataLimit) { 160 BytesEditorFragment.show(this, true); 161 return true; 162 } 163 return super.onPreferenceTreeClick(preference); 164 } 165 166 @Override onPreferenceChange(Preference preference, Object newValue)167 public boolean onPreferenceChange(Preference preference, Object newValue) { 168 if (mEnableDataLimit == preference) { 169 boolean enabled = (Boolean) newValue; 170 if (!enabled) { 171 setPolicyLimitBytes(LIMIT_DISABLED); 172 return true; 173 } 174 ConfirmLimitFragment.show(this); 175 // This preference is enabled / disabled by ConfirmLimitFragment. 176 return false; 177 } else if (mEnableDataWarning == preference) { 178 boolean enabled = (Boolean) newValue; 179 if (enabled) { 180 setPolicyWarningBytes(mDataUsageController.getDefaultWarningLevel()); 181 } else { 182 setPolicyWarningBytes(WARNING_DISABLED); 183 } 184 return true; 185 } 186 return false; 187 } 188 189 @Override getMetricsCategory()190 public int getMetricsCategory() { 191 return MetricsEvent.BILLING_CYCLE; 192 } 193 194 @VisibleForTesting setPolicyLimitBytes(long limitBytes)195 void setPolicyLimitBytes(long limitBytes) { 196 if (LOGD) Log.d(TAG, "setPolicyLimitBytes()"); 197 services.mPolicyEditor.setPolicyLimitBytes(mNetworkTemplate, limitBytes); 198 updatePrefs(); 199 } 200 setPolicyWarningBytes(long warningBytes)201 private void setPolicyWarningBytes(long warningBytes) { 202 if (LOGD) Log.d(TAG, "setPolicyWarningBytes()"); 203 services.mPolicyEditor.setPolicyWarningBytes(mNetworkTemplate, warningBytes); 204 updatePrefs(); 205 } 206 207 @Override getNetworkPolicyEditor()208 public NetworkPolicyEditor getNetworkPolicyEditor() { 209 return services.mPolicyEditor; 210 } 211 212 @Override getNetworkTemplate()213 public NetworkTemplate getNetworkTemplate() { 214 return mNetworkTemplate; 215 } 216 217 @Override updateDataUsage()218 public void updateDataUsage() { 219 updatePrefs(); 220 } 221 222 /** 223 * Dialog to edit {@link NetworkPolicy#warningBytes}. 224 */ 225 public static class BytesEditorFragment extends InstrumentedDialogFragment 226 implements DialogInterface.OnClickListener { 227 private static final String EXTRA_TEMPLATE = "template"; 228 private static final String EXTRA_LIMIT = "limit"; 229 private View mView; 230 show(DataUsageEditController parent, boolean isLimit)231 public static void show(DataUsageEditController parent, boolean isLimit) { 232 if (!(parent instanceof Fragment)) { 233 return; 234 } 235 Fragment targetFragment = (Fragment) parent; 236 if (!targetFragment.isAdded()) { 237 return; 238 } 239 240 final Bundle args = new Bundle(); 241 args.putParcelable(EXTRA_TEMPLATE, parent.getNetworkTemplate()); 242 args.putBoolean(EXTRA_LIMIT, isLimit); 243 244 final BytesEditorFragment dialog = new BytesEditorFragment(); 245 dialog.setArguments(args); 246 dialog.setTargetFragment(targetFragment, 0); 247 dialog.show(targetFragment.getFragmentManager(), TAG_WARNING_EDITOR); 248 } 249 250 @Override onCreateDialog(Bundle savedInstanceState)251 public Dialog onCreateDialog(Bundle savedInstanceState) { 252 final Context context = getActivity(); 253 final LayoutInflater dialogInflater = LayoutInflater.from(context); 254 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 255 mView = dialogInflater.inflate(R.layout.data_usage_bytes_editor, null, false); 256 setupPicker((EditText) mView.findViewById(R.id.bytes), 257 (Spinner) mView.findViewById(R.id.size_spinner)); 258 return new AlertDialog.Builder(context) 259 .setTitle(isLimit ? R.string.data_usage_limit_editor_title 260 : R.string.data_usage_warning_editor_title) 261 .setView(mView) 262 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 263 .create(); 264 } 265 setupPicker(EditText bytesPicker, Spinner type)266 private void setupPicker(EditText bytesPicker, Spinner type) { 267 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 268 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 269 270 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 271 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 272 final long bytes = isLimit ? editor.getPolicyLimitBytes(template) 273 : editor.getPolicyWarningBytes(template); 274 final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED; 275 276 if (bytes > 1.5f * GIB_IN_BYTES) { 277 final String bytesText = formatText(bytes / (float) GIB_IN_BYTES); 278 bytesPicker.setText(bytesText); 279 bytesPicker.setSelection(0, bytesText.length()); 280 281 type.setSelection(1); 282 } else { 283 final String bytesText = formatText(bytes / (float) MIB_IN_BYTES); 284 bytesPicker.setText(bytesText); 285 bytesPicker.setSelection(0, bytesText.length()); 286 287 type.setSelection(0); 288 } 289 } 290 formatText(float v)291 private String formatText(float v) { 292 v = Math.round(v * 100) / 100f; 293 return String.valueOf(v); 294 } 295 296 @Override onClick(DialogInterface dialog, int which)297 public void onClick(DialogInterface dialog, int which) { 298 if (which != DialogInterface.BUTTON_POSITIVE) { 299 return; 300 } 301 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 302 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 303 304 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 305 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 306 EditText bytesField = (EditText) mView.findViewById(R.id.bytes); 307 Spinner spinner = (Spinner) mView.findViewById(R.id.size_spinner); 308 309 String bytesString = bytesField.getText().toString(); 310 if (bytesString.isEmpty() || bytesString.equals(".")) { 311 bytesString = "0"; 312 } 313 final long bytes = (long) (Float.valueOf(bytesString) 314 * (spinner.getSelectedItemPosition() == 0 ? MIB_IN_BYTES : GIB_IN_BYTES)); 315 316 // to fix the overflow problem 317 final long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes); 318 if (isLimit) { 319 editor.setPolicyLimitBytes(template, correctedBytes); 320 } else { 321 editor.setPolicyWarningBytes(template, correctedBytes); 322 } 323 target.updateDataUsage(); 324 } 325 326 @Override getMetricsCategory()327 public int getMetricsCategory() { 328 return MetricsEvent.DIALOG_BILLING_BYTE_LIMIT; 329 } 330 } 331 332 /** 333 * Dialog to edit {@link NetworkPolicy}. 334 */ 335 public static class CycleEditorFragment extends InstrumentedDialogFragment implements 336 DialogInterface.OnClickListener { 337 private static final String EXTRA_TEMPLATE = "template"; 338 private NumberPicker mCycleDayPicker; 339 show(BillingCycleSettings parent)340 public static void show(BillingCycleSettings parent) { 341 if (!parent.isAdded()) return; 342 343 final Bundle args = new Bundle(); 344 args.putParcelable(EXTRA_TEMPLATE, parent.mNetworkTemplate); 345 346 final CycleEditorFragment dialog = new CycleEditorFragment(); 347 dialog.setArguments(args); 348 dialog.setTargetFragment(parent, 0); 349 dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR); 350 } 351 352 @Override getMetricsCategory()353 public int getMetricsCategory() { 354 return MetricsEvent.DIALOG_BILLING_CYCLE; 355 } 356 357 @Override onCreateDialog(Bundle savedInstanceState)358 public Dialog onCreateDialog(Bundle savedInstanceState) { 359 final Context context = getActivity(); 360 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 361 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 362 363 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 364 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); 365 366 final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false); 367 mCycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day); 368 369 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 370 final int cycleDay = editor.getPolicyCycleDay(template); 371 372 mCycleDayPicker.setMinValue(1); 373 mCycleDayPicker.setMaxValue(31); 374 mCycleDayPicker.setValue(cycleDay); 375 mCycleDayPicker.setWrapSelectorWheel(true); 376 377 return builder.setTitle(R.string.data_usage_cycle_editor_title) 378 .setView(view) 379 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 380 .create(); 381 } 382 383 @Override onClick(DialogInterface dialog, int which)384 public void onClick(DialogInterface dialog, int which) { 385 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 386 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 387 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 388 389 // clear focus to finish pending text edits 390 mCycleDayPicker.clearFocus(); 391 392 final int cycleDay = mCycleDayPicker.getValue(); 393 final String cycleTimezone = new Time().timezone; 394 editor.setPolicyCycleDay(template, cycleDay, cycleTimezone); 395 target.updateDataUsage(); 396 } 397 } 398 399 /** 400 * Dialog to request user confirmation before setting 401 * {@link NetworkPolicy#limitBytes}. 402 */ 403 public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements 404 DialogInterface.OnClickListener { 405 @VisibleForTesting static final String EXTRA_LIMIT_BYTES = "limitBytes"; 406 public static final float FLOAT = 1.2f; 407 show(BillingCycleSettings parent)408 public static void show(BillingCycleSettings parent) { 409 if (!parent.isAdded()) return; 410 411 final NetworkPolicy policy = parent.services.mPolicyEditor 412 .getPolicy(parent.mNetworkTemplate); 413 if (policy == null) return; 414 415 final Resources res = parent.getResources(); 416 final long minLimitBytes = (long) (policy.warningBytes * FLOAT); 417 final long limitBytes; 418 419 // TODO: customize default limits based on network template 420 limitBytes = Math.max(5 * GIB_IN_BYTES, minLimitBytes); 421 422 final Bundle args = new Bundle(); 423 args.putLong(EXTRA_LIMIT_BYTES, limitBytes); 424 425 final ConfirmLimitFragment dialog = new ConfirmLimitFragment(); 426 dialog.setArguments(args); 427 dialog.setTargetFragment(parent, 0); 428 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT); 429 } 430 431 @Override getMetricsCategory()432 public int getMetricsCategory() { 433 return MetricsEvent.DIALOG_BILLING_CONFIRM_LIMIT; 434 } 435 436 @Override onCreateDialog(Bundle savedInstanceState)437 public Dialog onCreateDialog(Bundle savedInstanceState) { 438 final Context context = getActivity(); 439 440 return new AlertDialog.Builder(context) 441 .setTitle(R.string.data_usage_limit_dialog_title) 442 .setMessage(R.string.data_usage_limit_dialog_mobile) 443 .setPositiveButton(android.R.string.ok, this) 444 .setNegativeButton(android.R.string.cancel, null) 445 .create(); 446 } 447 448 @Override onClick(DialogInterface dialog, int which)449 public void onClick(DialogInterface dialog, int which) { 450 final BillingCycleSettings target = (BillingCycleSettings) getTargetFragment(); 451 if (which != DialogInterface.BUTTON_POSITIVE) return; 452 final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES); 453 if (target != null) { 454 target.setPolicyLimitBytes(limitBytes); 455 } 456 target.getPreferenceManager().getSharedPreferences().edit() 457 .putBoolean(KEY_SET_DATA_LIMIT, true).apply(); 458 } 459 } 460 } 461