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