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