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