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