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.SearchIndexableResource; 29 import android.text.format.Time; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 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.settings.search.Indexable; 47 import com.android.settingslib.NetworkPolicyEditor; 48 import com.android.settingslib.net.DataUsageController; 49 import com.android.settingslib.search.SearchIndexable; 50 51 import java.util.ArrayList; 52 import java.util.List; 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 mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.data_warning_footnote); 123 } 124 125 @Override onResume()126 public void onResume() { 127 super.onResume(); 128 updatePrefs(); 129 } 130 131 @VisibleForTesting updatePrefs()132 void updatePrefs() { 133 mBillingCycle.setSummary(null); 134 final long warningBytes = services.mPolicyEditor.getPolicyWarningBytes(mNetworkTemplate); 135 if (warningBytes != WARNING_DISABLED) { 136 mDataWarning.setSummary(DataUsageUtils.formatDataUsage(getContext(), warningBytes)); 137 mDataWarning.setEnabled(true); 138 mEnableDataWarning.setChecked(true); 139 } else { 140 mDataWarning.setSummary(null); 141 mDataWarning.setEnabled(false); 142 mEnableDataWarning.setChecked(false); 143 } 144 final long limitBytes = services.mPolicyEditor.getPolicyLimitBytes(mNetworkTemplate); 145 if (limitBytes != LIMIT_DISABLED) { 146 mDataLimit.setSummary(DataUsageUtils.formatDataUsage(getContext(), limitBytes)); 147 mDataLimit.setEnabled(true); 148 mEnableDataLimit.setChecked(true); 149 } else { 150 mDataLimit.setSummary(null); 151 mDataLimit.setEnabled(false); 152 mEnableDataLimit.setChecked(false); 153 } 154 } 155 156 @Override onPreferenceTreeClick(Preference preference)157 public boolean onPreferenceTreeClick(Preference preference) { 158 if (preference == mBillingCycle) { 159 CycleEditorFragment.show(this); 160 return true; 161 } else if (preference == mDataWarning) { 162 BytesEditorFragment.show(this, false); 163 return true; 164 } else if (preference == mDataLimit) { 165 BytesEditorFragment.show(this, true); 166 return true; 167 } 168 return super.onPreferenceTreeClick(preference); 169 } 170 171 @Override onPreferenceChange(Preference preference, Object newValue)172 public boolean onPreferenceChange(Preference preference, Object newValue) { 173 if (mEnableDataLimit == preference) { 174 boolean enabled = (Boolean) newValue; 175 if (!enabled) { 176 setPolicyLimitBytes(LIMIT_DISABLED); 177 return true; 178 } 179 ConfirmLimitFragment.show(this); 180 // This preference is enabled / disabled by ConfirmLimitFragment. 181 return false; 182 } else if (mEnableDataWarning == preference) { 183 boolean enabled = (Boolean) newValue; 184 if (enabled) { 185 setPolicyWarningBytes(mDataUsageController.getDefaultWarningLevel()); 186 } else { 187 setPolicyWarningBytes(WARNING_DISABLED); 188 } 189 return true; 190 } 191 return false; 192 } 193 194 @Override getMetricsCategory()195 public int getMetricsCategory() { 196 return SettingsEnums.BILLING_CYCLE; 197 } 198 199 @Override getPreferenceScreenResId()200 protected int getPreferenceScreenResId() { 201 return R.xml.billing_cycle; 202 } 203 204 @Override getLogTag()205 protected String getLogTag() { 206 return TAG; 207 } 208 209 @VisibleForTesting setPolicyLimitBytes(long limitBytes)210 void setPolicyLimitBytes(long limitBytes) { 211 if (LOGD) Log.d(TAG, "setPolicyLimitBytes()"); 212 services.mPolicyEditor.setPolicyLimitBytes(mNetworkTemplate, limitBytes); 213 updatePrefs(); 214 } 215 setPolicyWarningBytes(long warningBytes)216 private void setPolicyWarningBytes(long warningBytes) { 217 if (LOGD) Log.d(TAG, "setPolicyWarningBytes()"); 218 services.mPolicyEditor.setPolicyWarningBytes(mNetworkTemplate, warningBytes); 219 updatePrefs(); 220 } 221 222 @Override getNetworkPolicyEditor()223 public NetworkPolicyEditor getNetworkPolicyEditor() { 224 return services.mPolicyEditor; 225 } 226 227 @Override getNetworkTemplate()228 public NetworkTemplate getNetworkTemplate() { 229 return mNetworkTemplate; 230 } 231 232 @Override updateDataUsage()233 public void updateDataUsage() { 234 updatePrefs(); 235 } 236 237 /** 238 * Dialog to edit {@link NetworkPolicy#warningBytes}. 239 */ 240 public static class BytesEditorFragment extends InstrumentedDialogFragment 241 implements DialogInterface.OnClickListener { 242 private static final String EXTRA_TEMPLATE = "template"; 243 private static final String EXTRA_LIMIT = "limit"; 244 private View mView; 245 show(DataUsageEditController parent, boolean isLimit)246 public static void show(DataUsageEditController parent, boolean isLimit) { 247 if (!(parent instanceof Fragment)) { 248 return; 249 } 250 Fragment targetFragment = (Fragment) parent; 251 if (!targetFragment.isAdded()) { 252 return; 253 } 254 255 final Bundle args = new Bundle(); 256 args.putParcelable(EXTRA_TEMPLATE, parent.getNetworkTemplate()); 257 args.putBoolean(EXTRA_LIMIT, isLimit); 258 259 final BytesEditorFragment dialog = new BytesEditorFragment(); 260 dialog.setArguments(args); 261 dialog.setTargetFragment(targetFragment, 0); 262 dialog.show(targetFragment.getFragmentManager(), TAG_WARNING_EDITOR); 263 } 264 265 @Override onCreateDialog(Bundle savedInstanceState)266 public Dialog onCreateDialog(Bundle savedInstanceState) { 267 final Context context = getActivity(); 268 final LayoutInflater dialogInflater = LayoutInflater.from(context); 269 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 270 mView = dialogInflater.inflate(R.layout.data_usage_bytes_editor, null, false); 271 setupPicker((EditText) mView.findViewById(R.id.bytes), 272 (Spinner) mView.findViewById(R.id.size_spinner)); 273 return new AlertDialog.Builder(context) 274 .setTitle(isLimit ? R.string.data_usage_limit_editor_title 275 : R.string.data_usage_warning_editor_title) 276 .setView(mView) 277 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 278 .create(); 279 } 280 setupPicker(EditText bytesPicker, Spinner type)281 private void setupPicker(EditText bytesPicker, Spinner type) { 282 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 283 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 284 285 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 286 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 287 final long bytes = isLimit ? editor.getPolicyLimitBytes(template) 288 : editor.getPolicyWarningBytes(template); 289 final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED; 290 291 if (bytes > 1.5f * GIB_IN_BYTES) { 292 final String bytesText = formatText(bytes / (float) GIB_IN_BYTES); 293 bytesPicker.setText(bytesText); 294 bytesPicker.setSelection(0, bytesText.length()); 295 296 type.setSelection(1); 297 } else { 298 final String bytesText = formatText(bytes / (float) MIB_IN_BYTES); 299 bytesPicker.setText(bytesText); 300 bytesPicker.setSelection(0, bytesText.length()); 301 302 type.setSelection(0); 303 } 304 } 305 formatText(float v)306 private String formatText(float v) { 307 v = Math.round(v * 100) / 100f; 308 return String.valueOf(v); 309 } 310 311 @Override onClick(DialogInterface dialog, int which)312 public void onClick(DialogInterface dialog, int which) { 313 if (which != DialogInterface.BUTTON_POSITIVE) { 314 return; 315 } 316 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 317 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 318 319 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 320 final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT); 321 EditText bytesField = (EditText) mView.findViewById(R.id.bytes); 322 Spinner spinner = (Spinner) mView.findViewById(R.id.size_spinner); 323 324 String bytesString = bytesField.getText().toString(); 325 if (bytesString.isEmpty() || bytesString.equals(".")) { 326 bytesString = "0"; 327 } 328 final long bytes = (long) (Float.valueOf(bytesString) 329 * (spinner.getSelectedItemPosition() == 0 ? MIB_IN_BYTES : GIB_IN_BYTES)); 330 331 // to fix the overflow problem 332 final long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes); 333 if (isLimit) { 334 editor.setPolicyLimitBytes(template, correctedBytes); 335 } else { 336 editor.setPolicyWarningBytes(template, correctedBytes); 337 } 338 target.updateDataUsage(); 339 } 340 341 @Override getMetricsCategory()342 public int getMetricsCategory() { 343 return SettingsEnums.DIALOG_BILLING_BYTE_LIMIT; 344 } 345 } 346 347 /** 348 * Dialog to edit {@link NetworkPolicy}. 349 */ 350 public static class CycleEditorFragment extends InstrumentedDialogFragment implements 351 DialogInterface.OnClickListener { 352 private static final String EXTRA_TEMPLATE = "template"; 353 private NumberPicker mCycleDayPicker; 354 show(BillingCycleSettings parent)355 public static void show(BillingCycleSettings parent) { 356 if (!parent.isAdded()) return; 357 358 final Bundle args = new Bundle(); 359 args.putParcelable(EXTRA_TEMPLATE, parent.mNetworkTemplate); 360 361 final CycleEditorFragment dialog = new CycleEditorFragment(); 362 dialog.setArguments(args); 363 dialog.setTargetFragment(parent, 0); 364 dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR); 365 } 366 367 @Override getMetricsCategory()368 public int getMetricsCategory() { 369 return SettingsEnums.DIALOG_BILLING_CYCLE; 370 } 371 372 @Override onCreateDialog(Bundle savedInstanceState)373 public Dialog onCreateDialog(Bundle savedInstanceState) { 374 final Context context = getActivity(); 375 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 376 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 377 378 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 379 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); 380 381 final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false); 382 mCycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day); 383 384 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 385 final int cycleDay = editor.getPolicyCycleDay(template); 386 387 mCycleDayPicker.setMinValue(1); 388 mCycleDayPicker.setMaxValue(31); 389 mCycleDayPicker.setValue(cycleDay); 390 mCycleDayPicker.setWrapSelectorWheel(true); 391 392 return builder.setTitle(R.string.data_usage_cycle_editor_title) 393 .setView(view) 394 .setPositiveButton(R.string.data_usage_cycle_editor_positive, this) 395 .create(); 396 } 397 398 @Override onClick(DialogInterface dialog, int which)399 public void onClick(DialogInterface dialog, int which) { 400 final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE); 401 final DataUsageEditController target = (DataUsageEditController) getTargetFragment(); 402 final NetworkPolicyEditor editor = target.getNetworkPolicyEditor(); 403 404 // clear focus to finish pending text edits 405 mCycleDayPicker.clearFocus(); 406 407 final int cycleDay = mCycleDayPicker.getValue(); 408 final String cycleTimezone = new Time().timezone; 409 editor.setPolicyCycleDay(template, cycleDay, cycleTimezone); 410 target.updateDataUsage(); 411 } 412 } 413 414 /** 415 * Dialog to request user confirmation before setting 416 * {@link NetworkPolicy#limitBytes}. 417 */ 418 public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements 419 DialogInterface.OnClickListener { 420 @VisibleForTesting 421 static final String EXTRA_LIMIT_BYTES = "limitBytes"; 422 public static final float FLOAT = 1.2f; 423 show(BillingCycleSettings parent)424 public static void show(BillingCycleSettings parent) { 425 if (!parent.isAdded()) return; 426 427 final NetworkPolicy policy = parent.services.mPolicyEditor 428 .getPolicy(parent.mNetworkTemplate); 429 if (policy == null) return; 430 431 final Resources res = parent.getResources(); 432 final long minLimitBytes = (long) (policy.warningBytes * FLOAT); 433 final long limitBytes; 434 435 // TODO: customize default limits based on network template 436 limitBytes = Math.max(5 * GIB_IN_BYTES, minLimitBytes); 437 438 final Bundle args = new Bundle(); 439 args.putLong(EXTRA_LIMIT_BYTES, limitBytes); 440 441 final ConfirmLimitFragment dialog = new ConfirmLimitFragment(); 442 dialog.setArguments(args); 443 dialog.setTargetFragment(parent, 0); 444 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT); 445 } 446 447 @Override getMetricsCategory()448 public int getMetricsCategory() { 449 return SettingsEnums.DIALOG_BILLING_CONFIRM_LIMIT; 450 } 451 452 @Override onCreateDialog(Bundle savedInstanceState)453 public Dialog onCreateDialog(Bundle savedInstanceState) { 454 final Context context = getActivity(); 455 456 return new AlertDialog.Builder(context) 457 .setTitle(R.string.data_usage_limit_dialog_title) 458 .setMessage(R.string.data_usage_limit_dialog_mobile) 459 .setPositiveButton(android.R.string.ok, this) 460 .setNegativeButton(android.R.string.cancel, null) 461 .create(); 462 } 463 464 @Override onClick(DialogInterface dialog, int which)465 public void onClick(DialogInterface dialog, int which) { 466 final BillingCycleSettings target = (BillingCycleSettings) getTargetFragment(); 467 if (which != DialogInterface.BUTTON_POSITIVE) return; 468 final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES); 469 if (target != null) { 470 target.setPolicyLimitBytes(limitBytes); 471 } 472 target.getPreferenceManager().getSharedPreferences().edit() 473 .putBoolean(KEY_SET_DATA_LIMIT, true).apply(); 474 } 475 } 476 477 public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 478 new BaseSearchIndexProvider() { 479 @Override 480 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, 481 boolean enabled) { 482 final ArrayList<SearchIndexableResource> result = new ArrayList<>(); 483 484 final SearchIndexableResource sir = new SearchIndexableResource(context); 485 sir.xmlResId = R.xml.billing_cycle; 486 result.add(sir); 487 return result; 488 } 489 490 @Override 491 protected boolean isPageSearchEnabled(Context context) { 492 return DataUsageUtils.hasMobileData(context); 493 } 494 }; 495 496 } 497