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