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