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