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 if (warningBytes != WARNING_DISABLED) { 156 mDataWarning.setSummary(DataUsageUtils.formatDataUsage(getContext(), warningBytes)); 157 mDataWarning.setEnabled(true); 158 mEnableDataWarning.setChecked(true); 159 } else { 160 mDataWarning.setSummary(null); 161 mDataWarning.setEnabled(false); 162 mEnableDataWarning.setChecked(false); 163 } 164 final long limitBytes = services.mPolicyEditor.getPolicyLimitBytes(mNetworkTemplate); 165 if (limitBytes != LIMIT_DISABLED) { 166 mDataLimit.setSummary(DataUsageUtils.formatDataUsage(getContext(), limitBytes)); 167 mDataLimit.setEnabled(true); 168 mEnableDataLimit.setChecked(true); 169 } else { 170 mDataLimit.setSummary(null); 171 mDataLimit.setEnabled(false); 172 mEnableDataLimit.setChecked(false); 173 } 174 } 175 176 @Override onPreferenceTreeClick(Preference preference)177 public boolean onPreferenceTreeClick(Preference preference) { 178 if (preference == mBillingCycle) { 179 writePreferenceClickMetric(preference); 180 CycleEditorFragment.show(this); 181 return true; 182 } else if (preference == mDataWarning) { 183 writePreferenceClickMetric(preference); 184 BytesEditorFragment.show(this, false); 185 return true; 186 } else if (preference == mDataLimit) { 187 writePreferenceClickMetric(preference); 188 BytesEditorFragment.show(this, true); 189 return true; 190 } 191 return super.onPreferenceTreeClick(preference); 192 } 193 194 @Override onPreferenceChange(Preference preference, Object newValue)195 public boolean onPreferenceChange(Preference preference, Object newValue) { 196 if (mEnableDataLimit == preference) { 197 boolean enabled = (Boolean) newValue; 198 if (!enabled) { 199 setPolicyLimitBytes(LIMIT_DISABLED); 200 return true; 201 } 202 ConfirmLimitFragment.show(this); 203 // This preference is enabled / disabled by ConfirmLimitFragment. 204 return false; 205 } else if (mEnableDataWarning == preference) { 206 boolean enabled = (Boolean) newValue; 207 if (enabled) { 208 setPolicyWarningBytes(mDataUsageController.getDefaultWarningLevel()); 209 } else { 210 setPolicyWarningBytes(WARNING_DISABLED); 211 } 212 return true; 213 } 214 return false; 215 } 216 217 @Override getMetricsCategory()218 public int getMetricsCategory() { 219 return SettingsEnums.BILLING_CYCLE; 220 } 221 222 @Override getPreferenceScreenResId()223 protected int getPreferenceScreenResId() { 224 return R.xml.billing_cycle; 225 } 226 227 @Override getLogTag()228 protected String getLogTag() { 229 return TAG; 230 } 231 232 @VisibleForTesting setPolicyLimitBytes(long limitBytes)233 void setPolicyLimitBytes(long limitBytes) { 234 if (LOGD) Log.d(TAG, "setPolicyLimitBytes()"); 235 services.mPolicyEditor.setPolicyLimitBytes(mNetworkTemplate, limitBytes); 236 updatePrefs(); 237 } 238 setPolicyWarningBytes(long warningBytes)239 private void setPolicyWarningBytes(long warningBytes) { 240 if (LOGD) Log.d(TAG, "setPolicyWarningBytes()"); 241 services.mPolicyEditor.setPolicyWarningBytes(mNetworkTemplate, warningBytes); 242 updatePrefs(); 243 } 244 245 @Override getNetworkPolicyEditor()246 public NetworkPolicyEditor getNetworkPolicyEditor() { 247 return services.mPolicyEditor; 248 } 249 250 @Override getNetworkTemplate()251 public NetworkTemplate getNetworkTemplate() { 252 return mNetworkTemplate; 253 } 254 255 @Override updateDataUsage()256 public void updateDataUsage() { 257 updatePrefs(); 258 } 259 260 /** 261 * Dialog to edit {@link NetworkPolicy#warningBytes}. 262 */ 263 public static class BytesEditorFragment extends InstrumentedDialogFragment 264 implements DialogInterface.OnClickListener { 265 private static final String EXTRA_TEMPLATE = "template"; 266 private static final String EXTRA_LIMIT = "limit"; 267 private View mView; 268 show(DataUsageEditController parent, boolean isLimit)269 public static void show(DataUsageEditController parent, boolean isLimit) { 270 if (!(parent instanceof Fragment)) { 271 return; 272 } 273 Fragment targetFragment = (Fragment) parent; 274 if (!targetFragment.isAdded()) { 275 return; 276 } 277 278 final Bundle args = new Bundle(); 279 args.putParcelable(EXTRA_TEMPLATE, parent.getNetworkTemplate()); 280 args.putBoolean(EXTRA_LIMIT, isLimit); 281 282 final BytesEditorFragment dialog = new BytesEditorFragment(); 283 dialog.setArguments(args); 284 dialog.setTargetFragment(targetFragment, 0); 285 dialog.show(targetFragment.getFragmentManager(), TAG_WARNING_EDITOR); 286 } 287 288 @Override onCreateDialog(Bundle savedInstanceState)289 public Dialog onCreateDialog(Bundle savedInstanceState) { 290 final Context context = getActivity(); 291 final LayoutInflater dialogInflater = LayoutInflater.from(context); 292 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 293 mView = dialogInflater.inflate(R.layout.data_usage_bytes_editor, null, false); 294 setupPicker((EditText) mView.findViewById(R.id.bytes), 295 (Spinner) mView.findViewById(R.id.size_spinner)); 296 Dialog dialog = new AlertDialog.Builder(context) 297 .setTitle(isLimit ? R.string.data_usage_limit_editor_title 298 : R.string.data_usage_warning_editor_title) 299 .setView(mView) 300 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 301 .create(); 302 dialog.setCanceledOnTouchOutside(false); 303 return dialog; 304 } 305 setupPicker(EditText bytesPicker, Spinner type)306 private void setupPicker(EditText bytesPicker, Spinner type) { 307 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 308 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 309 310 bytesPicker.setKeyListener(new NumberKeyListener() { 311 protected char[] getAcceptedChars() { 312 return new char [] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 313 ',', '.'}; 314 } 315 public int getInputType() { 316 return EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; 317 } 318 }); 319 320 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 321 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 322 final long bytes = isLimit ? editor.getPolicyLimitBytes(template) 323 : editor.getPolicyWarningBytes(template); 324 325 final String[] unitNames = new String[] { 326 DataUsageFormatter.Companion.getBytesDisplayUnit(getResources(), MIB_IN_BYTES), 327 DataUsageFormatter.Companion.getBytesDisplayUnit(getResources(), GIB_IN_BYTES), 328 }; 329 final ArrayAdapter<String> adapter = new ArrayAdapter<String>( 330 getContext(), android.R.layout.simple_spinner_item, unitNames); 331 type.setAdapter(adapter); 332 333 final boolean unitInGigaBytes = (bytes > 1.5f * GIB_IN_BYTES); 334 final String bytesText = formatText(bytes, 335 unitInGigaBytes ? GIB_IN_BYTES : MIB_IN_BYTES); 336 bytesPicker.setText(bytesText); 337 bytesPicker.setSelection(0, bytesText.length()); 338 339 type.setSelection(unitInGigaBytes ? 1 : 0); 340 } 341 formatText(double v, double unitInBytes)342 private String formatText(double v, double unitInBytes) { 343 final NumberFormat formatter = NumberFormat.getNumberInstance(); 344 formatter.setMaximumFractionDigits(2); 345 return formatter.format((double) (v / unitInBytes)); 346 } 347 348 @Override onClick(DialogInterface dialog, int which)349 public void onClick(DialogInterface dialog, int which) { 350 if (which != DialogInterface.BUTTON_POSITIVE) { 351 return; 352 } 353 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 354 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 355 356 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 357 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 358 final EditText bytesField = (EditText) mView.findViewById(R.id.bytes); 359 final Spinner spinner = (Spinner) mView.findViewById(R.id.size_spinner); 360 361 final String bytesString = bytesField.getText().toString(); 362 363 final NumberFormat formatter = NumberFormat.getNumberInstance(); 364 Number number = null; 365 try { 366 number = formatter.parse(bytesString); 367 } catch (ParseException ex) { 368 } 369 long bytes = 0L; 370 if (number != null) { 371 bytes = (long) (number.floatValue() 372 * (spinner.getSelectedItemPosition() == 0 ? MIB_IN_BYTES : GIB_IN_BYTES)); 373 } 374 375 // to fix the overflow problem 376 final long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes); 377 if (isLimit) { 378 editor.setPolicyLimitBytes(template, correctedBytes); 379 } else { 380 editor.setPolicyWarningBytes(template, correctedBytes); 381 } 382 target.updateDataUsage(); 383 } 384 385 @Override getMetricsCategory()386 public int getMetricsCategory() { 387 return SettingsEnums.DIALOG_BILLING_BYTE_LIMIT; 388 } 389 } 390 391 /** 392 * Dialog to edit {@link NetworkPolicy}. 393 */ 394 public static class CycleEditorFragment extends InstrumentedDialogFragment implements 395 DialogInterface.OnClickListener { 396 private static final String EXTRA_TEMPLATE = "template"; 397 private NumberPicker mCycleDayPicker; 398 show(BillingCycleSettings parent)399 public static void show(BillingCycleSettings parent) { 400 if (!parent.isAdded()) return; 401 402 final Bundle args = new Bundle(); 403 args.putParcelable(EXTRA_TEMPLATE, parent.mNetworkTemplate); 404 405 final CycleEditorFragment dialog = new CycleEditorFragment(); 406 dialog.setArguments(args); 407 dialog.setTargetFragment(parent, 0); 408 dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR); 409 } 410 411 @Override getMetricsCategory()412 public int getMetricsCategory() { 413 return SettingsEnums.DIALOG_BILLING_CYCLE; 414 } 415 416 @Override onCreateDialog(Bundle savedInstanceState)417 public Dialog onCreateDialog(Bundle savedInstanceState) { 418 final Context context = getActivity(); 419 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 420 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 421 422 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 423 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); 424 425 final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false); 426 mCycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day); 427 428 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 429 final int cycleDay = editor.getPolicyCycleDay(template); 430 431 mCycleDayPicker.setMinValue(1); 432 mCycleDayPicker.setMaxValue(31); 433 mCycleDayPicker.setValue(cycleDay); 434 mCycleDayPicker.setWrapSelectorWheel(true); 435 436 Dialog dialog = builder.setTitle(R.string.data_usage_cycle_editor_title) 437 .setView(view) 438 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 439 .create(); 440 dialog.setCanceledOnTouchOutside(false); 441 return dialog; 442 } 443 444 @Override onClick(DialogInterface dialog, int which)445 public void onClick(DialogInterface dialog, int which) { 446 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 447 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 448 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 449 450 // clear focus to finish pending text edits 451 mCycleDayPicker.clearFocus(); 452 453 final int cycleDay = mCycleDayPicker.getValue(); 454 final String cycleTimezone = TimeZone.getDefault().getID(); 455 editor.setPolicyCycleDay(template, cycleDay, cycleTimezone); 456 target.updateDataUsage(); 457 } 458 } 459 460 /** 461 * Dialog to request user confirmation before setting 462 * {@link NetworkPolicy#limitBytes}. 463 */ 464 public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements 465 DialogInterface.OnClickListener { 466 @VisibleForTesting 467 static final String EXTRA_LIMIT_BYTES = "limitBytes"; 468 public static final float FLOAT = 1.2f; 469 show(BillingCycleSettings parent)470 public static void show(BillingCycleSettings parent) { 471 if (!parent.isAdded()) return; 472 473 final NetworkPolicy policy = parent.services.mPolicyEditor 474 .getPolicy(parent.mNetworkTemplate); 475 if (policy == null) return; 476 477 final Resources res = parent.getResources(); 478 final long minLimitBytes = (long) (policy.warningBytes * FLOAT); 479 final long limitBytes; 480 481 // TODO: customize default limits based on network template 482 limitBytes = Math.max(5 * GIB_IN_BYTES, minLimitBytes); 483 484 final Bundle args = new Bundle(); 485 args.putLong(EXTRA_LIMIT_BYTES, limitBytes); 486 487 final ConfirmLimitFragment dialog = new ConfirmLimitFragment(); 488 dialog.setArguments(args); 489 dialog.setTargetFragment(parent, 0); 490 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT); 491 } 492 493 @Override getMetricsCategory()494 public int getMetricsCategory() { 495 return SettingsEnums.DIALOG_BILLING_CONFIRM_LIMIT; 496 } 497 498 @Override onCreateDialog(Bundle savedInstanceState)499 public Dialog onCreateDialog(Bundle savedInstanceState) { 500 final Context context = getActivity(); 501 502 Dialog dialog = new AlertDialog.Builder(context) 503 .setTitle(R.string.data_usage_limit_dialog_title) 504 .setMessage(R.string.data_usage_limit_dialog_mobile) 505 .setPositiveButton(android.R.string.ok, this) 506 .setNegativeButton(android.R.string.cancel, null) 507 .create(); 508 dialog.setCanceledOnTouchOutside(false); 509 return dialog; 510 } 511 512 @Override onClick(DialogInterface dialog, int which)513 public void onClick(DialogInterface dialog, int which) { 514 final BillingCycleSettings target = (BillingCycleSettings) getTargetFragment(); 515 if (which != DialogInterface.BUTTON_POSITIVE) return; 516 final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES); 517 if (target != null) { 518 target.setPolicyLimitBytes(limitBytes); 519 } 520 target.getPreferenceManager().getSharedPreferences().edit() 521 .putBoolean(KEY_SET_DATA_LIMIT, true).apply(); 522 } 523 } 524 525 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 526 new BaseSearchIndexProvider(R.xml.billing_cycle) { 527 528 @Override 529 protected boolean isPageSearchEnabled(Context context) { 530 return (!MobileNetworkUtils.isMobileNetworkUserRestricted(context)) 531 && SubscriptionUtil.isSimHardwareVisible(context) 532 && DataUsageUtils.hasMobileData(context); 533 } 534 }; 535 536 } 537