• 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.app.settings.SettingsEnums;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.graphics.Typeface;
24 import android.icu.text.MessageFormat;
25 import android.net.ConnectivityManager;
26 import android.net.NetworkTemplate;
27 import android.os.Bundle;
28 import android.text.Spannable;
29 import android.text.SpannableString;
30 import android.text.TextUtils;
31 import android.text.format.Formatter;
32 import android.text.style.AbsoluteSizeSpan;
33 import android.util.AttributeSet;
34 import android.view.View;
35 import android.widget.Button;
36 import android.widget.LinearLayout;
37 import android.widget.ProgressBar;
38 import android.widget.TextView;
39 
40 import androidx.annotation.VisibleForTesting;
41 import androidx.preference.Preference;
42 import androidx.preference.PreferenceViewHolder;
43 
44 import com.android.settings.R;
45 import com.android.settings.core.SubSettingLauncher;
46 import com.android.settingslib.Utils;
47 import com.android.settingslib.net.DataUsageController;
48 import com.android.settingslib.utils.StringUtil;
49 
50 import java.util.HashMap;
51 import java.util.Locale;
52 import java.util.Map;
53 import java.util.Objects;
54 import java.util.concurrent.TimeUnit;
55 
56 /**
57  * Provides a summary of data usage.
58  */
59 public class DataUsageSummaryPreference extends Preference {
60     private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1);
61     private static final long WARNING_AGE = TimeUnit.HOURS.toMillis(6L);
62     @VisibleForTesting
63     static final Typeface SANS_SERIF_MEDIUM =
64             Typeface.create("sans-serif-medium", Typeface.NORMAL);
65 
66     private boolean mChartEnabled = true;
67     private CharSequence mStartLabel;
68     private CharSequence mEndLabel;
69 
70     /** large vs small size is 36/16 ~ 2.25 */
71     private static final float LARGER_FONT_RATIO = 2.25f;
72     private static final float SMALLER_FONT_RATIO = 1.0f;
73 
74     private boolean mDefaultTextColorSet;
75     private int mDefaultTextColor;
76     private int mNumPlans;
77     /** The specified un-initialized value for cycle time */
78     private final long CYCLE_TIME_UNINITIAL_VALUE = 0;
79     /** The ending time of the billing cycle in milliseconds since epoch. */
80     private long mCycleEndTimeMs;
81     /** The time of the last update in standard milliseconds since the epoch */
82     private long mSnapshotTimeMs;
83     /** Name of carrier, or null if not available */
84     private CharSequence mCarrierName;
85     private CharSequence mLimitInfoText;
86     private Intent mLaunchIntent;
87 
88     /** Progress to display on ProgressBar */
89     private float mProgress;
90     private boolean mHasMobileData;
91 
92     /**
93      * The size of the first registered plan if one exists or the size of the warning if it is set.
94      * -1 if no information is available.
95      */
96     private long mDataplanSize;
97 
98     /** The number of bytes used since the start of the cycle. */
99     private long mDataplanUse;
100 
101     /** WiFi only mode */
102     private boolean mWifiMode;
103     private String mUsagePeriod;
104     private boolean mSingleWifi;    // Shows only one specified WiFi network usage
105 
DataUsageSummaryPreference(Context context, AttributeSet attrs)106     public DataUsageSummaryPreference(Context context, AttributeSet attrs) {
107         super(context, attrs);
108         setLayoutResource(R.layout.data_usage_summary_preference);
109     }
110 
setLimitInfo(CharSequence text)111     public void setLimitInfo(CharSequence text) {
112         if (!Objects.equals(text, mLimitInfoText)) {
113             mLimitInfoText = text;
114             notifyChanged();
115         }
116     }
117 
setProgress(float progress)118     public void setProgress(float progress) {
119         mProgress = progress;
120         notifyChanged();
121     }
122 
setUsageInfo(long cycleEnd, long snapshotTime, CharSequence carrierName, int numPlans, Intent launchIntent)123     public void setUsageInfo(long cycleEnd, long snapshotTime, CharSequence carrierName,
124             int numPlans, Intent launchIntent) {
125         mCycleEndTimeMs = cycleEnd;
126         mSnapshotTimeMs = snapshotTime;
127         mCarrierName = carrierName;
128         mNumPlans = numPlans;
129         mLaunchIntent = launchIntent;
130         notifyChanged();
131     }
132 
setChartEnabled(boolean enabled)133     public void setChartEnabled(boolean enabled) {
134         if (mChartEnabled != enabled) {
135             mChartEnabled = enabled;
136             notifyChanged();
137         }
138     }
139 
setLabels(CharSequence start, CharSequence end)140     public void setLabels(CharSequence start, CharSequence end) {
141         mStartLabel = start;
142         mEndLabel = end;
143         notifyChanged();
144     }
145 
setUsageNumbers(long used, long dataPlanSize, boolean hasMobileData)146     void setUsageNumbers(long used, long dataPlanSize, boolean hasMobileData) {
147         mDataplanUse = used;
148         mDataplanSize = dataPlanSize;
149         mHasMobileData = hasMobileData;
150         notifyChanged();
151     }
152 
setWifiMode(boolean isWifiMode, String usagePeriod, boolean isSingleWifi)153     void setWifiMode(boolean isWifiMode, String usagePeriod, boolean isSingleWifi) {
154         mWifiMode = isWifiMode;
155         mUsagePeriod = usagePeriod;
156         mSingleWifi = isSingleWifi;
157         notifyChanged();
158     }
159 
160     @Override
onBindViewHolder(PreferenceViewHolder holder)161     public void onBindViewHolder(PreferenceViewHolder holder) {
162         super.onBindViewHolder(holder);
163 
164         ProgressBar bar = getProgressBar(holder);
165         if (mChartEnabled && (!TextUtils.isEmpty(mStartLabel) || !TextUtils.isEmpty(mEndLabel))) {
166             bar.setVisibility(View.VISIBLE);
167             getLabelBar(holder).setVisibility(View.VISIBLE);
168             bar.setProgress((int) (mProgress * 100));
169             (getLabel1(holder)).setText(mStartLabel);
170             (getLabel2(holder)).setText(mEndLabel);
171         } else {
172             bar.setVisibility(View.GONE);
173             getLabelBar(holder).setVisibility(View.GONE);
174         }
175 
176         updateDataUsageLabels(holder);
177 
178         TextView usageTitle = getUsageTitle(holder);
179         TextView carrierInfo = getCarrierInfo(holder);
180         Button launchButton = getLaunchButton(holder);
181         TextView limitInfo = getDataLimits(holder);
182 
183         if (mWifiMode && mSingleWifi) {
184             updateCycleTimeText(holder);
185 
186             usageTitle.setVisibility(View.GONE);
187             launchButton.setVisibility(View.GONE);
188             carrierInfo.setVisibility(View.GONE);
189 
190             limitInfo.setVisibility(TextUtils.isEmpty(mLimitInfoText) ? View.GONE : View.VISIBLE);
191             limitInfo.setText(mLimitInfoText);
192         } else if (mWifiMode) {
193             usageTitle.setText(R.string.data_usage_wifi_title);
194             usageTitle.setVisibility(View.VISIBLE);
195             TextView cycleTime = getCycleTime(holder);
196             cycleTime.setText(mUsagePeriod);
197             carrierInfo.setVisibility(View.GONE);
198             limitInfo.setVisibility(View.GONE);
199 
200             final long usageLevel = getHistoricalUsageLevel();
201             if (usageLevel > 0L) {
202                 launchButton.setOnClickListener((view) -> {
203                     launchWifiDataUsage(getContext());
204                 });
205             } else {
206                 launchButton.setEnabled(false);
207             }
208             launchButton.setText(R.string.launch_wifi_text);
209             launchButton.setVisibility(View.VISIBLE);
210         } else {
211             usageTitle.setVisibility(mNumPlans > 1 ? View.VISIBLE : View.GONE);
212             updateCycleTimeText(holder);
213             updateCarrierInfo(carrierInfo);
214             if (mLaunchIntent != null) {
215                 launchButton.setOnClickListener((view) -> {
216                     getContext().startActivity(mLaunchIntent);
217                 });
218                 launchButton.setVisibility(View.VISIBLE);
219             } else {
220                 launchButton.setVisibility(View.GONE);
221             }
222             limitInfo.setVisibility(
223                     TextUtils.isEmpty(mLimitInfoText) ? View.GONE : View.VISIBLE);
224             limitInfo.setText(mLimitInfoText);
225         }
226     }
227 
228     @VisibleForTesting
launchWifiDataUsage(Context context)229     static void launchWifiDataUsage(Context context) {
230         final Bundle args = new Bundle(1);
231         args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE,
232                 new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build());
233         args.putInt(DataUsageList.EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_WIFI);
234         final SubSettingLauncher launcher = new SubSettingLauncher(context)
235                 .setArguments(args)
236                 .setDestination(DataUsageList.class.getName())
237                 .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN);
238         launcher.setTitleRes(R.string.wifi_data_usage);
239         launcher.launch();
240     }
241 
updateDataUsageLabels(PreferenceViewHolder holder)242     private void updateDataUsageLabels(PreferenceViewHolder holder) {
243         TextView usageNumberField = getDataUsed(holder);
244 
245         final Formatter.BytesResult usedResult = Formatter.formatBytes(getContext().getResources(),
246                 mDataplanUse, Formatter.FLAG_CALCULATE_ROUNDED | Formatter.FLAG_IEC_UNITS);
247         final SpannableString usageNumberText = new SpannableString(usedResult.value);
248         final int textSize =
249                 getContext().getResources().getDimensionPixelSize(R.dimen.usage_number_text_size);
250         usageNumberText.setSpan(new AbsoluteSizeSpan(textSize), 0, usageNumberText.length(),
251                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
252         CharSequence template = getContext().getText(R.string.data_used_formatted);
253 
254         CharSequence usageText =
255                 TextUtils.expandTemplate(template, usageNumberText, usedResult.units);
256         usageNumberField.setText(usageText);
257 
258         final MeasurableLinearLayout layout = getLayout(holder);
259 
260         if (mHasMobileData && mNumPlans >= 0 && mDataplanSize > 0L) {
261             TextView usageRemainingField = getDataRemaining(holder);
262             long dataRemaining = mDataplanSize - mDataplanUse;
263             if (dataRemaining >= 0) {
264                 usageRemainingField.setText(
265                         TextUtils.expandTemplate(getContext().getText(R.string.data_remaining),
266                                 DataUsageUtils.formatDataUsage(getContext(), dataRemaining)));
267                 usageRemainingField.setTextColor(
268                         Utils.getColorAttr(getContext(), android.R.attr.colorAccent));
269             } else {
270                 usageRemainingField.setText(
271                         TextUtils.expandTemplate(getContext().getText(R.string.data_overusage),
272                                 DataUsageUtils.formatDataUsage(getContext(), -dataRemaining)));
273                 usageRemainingField.setTextColor(
274                         Utils.getColorAttr(getContext(), android.R.attr.colorError));
275             }
276             layout.setChildren(usageNumberField, usageRemainingField);
277         } else {
278             layout.setChildren(usageNumberField, null);
279         }
280     }
281 
updateCycleTimeText(PreferenceViewHolder holder)282     private void updateCycleTimeText(PreferenceViewHolder holder) {
283         TextView cycleTime = getCycleTime(holder);
284 
285         // Takes zero as a special case which value is never set.
286         if (mCycleEndTimeMs == CYCLE_TIME_UNINITIAL_VALUE) {
287             cycleTime.setVisibility(View.GONE);
288             return;
289         }
290 
291         cycleTime.setVisibility(View.VISIBLE);
292         long millisLeft = mCycleEndTimeMs - System.currentTimeMillis();
293         if (millisLeft <= 0) {
294             cycleTime.setText(getContext().getString(R.string.billing_cycle_none_left));
295         } else {
296             int daysLeft = (int) (millisLeft / MILLIS_IN_A_DAY);
297             MessageFormat msgFormat = new MessageFormat(
298                     getContext().getResources().getString(R.string.billing_cycle_days_left),
299                     Locale.getDefault());
300             Map<String, Object> arguments = new HashMap<>();
301             arguments.put("count", daysLeft);
302             cycleTime.setText(daysLeft < 1
303                     ? getContext().getString(R.string.billing_cycle_less_than_one_day_left)
304                     : msgFormat.format(arguments));
305         }
306     }
307 
308 
309     private void updateCarrierInfo(TextView carrierInfo) {
310         if (mNumPlans > 0 && mSnapshotTimeMs >= 0L) {
311             carrierInfo.setVisibility(View.VISIBLE);
312             long updateAgeMillis = calculateTruncatedUpdateAge();
313 
314             int textResourceId;
315             CharSequence updateTime = null;
316             if (updateAgeMillis == 0) {
317                 if (mCarrierName != null) {
318                     textResourceId = R.string.carrier_and_update_now_text;
319                 } else {
320                     textResourceId = R.string.no_carrier_update_now_text;
321                 }
322             } else {
323                 if (mCarrierName != null) {
324                     textResourceId = R.string.carrier_and_update_text;
325                 } else {
326                     textResourceId = R.string.no_carrier_update_text;
327                 }
328                 updateTime = StringUtil.formatElapsedTime(
329                         getContext(),
330                         updateAgeMillis,
331                         false /* withSeconds */,
332                         false /* collapseTimeUnit */);
333             }
334             carrierInfo.setText(TextUtils.expandTemplate(
335                     getContext().getText(textResourceId),
336                     mCarrierName,
337                     updateTime));
338 
339             if (updateAgeMillis <= WARNING_AGE) {
340                 setCarrierInfoTextStyle(
341                         carrierInfo, android.R.attr.textColorSecondary, Typeface.SANS_SERIF);
342             } else {
343                 setCarrierInfoTextStyle(carrierInfo, android.R.attr.colorError, SANS_SERIF_MEDIUM);
344             }
345         } else {
346             carrierInfo.setVisibility(View.GONE);
347         }
348     }
349 
350     /**
351      * Returns the time since the last carrier update, as defined by {@link #mSnapshotTimeMs},
352      * truncated to the nearest day / hour / minute in milliseconds, or 0 if less than 1 min.
353      */
calculateTruncatedUpdateAge()354     private long calculateTruncatedUpdateAge() {
355         long updateAgeMillis = System.currentTimeMillis() - mSnapshotTimeMs;
356 
357         // Round to nearest whole unit
358         if (updateAgeMillis >= TimeUnit.DAYS.toMillis(1)) {
359             return (updateAgeMillis / TimeUnit.DAYS.toMillis(1)) * TimeUnit.DAYS.toMillis(1);
360         } else if (updateAgeMillis >= TimeUnit.HOURS.toMillis(1)) {
361             return (updateAgeMillis / TimeUnit.HOURS.toMillis(1)) * TimeUnit.HOURS.toMillis(1);
362         } else if (updateAgeMillis >= TimeUnit.MINUTES.toMillis(1)) {
363             return (updateAgeMillis / TimeUnit.MINUTES.toMillis(1)) * TimeUnit.MINUTES.toMillis(1);
364         } else {
365             return 0;
366         }
367     }
368 
setCarrierInfoTextStyle( TextView carrierInfo, @AttrRes int colorId, Typeface typeface)369     private void setCarrierInfoTextStyle(
370             TextView carrierInfo, @AttrRes int colorId, Typeface typeface) {
371         carrierInfo.setTextColor(Utils.getColorAttr(getContext(), colorId));
372         carrierInfo.setTypeface(typeface);
373     }
374 
375     @VisibleForTesting
getHistoricalUsageLevel()376     protected long getHistoricalUsageLevel() {
377         final DataUsageController controller = new DataUsageController(getContext());
378         return controller.getHistoricalUsageLevel(
379                 new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build());
380     }
381 
382     @VisibleForTesting
getUsageTitle(PreferenceViewHolder holder)383     protected TextView getUsageTitle(PreferenceViewHolder holder) {
384         return (TextView) holder.findViewById(R.id.usage_title);
385     }
386 
387     @VisibleForTesting
getCycleTime(PreferenceViewHolder holder)388     protected TextView getCycleTime(PreferenceViewHolder holder) {
389         return (TextView) holder.findViewById(R.id.cycle_left_time);
390     }
391 
392     @VisibleForTesting
getCarrierInfo(PreferenceViewHolder holder)393     protected TextView getCarrierInfo(PreferenceViewHolder holder) {
394         return (TextView) holder.findViewById(R.id.carrier_and_update);
395     }
396 
397     @VisibleForTesting
getDataLimits(PreferenceViewHolder holder)398     protected TextView getDataLimits(PreferenceViewHolder holder) {
399         return (TextView) holder.findViewById(R.id.data_limits);
400     }
401 
402     @VisibleForTesting
getDataUsed(PreferenceViewHolder holder)403     protected TextView getDataUsed(PreferenceViewHolder holder) {
404         return (TextView) holder.findViewById(R.id.data_usage_view);
405     }
406 
407     @VisibleForTesting
getDataRemaining(PreferenceViewHolder holder)408     protected TextView getDataRemaining(PreferenceViewHolder holder) {
409         return (TextView) holder.findViewById(R.id.data_remaining_view);
410     }
411 
412     @VisibleForTesting
getLaunchButton(PreferenceViewHolder holder)413     protected Button getLaunchButton(PreferenceViewHolder holder) {
414         return (Button) holder.findViewById(R.id.launch_mdp_app_button);
415     }
416 
417     @VisibleForTesting
getLabelBar(PreferenceViewHolder holder)418     protected LinearLayout getLabelBar(PreferenceViewHolder holder) {
419         return (LinearLayout) holder.findViewById(R.id.label_bar);
420     }
421 
422     @VisibleForTesting
getLabel1(PreferenceViewHolder holder)423     protected TextView getLabel1(PreferenceViewHolder holder) {
424         return (TextView) holder.findViewById(android.R.id.text1);
425     }
426 
427     @VisibleForTesting
getLabel2(PreferenceViewHolder holder)428     protected TextView getLabel2(PreferenceViewHolder holder) {
429         return (TextView) holder.findViewById(android.R.id.text2);
430     }
431 
432     @VisibleForTesting
getProgressBar(PreferenceViewHolder holder)433     protected ProgressBar getProgressBar(PreferenceViewHolder holder) {
434         return (ProgressBar) holder.findViewById(R.id.determinateBar);
435     }
436 
437     @VisibleForTesting
getLayout(PreferenceViewHolder holder)438     protected MeasurableLinearLayout getLayout(PreferenceViewHolder holder) {
439         return (MeasurableLinearLayout) holder.findViewById(R.id.usage_layout);
440     }
441 }
442