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