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