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