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