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