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 return 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 } 281 setupPicker(EditText bytesPicker, Spinner type)282 private void setupPicker(EditText bytesPicker, Spinner type) { 283 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 284 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 285 286 bytesPicker.setKeyListener(new NumberKeyListener() { 287 protected char[] getAcceptedChars() { 288 return new char [] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 289 ',', '.'}; 290 } 291 public int getInputType() { 292 return EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; 293 } 294 }); 295 296 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 297 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 298 final long bytes = isLimit ? editor.getPolicyLimitBytes(template) 299 : editor.getPolicyWarningBytes(template); 300 final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED; 301 302 final boolean unitInGigaBytes = (bytes > 1.5f * GIB_IN_BYTES); 303 final String bytesText = formatText(bytes, 304 unitInGigaBytes ? GIB_IN_BYTES : MIB_IN_BYTES); 305 bytesPicker.setText(bytesText); 306 bytesPicker.setSelection(0, bytesText.length()); 307 308 type.setSelection(unitInGigaBytes ? 1 : 0); 309 } 310 formatText(double v, double unitInBytes)311 private String formatText(double v, double unitInBytes) { 312 final NumberFormat formatter = NumberFormat.getNumberInstance(); 313 formatter.setMaximumFractionDigits(2); 314 return formatter.format((double) (v / unitInBytes)); 315 } 316 317 @Override onClick(DialogInterface dialog, int which)318 public void onClick(DialogInterface dialog, int which) { 319 if (which != DialogInterface.BUTTON_POSITIVE) { 320 return; 321 } 322 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 323 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 324 325 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 326 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 327 final EditText bytesField = (EditText) mView.findViewById(R.id.bytes); 328 final Spinner spinner = (Spinner) mView.findViewById(R.id.size_spinner); 329 330 final String bytesString = bytesField.getText().toString(); 331 332 final NumberFormat formatter = NumberFormat.getNumberInstance(); 333 Number number = null; 334 try { 335 number = formatter.parse(bytesString); 336 } catch (ParseException ex) { 337 } 338 long bytes = 0L; 339 if (number != null) { 340 bytes = (long) (number.floatValue() 341 * (spinner.getSelectedItemPosition() == 0 ? MIB_IN_BYTES : GIB_IN_BYTES)); 342 } 343 344 // to fix the overflow problem 345 final long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes); 346 if (isLimit) { 347 editor.setPolicyLimitBytes(template, correctedBytes); 348 } else { 349 editor.setPolicyWarningBytes(template, correctedBytes); 350 } 351 target.updateDataUsage(); 352 } 353 354 @Override getMetricsCategory()355 public int getMetricsCategory() { 356 return SettingsEnums.DIALOG_BILLING_BYTE_LIMIT; 357 } 358 } 359 360 /** 361 * Dialog to edit {@link NetworkPolicy}. 362 */ 363 public static class CycleEditorFragment extends InstrumentedDialogFragment implements 364 DialogInterface.OnClickListener { 365 private static final String EXTRA_TEMPLATE = "template"; 366 private NumberPicker mCycleDayPicker; 367 show(BillingCycleSettings parent)368 public static void show(BillingCycleSettings parent) { 369 if (!parent.isAdded()) return; 370 371 final Bundle args = new Bundle(); 372 args.putParcelable(EXTRA_TEMPLATE, parent.mNetworkTemplate); 373 374 final CycleEditorFragment dialog = new CycleEditorFragment(); 375 dialog.setArguments(args); 376 dialog.setTargetFragment(parent, 0); 377 dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR); 378 } 379 380 @Override getMetricsCategory()381 public int getMetricsCategory() { 382 return SettingsEnums.DIALOG_BILLING_CYCLE; 383 } 384 385 @Override onCreateDialog(Bundle savedInstanceState)386 public Dialog onCreateDialog(Bundle savedInstanceState) { 387 final Context context = getActivity(); 388 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 389 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 390 391 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 392 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); 393 394 final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false); 395 mCycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day); 396 397 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 398 final int cycleDay = editor.getPolicyCycleDay(template); 399 400 mCycleDayPicker.setMinValue(1); 401 mCycleDayPicker.setMaxValue(31); 402 mCycleDayPicker.setValue(cycleDay); 403 mCycleDayPicker.setWrapSelectorWheel(true); 404 405 return builder.setTitle(R.string.data_usage_cycle_editor_title) 406 .setView(view) 407 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 408 .create(); 409 } 410 411 @Override onClick(DialogInterface dialog, int which)412 public void onClick(DialogInterface dialog, int which) { 413 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 414 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 415 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 416 417 // clear focus to finish pending text edits 418 mCycleDayPicker.clearFocus(); 419 420 final int cycleDay = mCycleDayPicker.getValue(); 421 final String cycleTimezone = TimeZone.getDefault().getID(); 422 editor.setPolicyCycleDay(template, cycleDay, cycleTimezone); 423 target.updateDataUsage(); 424 } 425 } 426 427 /** 428 * Dialog to request user confirmation before setting 429 * {@link NetworkPolicy#limitBytes}. 430 */ 431 public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements 432 DialogInterface.OnClickListener { 433 @VisibleForTesting 434 static final String EXTRA_LIMIT_BYTES = "limitBytes"; 435 public static final float FLOAT = 1.2f; 436 show(BillingCycleSettings parent)437 public static void show(BillingCycleSettings parent) { 438 if (!parent.isAdded()) return; 439 440 final NetworkPolicy policy = parent.services.mPolicyEditor 441 .getPolicy(parent.mNetworkTemplate); 442 if (policy == null) return; 443 444 final Resources res = parent.getResources(); 445 final long minLimitBytes = (long) (policy.warningBytes * FLOAT); 446 final long limitBytes; 447 448 // TODO: customize default limits based on network template 449 limitBytes = Math.max(5 * GIB_IN_BYTES, minLimitBytes); 450 451 final Bundle args = new Bundle(); 452 args.putLong(EXTRA_LIMIT_BYTES, limitBytes); 453 454 final ConfirmLimitFragment dialog = new ConfirmLimitFragment(); 455 dialog.setArguments(args); 456 dialog.setTargetFragment(parent, 0); 457 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT); 458 } 459 460 @Override getMetricsCategory()461 public int getMetricsCategory() { 462 return SettingsEnums.DIALOG_BILLING_CONFIRM_LIMIT; 463 } 464 465 @Override onCreateDialog(Bundle savedInstanceState)466 public Dialog onCreateDialog(Bundle savedInstanceState) { 467 final Context context = getActivity(); 468 469 return new AlertDialog.Builder(context) 470 .setTitle(R.string.data_usage_limit_dialog_title) 471 .setMessage(R.string.data_usage_limit_dialog_mobile) 472 .setPositiveButton(android.R.string.ok, this) 473 .setNegativeButton(android.R.string.cancel, null) 474 .create(); 475 } 476 477 @Override onClick(DialogInterface dialog, int which)478 public void onClick(DialogInterface dialog, int which) { 479 final BillingCycleSettings target = (BillingCycleSettings) getTargetFragment(); 480 if (which != DialogInterface.BUTTON_POSITIVE) return; 481 final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES); 482 if (target != null) { 483 target.setPolicyLimitBytes(limitBytes); 484 } 485 target.getPreferenceManager().getSharedPreferences().edit() 486 .putBoolean(KEY_SET_DATA_LIMIT, true).apply(); 487 } 488 } 489 490 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 491 new BaseSearchIndexProvider(R.xml.billing_cycle) { 492 493 @Override 494 protected boolean isPageSearchEnabled(Context context) { 495 return DataUsageUtils.hasMobileData(context); 496 } 497 }; 498 499 } 500