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