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 android.app.AlertDialog;
18 import android.app.Dialog;
19 import android.app.DialogFragment;
20 import android.app.Fragment;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.res.Resources;
24 import android.net.NetworkPolicy;
25 import android.net.NetworkTemplate;
26 import android.os.Bundle;
27 import android.support.v14.preference.SwitchPreference;
28 import android.support.v7.preference.Preference;
29 import android.text.format.Formatter;
30 import android.text.format.Time;
31 import android.util.Log;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.widget.EditText;
35 import android.widget.NumberPicker;
36 import android.widget.Spinner;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
40 import com.android.settings.R;
41 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
42 import com.android.settingslib.NetworkPolicyEditor;
43 import com.android.settingslib.net.DataUsageController;
44 
45 import static android.net.NetworkPolicy.LIMIT_DISABLED;
46 import static android.net.NetworkPolicy.WARNING_DISABLED;
47 
48 public class BillingCycleSettings extends DataUsageBase implements
49         Preference.OnPreferenceChangeListener, DataUsageEditController {
50 
51     private static final String TAG = "BillingCycleSettings";
52     private static final boolean LOGD = false;
53     public static final long KB_IN_BYTES = 1000;
54     public static final long MB_IN_BYTES = KB_IN_BYTES * 1000;
55     public static final long GB_IN_BYTES = MB_IN_BYTES * 1000;
56 
57     private static final long MAX_DATA_LIMIT_BYTES = 50000 * GB_IN_BYTES;
58 
59     private static final String TAG_CONFIRM_LIMIT = "confirmLimit";
60     private static final String TAG_CYCLE_EDITOR = "cycleEditor";
61     private static final String TAG_WARNING_EDITOR = "warningEditor";
62 
63     private static final String KEY_BILLING_CYCLE = "billing_cycle";
64     private static final String KEY_SET_DATA_WARNING = "set_data_warning";
65     private static final String KEY_DATA_WARNING = "data_warning";
66     @VisibleForTesting static final String KEY_SET_DATA_LIMIT = "set_data_limit";
67     private static final String KEY_DATA_LIMIT = "data_limit";
68 
69     private NetworkTemplate mNetworkTemplate;
70     private Preference mBillingCycle;
71     private Preference mDataWarning;
72     private SwitchPreference mEnableDataWarning;
73     private SwitchPreference mEnableDataLimit;
74     private Preference mDataLimit;
75     private DataUsageController mDataUsageController;
76 
77     @Override
onCreate(Bundle icicle)78     public void onCreate(Bundle icicle) {
79         super.onCreate(icicle);
80 
81         mDataUsageController = new DataUsageController(getContext());
82 
83         Bundle args = getArguments();
84         mNetworkTemplate = args.getParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE);
85 
86         addPreferencesFromResource(R.xml.billing_cycle);
87         mBillingCycle = findPreference(KEY_BILLING_CYCLE);
88         mEnableDataWarning = (SwitchPreference) findPreference(KEY_SET_DATA_WARNING);
89         mEnableDataWarning.setOnPreferenceChangeListener(this);
90         mDataWarning = findPreference(KEY_DATA_WARNING);
91         mEnableDataLimit = (SwitchPreference) findPreference(KEY_SET_DATA_LIMIT);
92         mEnableDataLimit.setOnPreferenceChangeListener(this);
93         mDataLimit = findPreference(KEY_DATA_LIMIT);
94     }
95 
96     @Override
onResume()97     public void onResume() {
98         super.onResume();
99         updatePrefs();
100     }
101 
updatePrefs()102     private void updatePrefs() {
103         NetworkPolicy policy = services.mPolicyEditor.getPolicy(mNetworkTemplate);
104         mBillingCycle.setSummary(getString(R.string.billing_cycle_summary, policy != null ?
105                 policy.cycleDay : 1));
106         if (policy != null && policy.warningBytes != WARNING_DISABLED) {
107             mDataWarning.setSummary(Formatter.formatFileSize(getContext(), policy.warningBytes));
108             mDataWarning.setEnabled(true);
109             mEnableDataWarning.setChecked(true);
110         } else {
111             mDataWarning.setSummary(null);
112             mDataWarning.setEnabled(false);
113             mEnableDataWarning.setChecked(false);
114         }
115         if (policy != null && policy.limitBytes != LIMIT_DISABLED) {
116             mDataLimit.setSummary(Formatter.formatFileSize(getContext(), policy.limitBytes));
117             mDataLimit.setEnabled(true);
118             mEnableDataLimit.setChecked(true);
119         } else {
120             mDataLimit.setSummary(null);
121             mDataLimit.setEnabled(false);
122             mEnableDataLimit.setChecked(false);
123         }
124     }
125 
126     @Override
onPreferenceTreeClick(Preference preference)127     public boolean onPreferenceTreeClick(Preference preference) {
128         if (preference == mBillingCycle) {
129             CycleEditorFragment.show(this);
130             return true;
131         } else if (preference == mDataWarning) {
132             BytesEditorFragment.show(this, false);
133             return true;
134         } else if (preference == mDataLimit) {
135             BytesEditorFragment.show(this, true);
136             return true;
137         }
138         return super.onPreferenceTreeClick(preference);
139     }
140 
141     @Override
onPreferenceChange(Preference preference, Object newValue)142     public boolean onPreferenceChange(Preference preference, Object newValue) {
143         if (mEnableDataLimit == preference) {
144             boolean enabled = (Boolean) newValue;
145             if (!enabled) {
146                 setPolicyLimitBytes(LIMIT_DISABLED);
147                 return true;
148             }
149             ConfirmLimitFragment.show(this);
150             // This preference is enabled / disabled by ConfirmLimitFragment.
151             return false;
152         } else if (mEnableDataWarning == preference) {
153             boolean enabled = (Boolean) newValue;
154             if (enabled) {
155                 setPolicyWarningBytes(mDataUsageController.getDefaultWarningLevel());
156             } else {
157                 setPolicyWarningBytes(WARNING_DISABLED);
158             }
159             return true;
160         }
161         return false;
162     }
163 
164     @Override
getMetricsCategory()165     public int getMetricsCategory() {
166         return MetricsEvent.BILLING_CYCLE;
167     }
168 
169     @VisibleForTesting
setPolicyLimitBytes(long limitBytes)170     void setPolicyLimitBytes(long limitBytes) {
171         if (LOGD) Log.d(TAG, "setPolicyLimitBytes()");
172         services.mPolicyEditor.setPolicyLimitBytes(mNetworkTemplate, limitBytes);
173         updatePrefs();
174     }
175 
setPolicyWarningBytes(long warningBytes)176     private void setPolicyWarningBytes(long warningBytes) {
177         if (LOGD) Log.d(TAG, "setPolicyWarningBytes()");
178         services.mPolicyEditor.setPolicyWarningBytes(mNetworkTemplate, warningBytes);
179         updatePrefs();
180     }
181 
182     @Override
getNetworkPolicyEditor()183     public NetworkPolicyEditor getNetworkPolicyEditor() {
184         return services.mPolicyEditor;
185     }
186 
187     @Override
getNetworkTemplate()188     public NetworkTemplate getNetworkTemplate() {
189         return mNetworkTemplate;
190     }
191 
192     @Override
updateDataUsage()193     public void updateDataUsage() {
194         updatePrefs();
195     }
196 
197     /**
198      * Dialog to edit {@link NetworkPolicy#warningBytes}.
199      */
200     public static class BytesEditorFragment extends InstrumentedDialogFragment
201             implements DialogInterface.OnClickListener {
202         private static final String EXTRA_TEMPLATE = "template";
203         private static final String EXTRA_LIMIT = "limit";
204         private View mView;
205 
show(DataUsageEditController parent, boolean isLimit)206         public static void show(DataUsageEditController parent, boolean isLimit) {
207             if (!(parent instanceof Fragment)) {
208                 return;
209             }
210             Fragment targetFragment = (Fragment) parent;
211             if (!targetFragment.isAdded()) {
212                 return;
213             }
214 
215             final Bundle args = new Bundle();
216             args.putParcelable(EXTRA_TEMPLATE, parent.getNetworkTemplate());
217             args.putBoolean(EXTRA_LIMIT, isLimit);
218 
219             final BytesEditorFragment dialog = new BytesEditorFragment();
220             dialog.setArguments(args);
221             dialog.setTargetFragment(targetFragment, 0);
222             dialog.show(targetFragment.getFragmentManager(), TAG_WARNING_EDITOR);
223         }
224 
225         @Override
onCreateDialog(Bundle savedInstanceState)226         public Dialog onCreateDialog(Bundle savedInstanceState) {
227             final Context context = getActivity();
228             final LayoutInflater dialogInflater = LayoutInflater.from(context);
229             final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT);
230             mView = dialogInflater.inflate(R.layout.data_usage_bytes_editor, null, false);
231             setupPicker((EditText) mView.findViewById(R.id.bytes),
232                     (Spinner) mView.findViewById(R.id.size_spinner));
233             return new AlertDialog.Builder(context)
234                     .setTitle(isLimit ? R.string.data_usage_limit_editor_title
235                             : R.string.data_usage_warning_editor_title)
236                     .setView(mView)
237                     .setPositiveButton(R.string.data_usage_cycle_editor_positive, this)
238                     .create();
239         }
240 
setupPicker(EditText bytesPicker, Spinner type)241         private void setupPicker(EditText bytesPicker, Spinner type) {
242             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
243             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
244 
245             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
246             final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT);
247             final long bytes = isLimit ? editor.getPolicyLimitBytes(template)
248                     : editor.getPolicyWarningBytes(template);
249             final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED;
250 
251             if (bytes > 1.5f * GB_IN_BYTES) {
252                 final String bytesText = formatText(bytes / (float) GB_IN_BYTES);
253                 bytesPicker.setText(bytesText);
254                 bytesPicker.setSelection(0, bytesText.length());
255 
256                 type.setSelection(1);
257             } else {
258                 final String bytesText = formatText(bytes / (float) MB_IN_BYTES);
259                 bytesPicker.setText(bytesText);
260                 bytesPicker.setSelection(0, bytesText.length());
261 
262                 type.setSelection(0);
263             }
264         }
265 
formatText(float v)266         private String formatText(float v) {
267             v = Math.round(v * 100) / 100f;
268             return String.valueOf(v);
269         }
270 
271         @Override
onClick(DialogInterface dialog, int which)272         public void onClick(DialogInterface dialog, int which) {
273             if (which != DialogInterface.BUTTON_POSITIVE) {
274                 return;
275             }
276             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
277             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
278 
279             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
280             final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT);
281             EditText bytesField = (EditText) mView.findViewById(R.id.bytes);
282             Spinner spinner = (Spinner) mView.findViewById(R.id.size_spinner);
283 
284             String bytesString = bytesField.getText().toString();
285             if (bytesString.isEmpty()) {
286                 bytesString = "0";
287             }
288             final long bytes = (long) (Float.valueOf(bytesString)
289                         * (spinner.getSelectedItemPosition() == 0 ? MB_IN_BYTES : GB_IN_BYTES));
290 
291             // to fix the overflow problem
292             final long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes);
293             if (isLimit) {
294                 editor.setPolicyLimitBytes(template, correctedBytes);
295             } else {
296                 editor.setPolicyWarningBytes(template, correctedBytes);
297             }
298             target.updateDataUsage();
299         }
300 
301         @Override
getMetricsCategory()302         public int getMetricsCategory() {
303             return MetricsEvent.DIALOG_BILLING_BYTE_LIMIT;
304         }
305     }
306 
307     /**
308      * Dialog to edit {@link NetworkPolicy#cycleDay}.
309      */
310     public static class CycleEditorFragment extends InstrumentedDialogFragment implements
311             DialogInterface.OnClickListener {
312         private static final String EXTRA_TEMPLATE = "template";
313         private NumberPicker mCycleDayPicker;
314 
show(BillingCycleSettings parent)315         public static void show(BillingCycleSettings parent) {
316             if (!parent.isAdded()) return;
317 
318             final Bundle args = new Bundle();
319             args.putParcelable(EXTRA_TEMPLATE, parent.mNetworkTemplate);
320 
321             final CycleEditorFragment dialog = new CycleEditorFragment();
322             dialog.setArguments(args);
323             dialog.setTargetFragment(parent, 0);
324             dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR);
325         }
326 
327         @Override
getMetricsCategory()328         public int getMetricsCategory() {
329             return MetricsEvent.DIALOG_BILLING_CYCLE;
330         }
331 
332         @Override
onCreateDialog(Bundle savedInstanceState)333         public Dialog onCreateDialog(Bundle savedInstanceState) {
334             final Context context = getActivity();
335             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
336             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
337 
338             final AlertDialog.Builder builder = new AlertDialog.Builder(context);
339             final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
340 
341             final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false);
342             mCycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day);
343 
344             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
345             final int cycleDay = editor.getPolicyCycleDay(template);
346 
347             mCycleDayPicker.setMinValue(1);
348             mCycleDayPicker.setMaxValue(31);
349             mCycleDayPicker.setValue(cycleDay);
350             mCycleDayPicker.setWrapSelectorWheel(true);
351 
352             return builder.setTitle(R.string.data_usage_cycle_editor_title)
353                     .setView(view)
354                     .setPositiveButton(R.string.data_usage_cycle_editor_positive, this)
355                     .create();
356         }
357 
358         @Override
onClick(DialogInterface dialog, int which)359         public void onClick(DialogInterface dialog, int which) {
360             final NetworkTemplate template = getArguments().getParcelable(EXTRA_TEMPLATE);
361             final DataUsageEditController target = (DataUsageEditController) getTargetFragment();
362             final NetworkPolicyEditor editor = target.getNetworkPolicyEditor();
363 
364             // clear focus to finish pending text edits
365             mCycleDayPicker.clearFocus();
366 
367             final int cycleDay = mCycleDayPicker.getValue();
368             final String cycleTimezone = new Time().timezone;
369             editor.setPolicyCycleDay(template, cycleDay, cycleTimezone);
370             target.updateDataUsage();
371         }
372     }
373 
374     /**
375      * Dialog to request user confirmation before setting
376      * {@link NetworkPolicy#limitBytes}.
377      */
378     public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements
379             DialogInterface.OnClickListener {
380         private static final String EXTRA_MESSAGE = "message";
381         @VisibleForTesting static final String EXTRA_LIMIT_BYTES = "limitBytes";
382         public static final float FLOAT = 1.2f;
383 
show(BillingCycleSettings parent)384         public static void show(BillingCycleSettings parent) {
385             if (!parent.isAdded()) return;
386 
387             final NetworkPolicy policy = parent.services.mPolicyEditor
388                     .getPolicy(parent.mNetworkTemplate);
389             if (policy == null) return;
390 
391             final Resources res = parent.getResources();
392             final CharSequence message;
393             final long minLimitBytes = (long) (policy.warningBytes * FLOAT);
394             final long limitBytes;
395 
396             // TODO: customize default limits based on network template
397             message = res.getString(R.string.data_usage_limit_dialog_mobile);
398             limitBytes = Math.max(5 * GB_IN_BYTES, minLimitBytes);
399 
400             final Bundle args = new Bundle();
401             args.putCharSequence(EXTRA_MESSAGE, message);
402             args.putLong(EXTRA_LIMIT_BYTES, limitBytes);
403 
404             final ConfirmLimitFragment dialog = new ConfirmLimitFragment();
405             dialog.setArguments(args);
406             dialog.setTargetFragment(parent, 0);
407             dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT);
408         }
409 
410         @Override
getMetricsCategory()411         public int getMetricsCategory() {
412             return MetricsEvent.DIALOG_BILLING_CONFIRM_LIMIT;
413         }
414 
415         @Override
onCreateDialog(Bundle savedInstanceState)416         public Dialog onCreateDialog(Bundle savedInstanceState) {
417             final Context context = getActivity();
418 
419             final CharSequence message = getArguments().getCharSequence(EXTRA_MESSAGE);
420 
421             return new AlertDialog.Builder(context)
422                     .setTitle(R.string.data_usage_limit_dialog_title)
423                     .setMessage(message)
424                     .setPositiveButton(android.R.string.ok, this)
425                     .setNegativeButton(android.R.string.cancel, null)
426                     .create();
427         }
428 
429         @Override
onClick(DialogInterface dialog, int which)430         public void onClick(DialogInterface dialog, int which) {
431             final BillingCycleSettings target = (BillingCycleSettings) getTargetFragment();
432             if (which != DialogInterface.BUTTON_POSITIVE) return;
433             final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES);
434             if (target != null) {
435                 target.setPolicyLimitBytes(limitBytes);
436             }
437             target.getPreferenceManager().getSharedPreferences().edit()
438                     .putBoolean(KEY_SET_DATA_LIMIT, true).apply();
439         }
440     }
441 }
442