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