1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.settings.datausage;
18 
19 import android.app.AlertDialog;
20 import android.app.Dialog;
21 import android.content.DialogInterface;
22 import android.icu.text.MeasureFormat;
23 import android.icu.util.MeasureUnit;
24 import android.os.Bundle;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.widget.EditText;
28 import android.widget.NumberPicker;
29 
30 import androidx.annotation.IntegerRes;
31 import androidx.annotation.VisibleForTesting;
32 
33 import com.android.car.settings.R;
34 import com.android.car.ui.preference.CarUiDialogFragment;
35 
36 /** Dialog that is used to pick the data usage warning/limit threshold bytes. */
37 public class UsageBytesThresholdPickerDialog extends CarUiDialogFragment {
38 
39     /** Tag used to identify dialog in {@link androidx.fragment.app.FragmentManager}. */
40     public static final String TAG = "UsageBytesThresholdPickerDialog";
41     private static final String ARG_DIALOG_TITLE_RES = "arg_dialog_title_res";
42     private static final String ARG_CURRENT_THRESHOLD = "arg_current_threshold";
43     private static final float MB_GB_SUFFIX_THRESHOLD = 1.5f;
44 
45     @VisibleForTesting
46     static final long MIB_IN_BYTES = 1024 * 1024;
47     @VisibleForTesting
48     static final long GIB_IN_BYTES = MIB_IN_BYTES * 1024;
49     @VisibleForTesting
50     static final long MAX_DATA_LIMIT_BYTES = 50000 * GIB_IN_BYTES;
51 
52     // Number pickers can be used to pick strings as well.
53     private NumberPicker mBytesUnits;
54     private View mUpArrow;
55     private View mDownArrow;
56     private EditText mThresholdEditor;
57     private BytesThresholdPickedListener mBytesThresholdPickedListener;
58     private long mCurrentThreshold;
59 
60     /**
61      * Creates a new instance of the {@link UsageBytesThresholdPickerDialog} with the
62      * {@code currentThreshold} represented with the best units available.
63      */
newInstance(@ntegerRes int dialogTitle, long currentThreshold)64     public static UsageBytesThresholdPickerDialog newInstance(@IntegerRes int dialogTitle,
65             long currentThreshold) {
66         UsageBytesThresholdPickerDialog dialog = new UsageBytesThresholdPickerDialog();
67         Bundle args = new Bundle();
68         args.putInt(ARG_DIALOG_TITLE_RES, dialogTitle);
69         args.putLong(ARG_CURRENT_THRESHOLD, currentThreshold);
70         dialog.setArguments(args);
71         return dialog;
72     }
73 
74     /** Sets a {@link BytesThresholdPickedListener}. */
setBytesThresholdPickedListener( BytesThresholdPickedListener bytesThresholdPickedListener)75     public void setBytesThresholdPickedListener(
76             BytesThresholdPickedListener bytesThresholdPickedListener) {
77         mBytesThresholdPickedListener = bytesThresholdPickedListener;
78     }
79 
80     @Override
onCreateDialog(Bundle savedInstanceState)81     public Dialog onCreateDialog(Bundle savedInstanceState) {
82         AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
83 
84         // Use builder context to keep consistent theme.
85         LayoutInflater inflater = LayoutInflater.from(builder.getContext());
86         View view = inflater.inflate(R.layout.usage_bytes_threshold_picker,
87                 /* root= */ null, /* attachToRoot= */ false);
88 
89         mCurrentThreshold = getArguments().getLong(ARG_CURRENT_THRESHOLD);
90         if (mCurrentThreshold < 0) {
91             mCurrentThreshold = 0;
92         }
93 
94         MeasureFormat formatter = MeasureFormat.getInstance(
95                 getContext().getResources().getConfiguration().locale,
96                 MeasureFormat.FormatWidth.SHORT);
97         String[] units = new String[] {
98             formatter.getUnitDisplayName(MeasureUnit.MEGABYTE),
99             formatter.getUnitDisplayName(MeasureUnit.GIGABYTE)
100         };
101         mBytesUnits = view.findViewById(R.id.bytes_units);
102         mBytesUnits.setMinValue(0);
103         mBytesUnits.setMaxValue(units.length - 1);
104         mBytesUnits.setDisplayedValues(units);
105 
106         mThresholdEditor = view.findViewById(R.id.bytes_threshold);
107 
108         mUpArrow = view.findViewById(R.id.up_arrow_container);
109         mUpArrow.setOnClickListener(v -> mBytesUnits.setValue(mBytesUnits.getValue() - 1));
110 
111         mDownArrow = view.findViewById(R.id.down_arrow_container);
112         mDownArrow.setOnClickListener(v -> mBytesUnits.setValue(mBytesUnits.getValue() + 1));
113 
114         updateCurrentView(mCurrentThreshold);
115 
116         return builder
117                 .setTitle(getArguments().getInt(ARG_DIALOG_TITLE_RES))
118                 .setView(view)
119                 .setPositiveButton(R.string.usage_bytes_threshold_picker_positive_button,
120                         /* onClickListener= */ this)
121                 .create();
122     }
123 
124     @Override
onDialogClosed(boolean positiveResult)125     protected void onDialogClosed(boolean positiveResult) {
126     }
127 
128     @Override
onClick(DialogInterface dialog, int which)129     public void onClick(DialogInterface dialog, int which) {
130         if (which == DialogInterface.BUTTON_POSITIVE) {
131             long newThreshold = getCurrentThreshold();
132             if (mBytesThresholdPickedListener != null
133                     && mCurrentThreshold != newThreshold) {
134                 mBytesThresholdPickedListener.onThresholdPicked(newThreshold);
135             }
136         }
137     }
138 
139     /** Gets the threshold currently represented by this {@link UsageBytesThresholdPickerDialog}. */
getCurrentThreshold()140     public long getCurrentThreshold() {
141         String bytesString = mThresholdEditor.getText().toString();
142         if (bytesString.isEmpty() || bytesString.equals(".")) {
143             bytesString = "0";
144         }
145 
146         long bytes = (long) (Float.valueOf(bytesString) * (mBytesUnits.getValue() == 0
147                 ? MIB_IN_BYTES : GIB_IN_BYTES));
148 
149         // To fix the overflow problem.
150         long correctedBytes = Math.min(MAX_DATA_LIMIT_BYTES, bytes);
151 
152         return correctedBytes;
153     }
154 
155     @VisibleForTesting
setThresholdEditor(long threshold)156     void setThresholdEditor(long threshold) {
157         updateCurrentView(threshold);
158     }
159 
updateCurrentView(long threshold)160     private void updateCurrentView(long threshold) {
161         String bytesText;
162         if (threshold > MB_GB_SUFFIX_THRESHOLD * GIB_IN_BYTES) {
163             bytesText = formatText(threshold / (float) GIB_IN_BYTES);
164             mBytesUnits.setValue(1);
165         } else {
166             bytesText = formatText(threshold / (float) MIB_IN_BYTES);
167             mBytesUnits.setValue(0);
168         }
169         mThresholdEditor.setText(bytesText);
170         mThresholdEditor.setSelection(0, bytesText.length());
171     }
172 
formatText(float v)173     private String formatText(float v) {
174         v = Math.round(v * 100) / 100f;
175         return String.valueOf(v);
176     }
177 
178     /** A listener that is called when a date is selected. */
179     public interface BytesThresholdPickedListener {
180         /** A method that determines how to process the selected day of month. */
onThresholdPicked(long numBytes)181         void onThresholdPicked(long numBytes);
182     }
183 }
184