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