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