Home | History | Annotate | Download | only in datausage
      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