1 /*
2  * Copyright (C) 2018 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.settings.datausage;
18 
19 import android.annotation.AttrRes;
20 import android.content.Context;
21 import android.graphics.Typeface;
22 import android.icu.text.MessageFormat;
23 import android.telephony.SubscriptionPlan;
24 import android.text.Spannable;
25 import android.text.SpannableString;
26 import android.text.TextUtils;
27 import android.text.format.Formatter;
28 import android.text.style.AbsoluteSizeSpan;
29 import android.util.AttributeSet;
30 import android.view.View;
31 import android.widget.LinearLayout;
32 import android.widget.ProgressBar;
33 import android.widget.TextView;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.preference.Preference;
39 import androidx.preference.PreferenceViewHolder;
40 
41 import com.android.settings.R;
42 import com.android.settingslib.Utils;
43 import com.android.settingslib.utils.StringUtil;
44 
45 import java.util.HashMap;
46 import java.util.Locale;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.concurrent.TimeUnit;
50 
51 /**
52  * Provides a summary of data usage.
53  */
54 public class DataUsageSummaryPreference extends Preference {
55     private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1);
56     private static final long WARNING_AGE = TimeUnit.HOURS.toMillis(6L);
57     @VisibleForTesting
58     static final Typeface SANS_SERIF_MEDIUM =
59             Typeface.create("sans-serif-medium", Typeface.NORMAL);
60 
61     private boolean mChartEnabled = true;
62     private CharSequence mStartLabel;
63     private CharSequence mEndLabel;
64 
65     private int mNumPlans;
66     /** The ending time of the billing cycle in milliseconds since epoch. */
67     @Nullable
68     private Long mCycleEndTimeMs;
69     /** The time of the last update in standard milliseconds since the epoch */
70     private long mSnapshotTimeMs = SubscriptionPlan.TIME_UNKNOWN;
71     /** Name of carrier, or null if not available */
72     private CharSequence mCarrierName;
73     private CharSequence mLimitInfoText;
74 
75     /** Progress to display on ProgressBar */
76     private float mProgress;
77 
78     /**
79      * The size of the first registered plan if one exists or the size of the warning if it is set.
80      * -1 if no information is available.
81      */
82     private long mDataplanSize;
83 
84     /** The number of bytes used since the start of the cycle. */
85     private long mDataplanUse;
86 
DataUsageSummaryPreference(Context context, AttributeSet attrs)87     public DataUsageSummaryPreference(Context context, AttributeSet attrs) {
88         super(context, attrs);
89         setLayoutResource(R.layout.data_usage_summary_preference);
90     }
91 
setLimitInfo(CharSequence text)92     public void setLimitInfo(CharSequence text) {
93         if (!Objects.equals(text, mLimitInfoText)) {
94             mLimitInfoText = text;
95             notifyChanged();
96         }
97     }
98 
setProgress(float progress)99     public void setProgress(float progress) {
100         mProgress = progress;
101         notifyChanged();
102     }
103 
104     /**
105      * Sets the usage info.
106      */
setUsageInfo(@ullable Long cycleEnd, long snapshotTime, CharSequence carrierName, int numPlans)107     public void setUsageInfo(@Nullable Long cycleEnd, long snapshotTime, CharSequence carrierName,
108             int numPlans) {
109         mCycleEndTimeMs = cycleEnd;
110         mSnapshotTimeMs = snapshotTime;
111         mCarrierName = carrierName;
112         mNumPlans = numPlans;
113         notifyChanged();
114     }
115 
setChartEnabled(boolean enabled)116     public void setChartEnabled(boolean enabled) {
117         if (mChartEnabled != enabled) {
118             mChartEnabled = enabled;
119             notifyChanged();
120         }
121     }
122 
setLabels(CharSequence start, CharSequence end)123     public void setLabels(CharSequence start, CharSequence end) {
124         mStartLabel = start;
125         mEndLabel = end;
126         notifyChanged();
127     }
128 
129     /**
130      * Sets the usage numbers.
131      */
setUsageNumbers(long used, long dataPlanSize)132     public void setUsageNumbers(long used, long dataPlanSize) {
133         mDataplanUse = used;
134         mDataplanSize = dataPlanSize;
135         notifyChanged();
136     }
137 
138     @Override
onBindViewHolder(@onNull PreferenceViewHolder holder)139     public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
140         super.onBindViewHolder(holder);
141 
142         ProgressBar bar = getProgressBar(holder);
143         if (mChartEnabled && (!TextUtils.isEmpty(mStartLabel) || !TextUtils.isEmpty(mEndLabel))) {
144             bar.setVisibility(View.VISIBLE);
145             getLabelBar(holder).setVisibility(View.VISIBLE);
146             bar.setProgress((int) (mProgress * 100));
147             (getLabel1(holder)).setText(mStartLabel);
148             (getLabel2(holder)).setText(mEndLabel);
149         } else {
150             bar.setVisibility(View.GONE);
151             getLabelBar(holder).setVisibility(View.GONE);
152         }
153 
154         updateDataUsageLabels(holder);
155 
156         TextView usageTitle = getUsageTitle(holder);
157         TextView carrierInfo = getCarrierInfo(holder);
158         TextView limitInfo = getDataLimits(holder);
159 
160         usageTitle.setVisibility(mNumPlans > 1 ? View.VISIBLE : View.GONE);
161         updateCycleTimeText(holder);
162         updateCarrierInfo(carrierInfo);
163         limitInfo.setVisibility(TextUtils.isEmpty(mLimitInfoText) ? View.GONE : View.VISIBLE);
164         limitInfo.setText(mLimitInfoText);
165     }
166 
updateDataUsageLabels(PreferenceViewHolder holder)167     private void updateDataUsageLabels(PreferenceViewHolder holder) {
168         TextView usageNumberField = getDataUsed(holder);
169 
170         final Formatter.BytesResult usedResult = Formatter.formatBytes(getContext().getResources(),
171                 mDataplanUse, Formatter.FLAG_CALCULATE_ROUNDED | Formatter.FLAG_IEC_UNITS);
172         final SpannableString usageNumberText = new SpannableString(usedResult.value);
173         final int textSize =
174                 getContext().getResources().getDimensionPixelSize(R.dimen.usage_number_text_size);
175         usageNumberText.setSpan(new AbsoluteSizeSpan(textSize), 0, usageNumberText.length(),
176                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
177         CharSequence template = getContext().getText(R.string.data_used_formatted);
178 
179         CharSequence usageText =
180                 TextUtils.expandTemplate(template, usageNumberText, usedResult.units);
181         usageNumberField.setText(usageText);
182 
183         final MeasurableLinearLayout layout = getLayout(holder);
184 
185         if (mDataplanSize > 0L) {
186             TextView usageRemainingField = getDataRemaining(holder);
187             long dataRemaining = mDataplanSize - mDataplanUse;
188             if (dataRemaining >= 0) {
189                 usageRemainingField.setText(
190                         TextUtils.expandTemplate(getContext().getText(R.string.data_remaining),
191                                 DataUsageUtils.formatDataUsage(getContext(), dataRemaining)));
192                 usageRemainingField.setTextColor(
193                         Utils.getColorAttr(getContext(), android.R.attr.colorAccent));
194             } else {
195                 usageRemainingField.setText(
196                         TextUtils.expandTemplate(getContext().getText(R.string.data_overusage),
197                                 DataUsageUtils.formatDataUsage(getContext(), -dataRemaining)));
198                 usageRemainingField.setTextColor(
199                         Utils.getColorAttr(getContext(), android.R.attr.colorError));
200             }
201             layout.setChildren(usageNumberField, usageRemainingField);
202         } else {
203             layout.setChildren(usageNumberField, null);
204         }
205     }
206 
updateCycleTimeText(PreferenceViewHolder holder)207     private void updateCycleTimeText(PreferenceViewHolder holder) {
208         TextView cycleTime = getCycleTime(holder);
209 
210         // Takes zero as a special case which value is never set.
211         if (mCycleEndTimeMs == null) {
212             cycleTime.setVisibility(View.GONE);
213             return;
214         }
215 
216         cycleTime.setVisibility(View.VISIBLE);
217         long millisLeft = mCycleEndTimeMs - System.currentTimeMillis();
218         if (millisLeft <= 0) {
219             cycleTime.setText(getContext().getString(R.string.billing_cycle_none_left));
220         } else {
221             int daysLeft = (int) (millisLeft / MILLIS_IN_A_DAY);
222             MessageFormat msgFormat = new MessageFormat(
223                     getContext().getResources().getString(R.string.billing_cycle_days_left),
224                     Locale.getDefault());
225             Map<String, Object> arguments = new HashMap<>();
226             arguments.put("count", daysLeft);
227             cycleTime.setText(daysLeft < 1
228                     ? getContext().getString(R.string.billing_cycle_less_than_one_day_left)
229                     : msgFormat.format(arguments));
230         }
231     }
232 
233 
234     private void updateCarrierInfo(TextView carrierInfo) {
235         if (mSnapshotTimeMs >= 0L) {
236             carrierInfo.setVisibility(View.VISIBLE);
237             long updateAgeMillis = calculateTruncatedUpdateAge();
238 
239             int textResourceId;
240             CharSequence updateTime = null;
241             if (updateAgeMillis == 0) {
242                 if (mCarrierName != null) {
243                     textResourceId = R.string.carrier_and_update_now_text;
244                 } else {
245                     textResourceId = R.string.no_carrier_update_now_text;
246                 }
247             } else {
248                 if (mCarrierName != null) {
249                     textResourceId = R.string.carrier_and_update_text;
250                 } else {
251                     textResourceId = R.string.no_carrier_update_text;
252                 }
253                 updateTime = StringUtil.formatElapsedTime(
254                         getContext(),
255                         updateAgeMillis,
256                         false /* withSeconds */,
257                         false /* collapseTimeUnit */);
258             }
259             carrierInfo.setText(TextUtils.expandTemplate(
260                     getContext().getText(textResourceId),
261                     mCarrierName,
262                     updateTime));
263 
264             if (updateAgeMillis <= WARNING_AGE) {
265                 setCarrierInfoTextStyle(
266                         carrierInfo, android.R.attr.textColorSecondary, Typeface.SANS_SERIF);
267             } else {
268                 setCarrierInfoTextStyle(carrierInfo, android.R.attr.colorError, SANS_SERIF_MEDIUM);
269             }
270         } else {
271             carrierInfo.setVisibility(View.GONE);
272         }
273     }
274 
275     /**
276      * Returns the time since the last carrier update, as defined by {@link #mSnapshotTimeMs},
277      * truncated to the nearest day / hour / minute in milliseconds, or 0 if less than 1 min.
278      */
calculateTruncatedUpdateAge()279     private long calculateTruncatedUpdateAge() {
280         long updateAgeMillis = System.currentTimeMillis() - mSnapshotTimeMs;
281 
282         // Round to nearest whole unit
283         if (updateAgeMillis >= TimeUnit.DAYS.toMillis(1)) {
284             return (updateAgeMillis / TimeUnit.DAYS.toMillis(1)) * TimeUnit.DAYS.toMillis(1);
285         } else if (updateAgeMillis >= TimeUnit.HOURS.toMillis(1)) {
286             return (updateAgeMillis / TimeUnit.HOURS.toMillis(1)) * TimeUnit.HOURS.toMillis(1);
287         } else if (updateAgeMillis >= TimeUnit.MINUTES.toMillis(1)) {
288             return (updateAgeMillis / TimeUnit.MINUTES.toMillis(1)) * TimeUnit.MINUTES.toMillis(1);
289         } else {
290             return 0;
291         }
292     }
293 
setCarrierInfoTextStyle( TextView carrierInfo, @AttrRes int colorId, Typeface typeface)294     private void setCarrierInfoTextStyle(
295             TextView carrierInfo, @AttrRes int colorId, Typeface typeface) {
296         carrierInfo.setTextColor(Utils.getColorAttr(getContext(), colorId));
297         carrierInfo.setTypeface(typeface);
298     }
299 
300     @VisibleForTesting
getUsageTitle(PreferenceViewHolder holder)301     protected TextView getUsageTitle(PreferenceViewHolder holder) {
302         return (TextView) holder.findViewById(R.id.usage_title);
303     }
304 
305     @VisibleForTesting
getCycleTime(PreferenceViewHolder holder)306     protected TextView getCycleTime(PreferenceViewHolder holder) {
307         return (TextView) holder.findViewById(R.id.cycle_left_time);
308     }
309 
310     @VisibleForTesting
getCarrierInfo(PreferenceViewHolder holder)311     protected TextView getCarrierInfo(PreferenceViewHolder holder) {
312         return (TextView) holder.findViewById(R.id.carrier_and_update);
313     }
314 
315     @VisibleForTesting
getDataLimits(PreferenceViewHolder holder)316     protected TextView getDataLimits(PreferenceViewHolder holder) {
317         return (TextView) holder.findViewById(R.id.data_limits);
318     }
319 
320     @VisibleForTesting
getDataUsed(PreferenceViewHolder holder)321     protected TextView getDataUsed(PreferenceViewHolder holder) {
322         return (TextView) holder.findViewById(R.id.data_usage_view);
323     }
324 
325     @VisibleForTesting
getDataRemaining(PreferenceViewHolder holder)326     protected TextView getDataRemaining(PreferenceViewHolder holder) {
327         return (TextView) holder.findViewById(R.id.data_remaining_view);
328     }
329 
330     @VisibleForTesting
getLabelBar(PreferenceViewHolder holder)331     protected LinearLayout getLabelBar(PreferenceViewHolder holder) {
332         return (LinearLayout) holder.findViewById(R.id.label_bar);
333     }
334 
335     @VisibleForTesting
getLabel1(PreferenceViewHolder holder)336     protected TextView getLabel1(PreferenceViewHolder holder) {
337         return (TextView) holder.findViewById(android.R.id.text1);
338     }
339 
340     @VisibleForTesting
getLabel2(PreferenceViewHolder holder)341     protected TextView getLabel2(PreferenceViewHolder holder) {
342         return (TextView) holder.findViewById(android.R.id.text2);
343     }
344 
345     @VisibleForTesting
getProgressBar(PreferenceViewHolder holder)346     protected ProgressBar getProgressBar(PreferenceViewHolder holder) {
347         return (ProgressBar) holder.findViewById(R.id.determinateBar);
348     }
349 
350     @VisibleForTesting
getLayout(PreferenceViewHolder holder)351     protected MeasurableLinearLayout getLayout(PreferenceViewHolder holder) {
352         return (MeasurableLinearLayout) holder.findViewById(R.id.usage_layout);
353     }
354 }
355