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