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