| /* | |
| * Copyright (C) 2018 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.settings.datausage; | |
| import android.annotation.AttrRes; | |
| import android.content.Context; | |
| import android.graphics.Typeface; | |
| import android.icu.text.MessageFormat; | |
| import android.telephony.SubscriptionPlan; | |
| import android.text.Spannable; | |
| import android.text.SpannableString; | |
| import android.text.TextUtils; | |
| import android.text.style.AbsoluteSizeSpan; | |
| import android.util.AttributeSet; | |
| import android.view.View; | |
| import android.widget.LinearLayout; | |
| import android.widget.ProgressBar; | |
| import android.widget.TextView; | |
| import androidx.annotation.NonNull; | |
| import androidx.annotation.Nullable; | |
| import androidx.annotation.VisibleForTesting; | |
| import androidx.preference.Preference; | |
| import androidx.preference.PreferenceViewHolder; | |
| import com.android.settings.R; | |
| import com.android.settings.datausage.lib.DataUsageFormatter; | |
| import com.android.settingslib.Utils; | |
| import com.android.settingslib.spaprivileged.framework.common.BytesFormatter; | |
| import com.android.settingslib.utils.StringUtil; | |
| import com.android.settingslib.widget.GroupSectionDividerMixin; | |
| import com.android.settingslib.widget.SettingsThemeHelper; | |
| import java.util.HashMap; | |
| import java.util.Locale; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.concurrent.TimeUnit; | |
| /** | |
| * Provides a summary of data usage. | |
| */ | |
| public class DataUsageSummaryPreference extends Preference implements GroupSectionDividerMixin { | |
| private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1); | |
| private static final long WARNING_AGE = TimeUnit.HOURS.toMillis(6L); | |
| @VisibleForTesting | |
| static final Typeface SANS_SERIF_MEDIUM = | |
| Typeface.create("sans-serif-medium", Typeface.NORMAL); | |
| private boolean mChartEnabled = true; | |
| private CharSequence mStartLabel; | |
| private CharSequence mEndLabel; | |
| private int mNumPlans; | |
| /** The ending time of the billing cycle in milliseconds since epoch. */ | |
| @Nullable | |
| private Long mCycleEndTimeMs; | |
| /** The time of the last update in standard milliseconds since the epoch */ | |
| private long mSnapshotTimeMs = SubscriptionPlan.TIME_UNKNOWN; | |
| /** Name of carrier, or null if not available */ | |
| private CharSequence mCarrierName; | |
| private CharSequence mLimitInfoText; | |
| /** Progress to display on ProgressBar */ | |
| private float mProgress; | |
| /** | |
| * The size of the first registered plan if one exists or the size of the warning if it is set. | |
| * -1 if no information is available. | |
| */ | |
| private long mDataplanSize; | |
| /** The number of bytes used since the start of the cycle. */ | |
| private long mDataplanUse; | |
| private boolean mIsExpressiveTheme; | |
| public DataUsageSummaryPreference(Context context, AttributeSet attrs) { | |
| this(context, attrs, SettingsThemeHelper.isExpressiveTheme(context)); | |
| } | |
| public DataUsageSummaryPreference(Context context, AttributeSet attrs, | |
| boolean isExpressiveTheme) { | |
| super(context, attrs); | |
| mIsExpressiveTheme = isExpressiveTheme; | |
| int resId = mIsExpressiveTheme | |
| ? R.layout.data_usage_summary_preference_expressive | |
| : R.layout.data_usage_summary_preference; | |
| setLayoutResource(resId); | |
| } | |
| public void setLimitInfo(CharSequence text) { | |
| if (!Objects.equals(text, mLimitInfoText)) { | |
| mLimitInfoText = text; | |
| notifyChanged(); | |
| } | |
| } | |
| public void setProgress(float progress) { | |
| mProgress = progress; | |
| notifyChanged(); | |
| } | |
| /** | |
| * Sets the usage info. | |
| */ | |
| public void setUsageInfo(@Nullable Long cycleEnd, long snapshotTime, CharSequence carrierName, | |
| int numPlans) { | |
| mCycleEndTimeMs = cycleEnd; | |
| mSnapshotTimeMs = snapshotTime; | |
| mCarrierName = carrierName; | |
| mNumPlans = numPlans; | |
| notifyChanged(); | |
| } | |
| public void setChartEnabled(boolean enabled) { | |
| if (mChartEnabled != enabled) { | |
| mChartEnabled = enabled; | |
| notifyChanged(); | |
| } | |
| } | |
| public void setLabels(CharSequence start, CharSequence end) { | |
| mStartLabel = start; | |
| mEndLabel = end; | |
| notifyChanged(); | |
| } | |
| /** | |
| * Sets the usage numbers. | |
| */ | |
| public void setUsageNumbers(long used, long dataPlanSize) { | |
| mDataplanUse = used; | |
| mDataplanSize = dataPlanSize; | |
| notifyChanged(); | |
| } | |
| @Override | |
| public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { | |
| super.onBindViewHolder(holder); | |
| ProgressBar bar = getProgressBar(holder); | |
| if (mChartEnabled && (!TextUtils.isEmpty(mStartLabel) || !TextUtils.isEmpty(mEndLabel))) { | |
| bar.setVisibility(View.VISIBLE); | |
| getLabelBar(holder).setVisibility(View.VISIBLE); | |
| bar.setProgress((int) (mProgress * 100)); | |
| if (!mIsExpressiveTheme) { | |
| (getLabel1(holder)).setText(mStartLabel); | |
| } | |
| (getLabel2(holder)).setText(mEndLabel); | |
| } else { | |
| bar.setVisibility(View.GONE); | |
| getLabelBar(holder).setVisibility(View.GONE); | |
| } | |
| updateDataUsageLabels(holder); | |
| updateCycleTimeText(holder); | |
| if (mIsExpressiveTheme) { | |
| return; | |
| } | |
| TextView usageTitle = getUsageTitle(holder); | |
| TextView carrierInfo = getCarrierInfo(holder); | |
| TextView limitInfo = getDataLimits(holder); | |
| usageTitle.setVisibility(mNumPlans > 1 ? View.VISIBLE : View.GONE); | |
| updateCarrierInfo(carrierInfo); | |
| limitInfo.setVisibility(TextUtils.isEmpty(mLimitInfoText) ? View.GONE : View.VISIBLE); | |
| limitInfo.setText(mLimitInfoText); | |
| } | |
| private void updateDataUsageLabels(PreferenceViewHolder holder) { | |
| DataUsageFormatter dataUsageFormatter = new DataUsageFormatter(getContext()); | |
| TextView usageNumberField = getDataUsed(holder); | |
| final BytesFormatter.Result usedResult = | |
| dataUsageFormatter.formatDataUsageWithUnits(mDataplanUse); | |
| final SpannableString usageNumberText = new SpannableString(usedResult.getNumber()); | |
| final int textSize = | |
| getContext().getResources().getDimensionPixelSize(R.dimen.usage_number_text_size); | |
| usageNumberText.setSpan(new AbsoluteSizeSpan(textSize), 0, usageNumberText.length(), | |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); | |
| CharSequence template = getContext().getText(R.string.data_used_formatted); | |
| CharSequence usageText = | |
| TextUtils.expandTemplate(template, usageNumberText, usedResult.getUnits()); | |
| usageNumberField.setText(usageText); | |
| final MeasurableLinearLayout layout = getLayout(holder); | |
| if (mDataplanSize > 0L) { | |
| TextView usageRemainingField = getDataRemaining(holder); | |
| long dataRemaining = mDataplanSize - mDataplanUse; | |
| if (dataRemaining >= 0) { | |
| usageRemainingField.setText( | |
| TextUtils.expandTemplate(getContext().getText(R.string.data_remaining), | |
| dataUsageFormatter.formatDataUsage(dataRemaining))); | |
| usageRemainingField.setTextColor( | |
| Utils.getColorAttr(getContext(), android.R.attr.colorAccent)); | |
| } else { | |
| usageRemainingField.setText( | |
| TextUtils.expandTemplate(getContext().getText(R.string.data_overusage), | |
| dataUsageFormatter.formatDataUsage(-dataRemaining))); | |
| usageRemainingField.setTextColor( | |
| Utils.getColorAttr(getContext(), android.R.attr.colorError)); | |
| } | |
| layout.setChildren(usageNumberField, usageRemainingField); | |
| } else { | |
| layout.setChildren(usageNumberField, null); | |
| } | |
| } | |
| private void updateCycleTimeText(PreferenceViewHolder holder) { | |
| TextView cycleTime = mIsExpressiveTheme ? getLabel1(holder) : getCycleTime(holder); | |
| // Takes zero as a special case which value is never set. | |
| if (mCycleEndTimeMs == null) { | |
| cycleTime.setVisibility(View.GONE); | |
| return; | |
| } | |
| cycleTime.setVisibility(View.VISIBLE); | |
| long millisLeft = mCycleEndTimeMs - System.currentTimeMillis(); | |
| if (millisLeft <= 0) { | |
| cycleTime.setText(getContext().getString(R.string.billing_cycle_none_left)); | |
| } else { | |
| int daysLeft = (int) (millisLeft / MILLIS_IN_A_DAY); | |
| MessageFormat msgFormat = new MessageFormat( | |
| getContext().getResources().getString(R.string.billing_cycle_days_left), | |
| Locale.getDefault()); | |
| Map<String, Object> arguments = new HashMap<>(); | |
| arguments.put("count", daysLeft); | |
| cycleTime.setText(daysLeft < 1 | |
| ? getContext().getString(R.string.billing_cycle_less_than_one_day_left) | |
| : msgFormat.format(arguments)); | |
| } | |
| } | |
| private void updateCarrierInfo(TextView carrierInfo) { | |
| if (mSnapshotTimeMs >= 0L) { | |
| carrierInfo.setVisibility(View.VISIBLE); | |
| long updateAgeMillis = calculateTruncatedUpdateAge(); | |
| int textResourceId; | |
| CharSequence updateTime = null; | |
| if (updateAgeMillis == 0) { | |
| if (mCarrierName != null) { | |
| textResourceId = R.string.carrier_and_update_now_text; | |
| } else { | |
| textResourceId = R.string.no_carrier_update_now_text; | |
| } | |
| } else { | |
| if (mCarrierName != null) { | |
| textResourceId = R.string.carrier_and_update_text; | |
| } else { | |
| textResourceId = R.string.no_carrier_update_text; | |
| } | |
| updateTime = StringUtil.formatElapsedTime( | |
| getContext(), | |
| updateAgeMillis, | |
| false /* withSeconds */, | |
| false /* collapseTimeUnit */); | |
| } | |
| carrierInfo.setText(TextUtils.expandTemplate( | |
| getContext().getText(textResourceId), | |
| mCarrierName, | |
| updateTime)); | |
| if (updateAgeMillis <= WARNING_AGE) { | |
| setCarrierInfoTextStyle( | |
| carrierInfo, android.R.attr.textColorSecondary, Typeface.SANS_SERIF); | |
| } else { | |
| setCarrierInfoTextStyle(carrierInfo, android.R.attr.colorError, SANS_SERIF_MEDIUM); | |
| } | |
| } else { | |
| carrierInfo.setVisibility(View.GONE); | |
| } | |
| } | |
| /** | |
| * Returns the time since the last carrier update, as defined by {@link #mSnapshotTimeMs}, | |
| * truncated to the nearest day / hour / minute in milliseconds, or 0 if less than 1 min. | |
| */ | |
| private long calculateTruncatedUpdateAge() { | |
| long updateAgeMillis = System.currentTimeMillis() - mSnapshotTimeMs; | |
| // Round to nearest whole unit | |
| if (updateAgeMillis >= TimeUnit.DAYS.toMillis(1)) { | |
| return (updateAgeMillis / TimeUnit.DAYS.toMillis(1)) * TimeUnit.DAYS.toMillis(1); | |
| } else if (updateAgeMillis >= TimeUnit.HOURS.toMillis(1)) { | |
| return (updateAgeMillis / TimeUnit.HOURS.toMillis(1)) * TimeUnit.HOURS.toMillis(1); | |
| } else if (updateAgeMillis >= TimeUnit.MINUTES.toMillis(1)) { | |
| return (updateAgeMillis / TimeUnit.MINUTES.toMillis(1)) * TimeUnit.MINUTES.toMillis(1); | |
| } else { | |
| return 0; | |
| } | |
| } | |
| private void setCarrierInfoTextStyle( | |
| TextView carrierInfo, @AttrRes int colorId, Typeface typeface) { | |
| carrierInfo.setTextColor(Utils.getColorAttr(getContext(), colorId)); | |
| carrierInfo.setTypeface(typeface); | |
| } | |
| @VisibleForTesting | |
| protected TextView getUsageTitle(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(R.id.usage_title); | |
| } | |
| @VisibleForTesting | |
| protected TextView getCycleTime(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(R.id.cycle_left_time); | |
| } | |
| @VisibleForTesting | |
| protected TextView getCarrierInfo(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(R.id.carrier_and_update); | |
| } | |
| @VisibleForTesting | |
| protected TextView getDataLimits(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(R.id.data_limits); | |
| } | |
| @VisibleForTesting | |
| protected TextView getDataUsed(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(R.id.data_usage_view); | |
| } | |
| @VisibleForTesting | |
| protected TextView getDataRemaining(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(R.id.data_remaining_view); | |
| } | |
| @VisibleForTesting | |
| protected LinearLayout getLabelBar(PreferenceViewHolder holder) { | |
| return (LinearLayout) holder.findViewById(R.id.label_bar); | |
| } | |
| @VisibleForTesting | |
| protected TextView getLabel1(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(android.R.id.text1); | |
| } | |
| @VisibleForTesting | |
| protected TextView getLabel2(PreferenceViewHolder holder) { | |
| return (TextView) holder.findViewById(android.R.id.text2); | |
| } | |
| @VisibleForTesting | |
| protected ProgressBar getProgressBar(PreferenceViewHolder holder) { | |
| return (ProgressBar) holder.findViewById(R.id.determinateBar); | |
| } | |
| @VisibleForTesting | |
| protected MeasurableLinearLayout getLayout(PreferenceViewHolder holder) { | |
| return (MeasurableLinearLayout) holder.findViewById(R.id.usage_layout); | |
| } | |
| } |