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.LIMIT_DISABLED; 18 import static android.net.NetworkPolicy.WARNING_DISABLED; 19 20 import android.app.Dialog; 21 import android.app.settings.SettingsEnums; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.res.Resources; 25 import android.net.NetworkPolicy; 26 import android.net.NetworkTemplate; 27 import android.os.Bundle; 28 import android.text.method.NumberKeyListener; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.inputmethod.EditorInfo; 33 import android.widget.EditText; 34 import android.widget.NumberPicker; 35 import android.widget.Spinner; 36 37 import androidx.annotation.VisibleForTesting; 38 import androidx.appcompat.app.AlertDialog; 39 import androidx.fragment.app.Fragment; 40 import androidx.preference.Preference; 41 import androidx.preference.SwitchPreference; 42 43 import com.android.settings.R; 44 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 45 import com.android.settings.search.BaseSearchIndexProvider; 46 import com.android.settingslib.NetworkPolicyEditor; 47 import com.android.settingslib.net.DataUsageController; 48 import com.android.settingslib.search.SearchIndexable; 49 50 import java.text.NumberFormat; 51 import java.text.ParseException; 52 import java.util.TimeZone; 53 54 @SearchIndexable 55 public class BillingCycleSettings extends DataUsageBaseFragment implements 56 Preference.OnPreferenceChangeListener, DataUsageEditController { 57 58 private static final String TAG = "BillingCycleSettings"; 59 private static final boolean LOGD = false; 60 public static final long MIB_IN_BYTES = 1024 * 1024; 61 public static final long GIB_IN_BYTES = MIB_IN_BYTES * 1024; 62 63 private static final long MAX_DATA_LIMIT_BYTES = 50000 * GIB_IN_BYTES; 64 65 private static final String TAG_CONFIRM_LIMIT = "confirmLimit"; 66 private static final String TAG_CYCLE_EDITOR = "cycleEditor"; 67 private static final String TAG_WARNING_EDITOR = "warningEditor"; 68 69 private static final String KEY_BILLING_CYCLE = "billing_cycle"; 70 private static final String KEY_SET_DATA_WARNING = "set_data_warning"; 71 private static final String KEY_DATA_WARNING = "data_warning"; 72 @VisibleForTesting 73 static final String KEY_SET_DATA_LIMIT = "set_data_limit"; 74 private static final String KEY_DATA_LIMIT = "data_limit"; 75 76 @VisibleForTesting 77 NetworkTemplate mNetworkTemplate; 78 private Preference mBillingCycle; 79 private Preference mDataWarning; 80 private SwitchPreference mEnableDataWarning; 81 private SwitchPreference mEnableDataLimit; 82 private Preference mDataLimit; 83 private DataUsageController mDataUsageController; 84 85 @VisibleForTesting setUpForTest(NetworkPolicyEditor policyEditor, Preference billingCycle, Preference dataLimit, Preference dataWarning, SwitchPreference enableLimit, SwitchPreference enableWarning)86 void setUpForTest(NetworkPolicyEditor policyEditor, 87 Preference billingCycle, 88 Preference dataLimit, 89 Preference dataWarning, 90 SwitchPreference enableLimit, 91 SwitchPreference enableWarning) { 92 services.mPolicyEditor = policyEditor; 93 mBillingCycle = billingCycle; 94 mDataLimit = dataLimit; 95 mDataWarning = dataWarning; 96 mEnableDataLimit = enableLimit; 97 mEnableDataWarning = enableWarning; 98 } 99 100 @Override onCreate(Bundle icicle)101 public void onCreate(Bundle icicle) { 102 super.onCreate(icicle); 103 104 final Context context = getContext(); 105 mDataUsageController = new DataUsageController(context); 106 107 Bundle args = getArguments(); 108 mNetworkTemplate = args.getParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE); 109 if (mNetworkTemplate == null) { 110 mNetworkTemplate = DataUsageUtils.getDefaultTemplate(context, 111 DataUsageUtils.getDefaultSubscriptionId(context)); 112 } 113 114 mBillingCycle = findPreference(KEY_BILLING_CYCLE); 115 mEnableDataWarning = (SwitchPreference) findPreference(KEY_SET_DATA_WARNING); 116 mEnableDataWarning.setOnPreferenceChangeListener(this); 117 mDataWarning = findPreference(KEY_DATA_WARNING); 118 mEnableDataLimit = (SwitchPreference) findPreference(KEY_SET_DATA_LIMIT); 119 mEnableDataLimit.setOnPreferenceChangeListener(this); 120 mDataLimit = findPreference(KEY_DATA_LIMIT); 121 } 122 123 @Override onResume()124 public void onResume() { 125 super.onResume(); 126 updatePrefs(); 127 } 128 129 @VisibleForTesting updatePrefs()130 void updatePrefs() { 131 mBillingCycle.setSummary(null); 132 final long warningBytes = services.mPolicyEditor.getPolicyWarningBytes(mNetworkTemplate); 133 if (warningBytes != WARNING_DISABLED) { 134 mDataWarning.setSummary(DataUsageUtils.formatDataUsage(getContext(), warningBytes)); 135 mDataWarning.setEnabled(true); 136 mEnableDataWarning.setChecked(true); 137 } else { 138 mDataWarning.setSummary(null); 139 mDataWarning.setEnabled(false); 140 mEnableDataWarning.setChecked(false); 141 } 142 final long limitBytes = services.mPolicyEditor.getPolicyLimitBytes(mNetworkTemplate); 143 if (limitBytes != LIMIT_DISABLED) { 144 mDataLimit.setSummary(DataUsageUtils.formatDataUsage(getContext(), limitBytes)); 145 mDataLimit.setEnabled(true); 146 mEnableDataLimit.setChecked(true); 147 } else { 148 mDataLimit.setSummary(null); 149 mDataLimit.setEnabled(false); 150 mEnableDataLimit.setChecked(false); 151 } 152 } 153 154 @Override onPreferenceTreeClick(Preference preference)155 public boolean onPreferenceTreeClick(Preference preference) { 156 if (preference == mBillingCycle) { 157 writePreferenceClickMetric(preference); 158 CycleEditorFragment.show(this); 159 return true; 160 } else if (preference == mDataWarning) { 161 writePreferenceClickMetric(preference); 162 BytesEditorFragment.show(this, false); 163 return true; 164 } else if (preference == mDataLimit) { 165 writePreferenceClickMetric(preference); 166 BytesEditorFragment.show(this, true); 167 return true; 168 } 169 return super.onPreferenceTreeClick(preference); 170 } 171 172 @Override onPreferenceChange(Preference preference, Object newValue)173 public boolean onPreferenceChange(Preference preference, Object newValue) { 174 if (mEnableDataLimit == preference) { 175 boolean enabled = (Boolean) newValue; 176 if (!enabled) { 177 setPolicyLimitBytes(LIMIT_DISABLED); 178 return true; 179 } 180 ConfirmLimitFragment.show(this); 181 // This preference is enabled / disabled by ConfirmLimitFragment. 182 return false; 183 } else if (mEnableDataWarning == preference) { 184 boolean enabled = (Boolean) newValue; 185 if (enabled) { 186 setPolicyWarningBytes(mDataUsageController.getDefaultWarningLevel()); 187 } else { 188 setPolicyWarningBytes(WARNING_DISABLED); 189 } 190 return true; 191 } 192 return false; 193 } 194 195 @Override getMetricsCategory()196 public int getMetricsCategory() { 197 return SettingsEnums.BILLING_CYCLE; 198 } 199 200 @Override getPreferenceScreenResId()201 protected int getPreferenceScreenResId() { 202 return R.xml.billing_cycle; 203 } 204 205 @Override getLogTag()206 protected String getLogTag() { 207 return TAG; 208 } 209 210 @VisibleForTesting setPolicyLimitBytes(long limitBytes)211 void setPolicyLimitBytes(long limitBytes) { 212 if (LOGD) Log.d(TAG, "setPolicyLimitBytes()"); 213 services.mPolicyEditor.setPolicyLimitBytes(mNetworkTemplate, limitBytes); 214 updatePrefs(); 215 } 216 setPolicyWarningBytes(long warningBytes)217 private void setPolicyWarningBytes(long warningBytes) { 218 if (LOGD) Log.d(TAG, "setPolicyWarningBytes()"); 219 services.mPolicyEditor.setPolicyWarningBytes(mNetworkTemplate, warningBytes); 220 updatePrefs(); 221 } 222 223 @Override getNetworkPolicyEditor()224 public NetworkPolicyEditor getNetworkPolicyEditor() { 225 return services.mPolicyEditor; 226 } 227 228 @Override getNetworkTemplate()229 public NetworkTemplate getNetworkTemplate() { 230 return mNetworkTemplate; 231 } 232 233 @Override updateDataUsage()234 public void updateDataUsage() { 235 updatePrefs(); 236 } 237 238 /** 239 * Dialog to edit {@link NetworkPolicy#warningBytes}. 240 */ 241 public static class BytesEditorFragment extends InstrumentedDialogFragment 242 implements DialogInterface.OnClickListener { 243 private static final String EXTRA_TEMPLATE = "template"; 244 private static final String EXTRA_LIMIT = "limit"; 245 private View mView; 246 show(DataUsageEditController parent, boolean isLimit)247 public static void show(DataUsageEditController parent, boolean isLimit) { 248 if (!(parent instanceof Fragment)) { 249 return; 250 } 251 Fragment targetFragment = (Fragment) parent; 252 if (!targetFragment.isAdded()) { 253 return; 254 } 255 256 final Bundle args = new Bundle(); 257 args.putParcelable(EXTRA_TEMPLATE, parent.getNetworkTemplate()); 258 args.putBoolean(EXTRA_LIMIT, isLimit); 259 260 final BytesEditorFragment dialog = new BytesEditorFragment(); 261 dialog.setArguments(args); 262 dialog.setTargetFragment(targetFragment, 0); 263 dialog.show(targetFragment.getFragmentManager(), TAG_WARNING_EDITOR); 264 } 265 266 @Override onCreateDialog(Bundle savedInstanceState)267 public Dialog onCreateDialog(Bundle savedInstanceState) { 268 final Context context = getActivity(); 269 final LayoutInflater dialogInflater = LayoutInflater.from(context); 270 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 271 mView = dialogInflater.inflate(R.layout.data_usage_bytes_editor, null, false); 272 setupPicker((EditText) mView.findViewById(R.id.bytes), 273 (Spinner) mView.findViewById(R.id.size_spinner)); 274 Dialog dialog = new AlertDialog.Builder(context) 275 .setTitle(isLimit ? R.string.data_usage_limit_editor_title 276 : R.string.data_usage_warning_editor_title) 277 .setView(mView) 278 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 279 .create(); 280 dialog.setCanceledOnTouchOutside(false); 281 return dialog; 282 } 283 setupPicker(EditText bytesPicker, Spinner type)284 private void setupPicker(EditText bytesPicker, Spinner type) { 285 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 286 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 287 288 bytesPicker.setKeyListener(new NumberKeyListener() { 289 protected char[] getAcceptedChars() { 290 return new char [] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 291 ',', '.'}; 292 } 293 public int getInputType() { 294 return EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; 295 } 296 }); 297 298 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 299 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 300 final long bytes = isLimit ? editor.getPolicyLimitBytes(template) 301 : editor.getPolicyWarningBytes(template); 302 final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED; 303 304 final boolean unitInGigaBytes = (bytes > 1.5f * GIB_IN_BYTES); 305 final String bytesText = formatText(bytes, 306 unitInGigaBytes ? GIB_IN_BYTES : MIB_IN_BYTES); 307 bytesPicker.setText(bytesText); 308 bytesPicker.setSelection(0, bytesText.length()); 309 310 type.setSelection(unitInGigaBytes ? 1 : 0); 311 } 312 formatText(double v, double unitInBytes)313 private String formatText(double v, double unitInBytes) { 314 final NumberFormat formatter = NumberFormat.getNumberInstance(); 315 formatter.setMaximumFractionDigits(2); 316 return formatter.format((double) (v / unitInBytes)); 317 } 318 319 @Override onClick(DialogInterface dialog, int which)320 public void onClick(DialogInterface dialog, int which) { 321 if (which != DialogInterface.BUTTON_POSITIVE) { 322 return; 323 } 324 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 325 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 326 327 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 328 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 329 final EditText bytesField = (EditText) mView.findViewById(R.id.bytes); 330 final Spinner spinner = (Spinner) mView.findViewById(R.id.size_spinner); 331 332 final String bytesString = bytesField.getText().toString(); 333 334 final NumberFormat formatter = NumberFormat.getNumberInstance(); 335 Number number = null; 336 try { 337 number = formatter.parse(bytesString); 338 } catch (ParseException ex) { 339 } 340 long bytes = 0L; 341 if (number != null) { 342 bytes = (long) (number.floatValue() 343 * (spinner.getSelectedItemPosition() == 0 ? MIB_IN_BYTES : GIB_IN_BYTES)); 344 } 345 346 // to fix the overflow problem 347 final long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes); 348 if (isLimit) { 349 editor.setPolicyLimitBytes(template, correctedBytes); 350 } else { 351 editor.setPolicyWarningBytes(template, correctedBytes); 352 } 353 target.updateDataUsage(); 354 } 355 356 @Override getMetricsCategory()357 public int getMetricsCategory() { 358 return SettingsEnums.DIALOG_BILLING_BYTE_LIMIT; 359 } 360 } 361 362 /** 363 * Dialog to edit {@link NetworkPolicy}. 364 */ 365 public static class CycleEditorFragment extends InstrumentedDialogFragment implements 366 DialogInterface.OnClickListener { 367 private static final String EXTRA_TEMPLATE = "template"; 368 private NumberPicker mCycleDayPicker; 369 show(BillingCycleSettings parent)370 public static void show(BillingCycleSettings parent) { 371 if (!parent.isAdded()) return; 372 373 final Bundle args = new Bundle(); 374 args.putParcelable(EXTRA_TEMPLATE, parent.mNetworkTemplate); 375 376 final CycleEditorFragment dialog = new CycleEditorFragment(); 377 dialog.setArguments(args); 378 dialog.setTargetFragment(parent, 0); 379 dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR); 380 } 381 382 @Override getMetricsCategory()383 public int getMetricsCategory() { 384 return SettingsEnums.DIALOG_BILLING_CYCLE; 385 } 386 387 @Override onCreateDialog(Bundle savedInstanceState)388 public Dialog onCreateDialog(Bundle savedInstanceState) { 389 final Context context = getActivity(); 390 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 391 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 392 393 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 394 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); 395 396 final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false); 397 mCycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day); 398 399 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 400 final int cycleDay = editor.getPolicyCycleDay(template); 401 402 mCycleDayPicker.setMinValue(1); 403 mCycleDayPicker.setMaxValue(31); 404 mCycleDayPicker.setValue(cycleDay); 405 mCycleDayPicker.setWrapSelectorWheel(true); 406 407 Dialog dialog = builder.setTitle(R.string.data_usage_cycle_editor_title) 408 .setView(view) 409 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 410 .create(); 411 dialog.setCanceledOnTouchOutside(false); 412 return dialog; 413 } 414 415 @Override onClick(DialogInterface dialog, int which)416 public void onClick(DialogInterface dialog, int which) { 417 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 418 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 419 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 420 421 // clear focus to finish pending text edits 422 mCycleDayPicker.clearFocus(); 423 424 final int cycleDay = mCycleDayPicker.getValue(); 425 final String cycleTimezone = TimeZone.getDefault().getID(); 426 editor.setPolicyCycleDay(template, cycleDay, cycleTimezone); 427 target.updateDataUsage(); 428 } 429 } 430 431 /** 432 * Dialog to request user confirmation before setting 433 * {@link NetworkPolicy#limitBytes}. 434 */ 435 public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements 436 DialogInterface.OnClickListener { 437 @VisibleForTesting 438 static final String EXTRA_LIMIT_BYTES = "limitBytes"; 439 public static final float FLOAT = 1.2f; 440 show(BillingCycleSettings parent)441 public static void show(BillingCycleSettings parent) { 442 if (!parent.isAdded()) return; 443 444 final NetworkPolicy policy = parent.services.mPolicyEditor 445 .getPolicy(parent.mNetworkTemplate); 446 if (policy == null) return; 447 448 final Resources res = parent.getResources(); 449 final long minLimitBytes = (long) (policy.warningBytes * FLOAT); 450 final long limitBytes; 451 452 // TODO: customize default limits based on network template 453 limitBytes = Math.max(5 * GIB_IN_BYTES, minLimitBytes); 454 455 final Bundle args = new Bundle(); 456 args.putLong(EXTRA_LIMIT_BYTES, limitBytes); 457 458 final ConfirmLimitFragment dialog = new ConfirmLimitFragment(); 459 dialog.setArguments(args); 460 dialog.setTargetFragment(parent, 0); 461 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT); 462 } 463 464 @Override getMetricsCategory()465 public int getMetricsCategory() { 466 return SettingsEnums.DIALOG_BILLING_CONFIRM_LIMIT; 467 } 468 469 @Override onCreateDialog(Bundle savedInstanceState)470 public Dialog onCreateDialog(Bundle savedInstanceState) { 471 final Context context = getActivity(); 472 473 Dialog dialog = new AlertDialog.Builder(context) 474 .setTitle(R.string.data_usage_limit_dialog_title) 475 .setMessage(R.string.data_usage_limit_dialog_mobile) 476 .setPositiveButton(android.R.string.ok, this) 477 .setNegativeButton(android.R.string.cancel, null) 478 .create(); 479 dialog.setCanceledOnTouchOutside(false); 480 return dialog; 481 } 482 483 @Override onClick(DialogInterface dialog, int which)484 public void onClick(DialogInterface dialog, int which) { 485 final BillingCycleSettings target = (BillingCycleSettings) getTargetFragment(); 486 if (which != DialogInterface.BUTTON_POSITIVE) return; 487 final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES); 488 if (target != null) { 489 target.setPolicyLimitBytes(limitBytes); 490 } 491 target.getPreferenceManager().getSharedPreferences().edit() 492 .putBoolean(KEY_SET_DATA_LIMIT, true).apply(); 493 } 494 } 495 496 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 497 new BaseSearchIndexProvider(R.xml.billing_cycle) { 498 499 @Override 500 protected boolean isPageSearchEnabled(Context context) { 501 return DataUsageUtils.hasMobileData(context); 502 } 503 }; 504 505 } 506