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 new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build()); 229 args.putInt(DataUsageList.EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_WIFI); 230 final SubSettingLauncher launcher = new SubSettingLauncher(context) 231 .setArguments(args) 232 .setDestination(DataUsageList.class.getName()) 233 .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN); 234 launcher.setTitleRes(R.string.wifi_data_usage); 235 launcher.launch(); 236 } 237 updateDataUsageLabels(PreferenceViewHolder holder)238 private void updateDataUsageLabels(PreferenceViewHolder holder) { 239 TextView usageNumberField = getDataUsed(holder); 240 241 final Formatter.BytesResult usedResult = Formatter.formatBytes(getContext().getResources(), 242 mDataplanUse, Formatter.FLAG_CALCULATE_ROUNDED | Formatter.FLAG_IEC_UNITS); 243 final SpannableString usageNumberText = new SpannableString(usedResult.value); 244 final int textSize = 245 getContext().getResources().getDimensionPixelSize(R.dimen.usage_number_text_size); 246 usageNumberText.setSpan(new AbsoluteSizeSpan(textSize), 0, usageNumberText.length(), 247 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 248 CharSequence template = getContext().getText(R.string.data_used_formatted); 249 250 CharSequence usageText = 251 TextUtils.expandTemplate(template, usageNumberText, usedResult.units); 252 usageNumberField.setText(usageText); 253 254 final MeasurableLinearLayout layout = getLayout(holder); 255 256 if (mHasMobileData && mNumPlans >= 0 && mDataplanSize > 0L) { 257 TextView usageRemainingField = getDataRemaining(holder); 258 long dataRemaining = mDataplanSize - mDataplanUse; 259 if (dataRemaining >= 0) { 260 usageRemainingField.setText( 261 TextUtils.expandTemplate(getContext().getText(R.string.data_remaining), 262 DataUsageUtils.formatDataUsage(getContext(), dataRemaining))); 263 usageRemainingField.setTextColor( 264 Utils.getColorAttr(getContext(), android.R.attr.colorAccent)); 265 } else { 266 usageRemainingField.setText( 267 TextUtils.expandTemplate(getContext().getText(R.string.data_overusage), 268 DataUsageUtils.formatDataUsage(getContext(), -dataRemaining))); 269 usageRemainingField.setTextColor( 270 Utils.getColorAttr(getContext(), android.R.attr.colorError)); 271 } 272 layout.setChildren(usageNumberField, usageRemainingField); 273 } else { 274 layout.setChildren(usageNumberField, null); 275 } 276 } 277 updateCycleTimeText(PreferenceViewHolder holder)278 private void updateCycleTimeText(PreferenceViewHolder holder) { 279 TextView cycleTime = getCycleTime(holder); 280 281 // Takes zero as a special case which value is never set. 282 if (mCycleEndTimeMs == CYCLE_TIME_UNINITIAL_VALUE) { 283 cycleTime.setVisibility(View.GONE); 284 return; 285 } 286 287 cycleTime.setVisibility(View.VISIBLE); 288 long millisLeft = mCycleEndTimeMs - System.currentTimeMillis(); 289 if (millisLeft <= 0) { 290 cycleTime.setText(getContext().getString(R.string.billing_cycle_none_left)); 291 } else { 292 int daysLeft = (int) (millisLeft / MILLIS_IN_A_DAY); 293 cycleTime.setText(daysLeft < 1 294 ? getContext().getString(R.string.billing_cycle_less_than_one_day_left) 295 : getContext().getResources().getQuantityString( 296 R.plurals.billing_cycle_days_left, daysLeft, daysLeft)); 297 } 298 } 299 300 301 private void updateCarrierInfo(TextView carrierInfo) { 302 if (mNumPlans > 0 && mSnapshotTimeMs >= 0L) { 303 carrierInfo.setVisibility(View.VISIBLE); 304 long updateAgeMillis = calculateTruncatedUpdateAge(); 305 306 int textResourceId; 307 CharSequence updateTime = null; 308 if (updateAgeMillis == 0) { 309 if (mCarrierName != null) { 310 textResourceId = R.string.carrier_and_update_now_text; 311 } else { 312 textResourceId = R.string.no_carrier_update_now_text; 313 } 314 } else { 315 if (mCarrierName != null) { 316 textResourceId = R.string.carrier_and_update_text; 317 } else { 318 textResourceId = R.string.no_carrier_update_text; 319 } 320 updateTime = StringUtil.formatElapsedTime( 321 getContext(), 322 updateAgeMillis, 323 false /* withSeconds */, 324 false /* collapseTimeUnit */); 325 } 326 carrierInfo.setText(TextUtils.expandTemplate( 327 getContext().getText(textResourceId), 328 mCarrierName, 329 updateTime)); 330 331 if (updateAgeMillis <= WARNING_AGE) { 332 setCarrierInfoTextStyle( 333 carrierInfo, android.R.attr.textColorSecondary, Typeface.SANS_SERIF); 334 } else { 335 setCarrierInfoTextStyle(carrierInfo, android.R.attr.colorError, SANS_SERIF_MEDIUM); 336 } 337 } else { 338 carrierInfo.setVisibility(View.GONE); 339 } 340 } 341 342 /** 343 * Returns the time since the last carrier update, as defined by {@link #mSnapshotTimeMs}, 344 * truncated to the nearest day / hour / minute in milliseconds, or 0 if less than 1 min. 345 */ calculateTruncatedUpdateAge()346 private long calculateTruncatedUpdateAge() { 347 long updateAgeMillis = System.currentTimeMillis() - mSnapshotTimeMs; 348 349 // Round to nearest whole unit 350 if (updateAgeMillis >= TimeUnit.DAYS.toMillis(1)) { 351 return (updateAgeMillis / TimeUnit.DAYS.toMillis(1)) * TimeUnit.DAYS.toMillis(1); 352 } else if (updateAgeMillis >= TimeUnit.HOURS.toMillis(1)) { 353 return (updateAgeMillis / TimeUnit.HOURS.toMillis(1)) * TimeUnit.HOURS.toMillis(1); 354 } else if (updateAgeMillis >= TimeUnit.MINUTES.toMillis(1)) { 355 return (updateAgeMillis / TimeUnit.MINUTES.toMillis(1)) * TimeUnit.MINUTES.toMillis(1); 356 } else { 357 return 0; 358 } 359 } 360 setCarrierInfoTextStyle( TextView carrierInfo, @AttrRes int colorId, Typeface typeface)361 private void setCarrierInfoTextStyle( 362 TextView carrierInfo, @AttrRes int colorId, Typeface typeface) { 363 carrierInfo.setTextColor(Utils.getColorAttr(getContext(), colorId)); 364 carrierInfo.setTypeface(typeface); 365 } 366 367 @VisibleForTesting getHistoricalUsageLevel()368 protected long getHistoricalUsageLevel() { 369 final DataUsageController controller = new DataUsageController(getContext()); 370 return controller.getHistoricalUsageLevel( 371 new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build()); 372 } 373 374 @VisibleForTesting getUsageTitle(PreferenceViewHolder holder)375 protected TextView getUsageTitle(PreferenceViewHolder holder) { 376 return (TextView) holder.findViewById(R.id.usage_title); 377 } 378 379 @VisibleForTesting getCycleTime(PreferenceViewHolder holder)380 protected TextView getCycleTime(PreferenceViewHolder holder) { 381 return (TextView) holder.findViewById(R.id.cycle_left_time); 382 } 383 384 @VisibleForTesting getCarrierInfo(PreferenceViewHolder holder)385 protected TextView getCarrierInfo(PreferenceViewHolder holder) { 386 return (TextView) holder.findViewById(R.id.carrier_and_update); 387 } 388 389 @VisibleForTesting getDataLimits(PreferenceViewHolder holder)390 protected TextView getDataLimits(PreferenceViewHolder holder) { 391 return (TextView) holder.findViewById(R.id.data_limits); 392 } 393 394 @VisibleForTesting getDataUsed(PreferenceViewHolder holder)395 protected TextView getDataUsed(PreferenceViewHolder holder) { 396 return (TextView) holder.findViewById(R.id.data_usage_view); 397 } 398 399 @VisibleForTesting getDataRemaining(PreferenceViewHolder holder)400 protected TextView getDataRemaining(PreferenceViewHolder holder) { 401 return (TextView) holder.findViewById(R.id.data_remaining_view); 402 } 403 404 @VisibleForTesting getLaunchButton(PreferenceViewHolder holder)405 protected Button getLaunchButton(PreferenceViewHolder holder) { 406 return (Button) holder.findViewById(R.id.launch_mdp_app_button); 407 } 408 409 @VisibleForTesting getLabelBar(PreferenceViewHolder holder)410 protected LinearLayout getLabelBar(PreferenceViewHolder holder) { 411 return (LinearLayout) holder.findViewById(R.id.label_bar); 412 } 413 414 @VisibleForTesting getLabel1(PreferenceViewHolder holder)415 protected TextView getLabel1(PreferenceViewHolder holder) { 416 return (TextView) holder.findViewById(android.R.id.text1); 417 } 418 419 @VisibleForTesting getLabel2(PreferenceViewHolder holder)420 protected TextView getLabel2(PreferenceViewHolder holder) { 421 return (TextView) holder.findViewById(android.R.id.text2); 422 } 423 424 @VisibleForTesting getProgressBar(PreferenceViewHolder holder)425 protected ProgressBar getProgressBar(PreferenceViewHolder holder) { 426 return (ProgressBar) holder.findViewById(R.id.determinateBar); 427 } 428 429 @VisibleForTesting getLayout(PreferenceViewHolder holder)430 protected MeasurableLinearLayout getLayout(PreferenceViewHolder holder) { 431 return (MeasurableLinearLayout) holder.findViewById(R.id.usage_layout); 432 } 433 } 434