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