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             Dialog dialog = 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             dialog.setCanceledOnTouchOutside(false);
281             return dialog;
282         }
283 
setupPicker(EditText bytesPicker, Spinner type)284         private void setupPicker(EditText bytesPicker, Spinner type) {
285             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
286             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
287 
288             bytesPicker.setKeyListener(new NumberKeyListener() {
289                 protected char[] getAcceptedChars() {
290                     return new char [] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
291                             ',', '.'};
292                 }
293                 public int getInputType() {
294                     return EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
295                 }
296             });
297 
298             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
299             final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT);
300             final long bytes = isLimit ? editor.getPolicyLimitBytes(template)
301                     : editor.getPolicyWarningBytes(template);
302             final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED;
303 
304             final boolean unitInGigaBytes = (bytes > 1.5f * GIB_IN_BYTES);
305             final String bytesText = formatText(bytes,
306                     unitInGigaBytes ? GIB_IN_BYTES : MIB_IN_BYTES);
307             bytesPicker.setText(bytesText);
308             bytesPicker.setSelection(0, bytesText.length());
309 
310             type.setSelection(unitInGigaBytes ? 1 : 0);
311         }
312 
formatText(double v, double unitInBytes)313         private String formatText(double v, double unitInBytes) {
314             final NumberFormat formatter = NumberFormat.getNumberInstance();
315             formatter.setMaximumFractionDigits(2);
316             return formatter.format((double) (v / unitInBytes));
317         }
318 
319         @Override
onClick(DialogInterface dialog, int which)320         public void onClick(DialogInterface dialog, int which) {
321             if (which != DialogInterface.BUTTON_POSITIVE) {
322                 return;
323             }
324             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
325             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
326 
327             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
328             final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT);
329             final EditText bytesField = (EditText) mView.findViewById(R.id.bytes);
330             final Spinner spinner = (Spinner) mView.findViewById(R.id.size_spinner);
331 
332             final String bytesString = bytesField.getText().toString();
333 
334             final NumberFormat formatter = NumberFormat.getNumberInstance();
335             Number number = null;
336             try {
337                 number = formatter.parse(bytesString);
338             } catch (ParseException ex) {
339             }
340             long bytes = 0L;
341             if (number != null) {
342                 bytes = (long) (number.floatValue()
343                         * (spinner.getSelectedItemPosition() == 0 ? MIB_IN_BYTES : GIB_IN_BYTES));
344             }
345 
346             // to fix the overflow problem
347             final long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes);
348             if (isLimit) {
349                 editor.setPolicyLimitBytes(template, correctedBytes);
350             } else {
351                 editor.setPolicyWarningBytes(template, correctedBytes);
352             }
353             target.updateDataUsage();
354         }
355 
356         @Override
getMetricsCategory()357         public int getMetricsCategory() {
358             return SettingsEnums.DIALOG_BILLING_BYTE_LIMIT;
359         }
360     }
361 
362     /**
363      * Dialog to edit {@link NetworkPolicy}.
364      */
365     public static class CycleEditorFragment extends InstrumentedDialogFragment implements
366             DialogInterface.OnClickListener {
367         private static final String EXTRA_TEMPLATE = "template";
368         private NumberPicker mCycleDayPicker;
369 
show(BillingCycleSettings parent)370         public static void show(BillingCycleSettings parent) {
371             if (!parent.isAdded()) return;
372 
373             final Bundle args = new Bundle();
374             args.putParcelable(EXTRA_TEMPLATE, parent.mNetworkTemplate);
375 
376             final CycleEditorFragment dialog = new CycleEditorFragment();
377             dialog.setArguments(args);
378             dialog.setTargetFragment(parent, 0);
379             dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR);
380         }
381 
382         @Override
getMetricsCategory()383         public int getMetricsCategory() {
384             return SettingsEnums.DIALOG_BILLING_CYCLE;
385         }
386 
387         @Override
onCreateDialog(Bundle savedInstanceState)388         public Dialog onCreateDialog(Bundle savedInstanceState) {
389             final Context context = getActivity();
390             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
391             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
392 
393             final AlertDialog.Builder builder = new AlertDialog.Builder(context);
394             final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
395 
396             final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false);
397             mCycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day);
398 
399             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
400             final int cycleDay = editor.getPolicyCycleDay(template);
401 
402             mCycleDayPicker.setMinValue(1);
403             mCycleDayPicker.setMaxValue(31);
404             mCycleDayPicker.setValue(cycleDay);
405             mCycleDayPicker.setWrapSelectorWheel(true);
406 
407             Dialog dialog = builder.setTitle(R.string.data_usage_cycle_editor_title)
408                     .setView(view)
409                     .setPositiveButton(R.string.data_usage_cycle_editor_positive, this)
410                     .create();
411             dialog.setCanceledOnTouchOutside(false);
412             return dialog;
413         }
414 
415         @Override
onClick(DialogInterface dialog, int which)416         public void onClick(DialogInterface dialog, int which) {
417             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
418             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
419             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
420 
421             // clear focus to finish pending text edits
422             mCycleDayPicker.clearFocus();
423 
424             final int cycleDay = mCycleDayPicker.getValue();
425             final String cycleTimezone = TimeZone.getDefault().getID();
426             editor.setPolicyCycleDay(template, cycleDay, cycleTimezone);
427             target.updateDataUsage();
428         }
429     }
430 
431     /**
432      * Dialog to request user confirmation before setting
433      * {@link NetworkPolicy#limitBytes}.
434      */
435     public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements
436             DialogInterface.OnClickListener {
437         @VisibleForTesting
438         static final String EXTRA_LIMIT_BYTES = "limitBytes";
439         public static final float FLOAT = 1.2f;
440 
show(BillingCycleSettings parent)441         public static void show(BillingCycleSettings parent) {
442             if (!parent.isAdded()) return;
443 
444             final NetworkPolicy policy = parent.services.mPolicyEditor
445                     .getPolicy(parent.mNetworkTemplate);
446             if (policy == null) return;
447 
448             final Resources res = parent.getResources();
449             final long minLimitBytes = (long) (policy.warningBytes * FLOAT);
450             final long limitBytes;
451 
452             // TODO: customize default limits based on network template
453             limitBytes = Math.max(5 * GIB_IN_BYTES, minLimitBytes);
454 
455             final Bundle args = new Bundle();
456             args.putLong(EXTRA_LIMIT_BYTES, limitBytes);
457 
458             final ConfirmLimitFragment dialog = new ConfirmLimitFragment();
459             dialog.setArguments(args);
460             dialog.setTargetFragment(parent, 0);
461             dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT);
462         }
463 
464         @Override
getMetricsCategory()465         public int getMetricsCategory() {
466             return SettingsEnums.DIALOG_BILLING_CONFIRM_LIMIT;
467         }
468 
469         @Override
onCreateDialog(Bundle savedInstanceState)470         public Dialog onCreateDialog(Bundle savedInstanceState) {
471             final Context context = getActivity();
472 
473             Dialog dialog = new AlertDialog.Builder(context)
474                     .setTitle(R.string.data_usage_limit_dialog_title)
475                     .setMessage(R.string.data_usage_limit_dialog_mobile)
476                     .setPositiveButton(android.R.string.ok, this)
477                     .setNegativeButton(android.R.string.cancel, null)
478                     .create();
479             dialog.setCanceledOnTouchOutside(false);
480             return dialog;
481         }
482 
483         @Override
onClick(DialogInterface dialog, int which)484         public void onClick(DialogInterface dialog, int which) {
485             final BillingCycleSettings target = (BillingCycleSettings) getTargetFragment();
486             if (which != DialogInterface.BUTTON_POSITIVE) return;
487             final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES);
488             if (target != null) {
489                 target.setPolicyLimitBytes(limitBytes);
490             }
491             target.getPreferenceManager().getSharedPreferences().edit()
492                     .putBoolean(KEY_SET_DATA_LIMIT, true).apply();
493         }
494     }
495 
496     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
497             new BaseSearchIndexProvider(R.xml.billing_cycle) {
498 
499                 @Override
500                 protected boolean isPageSearchEnabled(Context context) {
501                     return DataUsageUtils.hasMobileData(context);
502                 }
503             };
504 
505 }
506