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