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