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.ProgressBar; 36 import android.widget.TextView; 37 38 import androidx.annotation.VisibleForTesting; 39 import androidx.preference.Preference; 40 import androidx.preference.PreferenceViewHolder; 41 42 import com.android.settings.R; 43 import com.android.settings.core.SubSettingLauncher; 44 import com.android.settingslib.Utils; 45 import com.android.settingslib.net.DataUsageController; 46 import com.android.settingslib.utils.StringUtil; 47 48 import java.util.Objects; 49 import java.util.concurrent.TimeUnit; 50 51 /** 52 * Provides a summary of data usage. 53 */ 54 public class DataUsageSummaryPreference extends Preference { 55 private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1); 56 private static final long WARNING_AGE = TimeUnit.HOURS.toMillis(6L); 57 @VisibleForTesting 58 static final Typeface SANS_SERIF_MEDIUM = 59 Typeface.create("sans-serif-medium", Typeface.NORMAL); 60 61 private boolean mChartEnabled = true; 62 private CharSequence mStartLabel; 63 private CharSequence mEndLabel; 64 65 /** large vs small size is 36/16 ~ 2.25 */ 66 private static final float LARGER_FONT_RATIO = 2.25f; 67 private static final float SMALLER_FONT_RATIO = 1.0f; 68 69 private boolean mDefaultTextColorSet; 70 private int mDefaultTextColor; 71 private int mNumPlans; 72 /** The specified un-initialized value for cycle time */ 73 private final long CYCLE_TIME_UNINITIAL_VALUE = 0; 74 /** The ending time of the billing cycle in milliseconds since epoch. */ 75 private long mCycleEndTimeMs; 76 /** The time of the last update in standard milliseconds since the epoch */ 77 private long mSnapshotTimeMs; 78 /** Name of carrier, or null if not available */ 79 private CharSequence mCarrierName; 80 private CharSequence mLimitInfoText; 81 private Intent mLaunchIntent; 82 83 /** Progress to display on ProgressBar */ 84 private float mProgress; 85 private boolean mHasMobileData; 86 87 /** 88 * The size of the first registered plan if one exists or the size of the warning if it is set. 89 * -1 if no information is available. 90 */ 91 private long mDataplanSize; 92 93 /** The number of bytes used since the start of the cycle. */ 94 private long mDataplanUse; 95 96 /** WiFi only mode */ 97 private boolean mWifiMode; 98 private String mUsagePeriod; 99 private boolean mSingleWifi; // Shows only one specified WiFi network usage 100 DataUsageSummaryPreference(Context context, AttributeSet attrs)101 public DataUsageSummaryPreference(Context context, AttributeSet attrs) { 102 super(context, attrs); 103 setLayoutResource(R.layout.data_usage_summary_preference); 104 } 105 setLimitInfo(CharSequence text)106 public void setLimitInfo(CharSequence text) { 107 if (!Objects.equals(text, mLimitInfoText)) { 108 mLimitInfoText = text; 109 notifyChanged(); 110 } 111 } 112 setProgress(float progress)113 public void setProgress(float progress) { 114 mProgress = progress; 115 notifyChanged(); 116 } 117 setUsageInfo(long cycleEnd, long snapshotTime, CharSequence carrierName, int numPlans, Intent launchIntent)118 public void setUsageInfo(long cycleEnd, long snapshotTime, CharSequence carrierName, 119 int numPlans, Intent launchIntent) { 120 mCycleEndTimeMs = cycleEnd; 121 mSnapshotTimeMs = snapshotTime; 122 mCarrierName = carrierName; 123 mNumPlans = numPlans; 124 mLaunchIntent = launchIntent; 125 notifyChanged(); 126 } 127 setChartEnabled(boolean enabled)128 public void setChartEnabled(boolean enabled) { 129 if (mChartEnabled != enabled) { 130 mChartEnabled = enabled; 131 notifyChanged(); 132 } 133 } 134 setLabels(CharSequence start, CharSequence end)135 public void setLabels(CharSequence start, CharSequence end) { 136 mStartLabel = start; 137 mEndLabel = end; 138 notifyChanged(); 139 } 140 setUsageNumbers(long used, long dataPlanSize, boolean hasMobileData)141 void setUsageNumbers(long used, long dataPlanSize, boolean hasMobileData) { 142 mDataplanUse = used; 143 mDataplanSize = dataPlanSize; 144 mHasMobileData = hasMobileData; 145 notifyChanged(); 146 } 147 setWifiMode(boolean isWifiMode, String usagePeriod, boolean isSingleWifi)148 void setWifiMode(boolean isWifiMode, String usagePeriod, boolean isSingleWifi) { 149 mWifiMode = isWifiMode; 150 mUsagePeriod = usagePeriod; 151 mSingleWifi = isSingleWifi; 152 notifyChanged(); 153 } 154 155 @Override onBindViewHolder(PreferenceViewHolder holder)156 public void onBindViewHolder(PreferenceViewHolder holder) { 157 super.onBindViewHolder(holder); 158 159 ProgressBar bar = (ProgressBar) holder.findViewById(R.id.determinateBar); 160 if (mChartEnabled && (!TextUtils.isEmpty(mStartLabel) || !TextUtils.isEmpty(mEndLabel))) { 161 bar.setVisibility(View.VISIBLE); 162 holder.findViewById(R.id.label_bar).setVisibility(View.VISIBLE); 163 bar.setProgress((int) (mProgress * 100)); 164 ((TextView) holder.findViewById(android.R.id.text1)).setText(mStartLabel); 165 ((TextView) holder.findViewById(android.R.id.text2)).setText(mEndLabel); 166 } else { 167 bar.setVisibility(View.GONE); 168 holder.findViewById(R.id.label_bar).setVisibility(View.GONE); 169 } 170 171 updateDataUsageLabels(holder); 172 173 TextView usageTitle = (TextView) holder.findViewById(R.id.usage_title); 174 TextView carrierInfo = (TextView) holder.findViewById(R.id.carrier_and_update); 175 Button launchButton = (Button) holder.findViewById(R.id.launch_mdp_app_button); 176 TextView limitInfo = (TextView) holder.findViewById(R.id.data_limits); 177 178 if (mWifiMode && mSingleWifi) { 179 updateCycleTimeText(holder); 180 181 usageTitle.setVisibility(View.GONE); 182 launchButton.setVisibility(View.GONE); 183 carrierInfo.setVisibility(View.GONE); 184 185 limitInfo.setVisibility(TextUtils.isEmpty(mLimitInfoText) ? View.GONE : View.VISIBLE); 186 limitInfo.setText(mLimitInfoText); 187 } else if (mWifiMode) { 188 usageTitle.setText(R.string.data_usage_wifi_title); 189 usageTitle.setVisibility(View.VISIBLE); 190 TextView cycleTime = (TextView) holder.findViewById(R.id.cycle_left_time); 191 cycleTime.setText(mUsagePeriod); 192 carrierInfo.setVisibility(View.GONE); 193 limitInfo.setVisibility(View.GONE); 194 195 final long usageLevel = getHistoricalUsageLevel(); 196 if (usageLevel > 0L) { 197 launchButton.setOnClickListener((view) -> { 198 launchWifiDataUsage(getContext()); 199 }); 200 } else { 201 launchButton.setEnabled(false); 202 } 203 launchButton.setText(R.string.launch_wifi_text); 204 launchButton.setVisibility(View.VISIBLE); 205 } else { 206 usageTitle.setVisibility(mNumPlans > 1 ? View.VISIBLE : View.GONE); 207 updateCycleTimeText(holder); 208 updateCarrierInfo(carrierInfo); 209 if (mLaunchIntent != null) { 210 launchButton.setOnClickListener((view) -> { 211 getContext().startActivity(mLaunchIntent); 212 }); 213 launchButton.setVisibility(View.VISIBLE); 214 } else { 215 launchButton.setVisibility(View.GONE); 216 } 217 limitInfo.setVisibility( 218 TextUtils.isEmpty(mLimitInfoText) ? View.GONE : View.VISIBLE); 219 limitInfo.setText(mLimitInfoText); 220 } 221 } 222 223 @VisibleForTesting launchWifiDataUsage(Context context)224 static void launchWifiDataUsage(Context context) { 225 final Bundle args = new Bundle(1); 226 args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, 227 NetworkTemplate.buildTemplateWifiWildcard()); 228 args.putInt(DataUsageList.EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_WIFI); 229 final SubSettingLauncher launcher = new SubSettingLauncher(context) 230 .setArguments(args) 231 .setDestination(DataUsageList.class.getName()) 232 .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN); 233 launcher.setTitleRes(R.string.wifi_data_usage); 234 launcher.launch(); 235 } 236 updateDataUsageLabels(PreferenceViewHolder holder)237 private void updateDataUsageLabels(PreferenceViewHolder holder) { 238 TextView usageNumberField = (TextView) holder.findViewById(R.id.data_usage_view); 239 240 final Formatter.BytesResult usedResult = Formatter.formatBytes(getContext().getResources(), 241 mDataplanUse, Formatter.FLAG_CALCULATE_ROUNDED | Formatter.FLAG_IEC_UNITS); 242 final SpannableString usageNumberText = new SpannableString(usedResult.value); 243 final int textSize = 244 getContext().getResources().getDimensionPixelSize(R.dimen.usage_number_text_size); 245 usageNumberText.setSpan(new AbsoluteSizeSpan(textSize), 0, usageNumberText.length(), 246 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 247 CharSequence template = getContext().getText(R.string.data_used_formatted); 248 249 CharSequence usageText = 250 TextUtils.expandTemplate(template, usageNumberText, usedResult.units); 251 usageNumberField.setText(usageText); 252 253 final MeasurableLinearLayout layout = 254 (MeasurableLinearLayout) holder.findViewById(R.id.usage_layout); 255 256 if (mHasMobileData && mNumPlans >= 0 && mDataplanSize > 0L) { 257 TextView usageRemainingField = (TextView) holder.findViewById(R.id.data_remaining_view); 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 = (TextView) holder.findViewById(R.id.cycle_left_time); 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(), updateAgeMillis, false /* withSeconds */); 322 } 323 carrierInfo.setText(TextUtils.expandTemplate( 324 getContext().getText(textResourceId), 325 mCarrierName, 326 updateTime)); 327 328 if (updateAgeMillis <= WARNING_AGE) { 329 setCarrierInfoTextStyle( 330 carrierInfo, android.R.attr.textColorSecondary, Typeface.SANS_SERIF); 331 } else { 332 setCarrierInfoTextStyle(carrierInfo, android.R.attr.colorError, SANS_SERIF_MEDIUM); 333 } 334 } else { 335 carrierInfo.setVisibility(View.GONE); 336 } 337 } 338 339 /** 340 * Returns the time since the last carrier update, as defined by {@link #mSnapshotTimeMs}, 341 * truncated to the nearest day / hour / minute in milliseconds, or 0 if less than 1 min. 342 */ calculateTruncatedUpdateAge()343 private long calculateTruncatedUpdateAge() { 344 long updateAgeMillis = System.currentTimeMillis() - mSnapshotTimeMs; 345 346 // Round to nearest whole unit 347 if (updateAgeMillis >= TimeUnit.DAYS.toMillis(1)) { 348 return (updateAgeMillis / TimeUnit.DAYS.toMillis(1)) * TimeUnit.DAYS.toMillis(1); 349 } else if (updateAgeMillis >= TimeUnit.HOURS.toMillis(1)) { 350 return (updateAgeMillis / TimeUnit.HOURS.toMillis(1)) * TimeUnit.HOURS.toMillis(1); 351 } else if (updateAgeMillis >= TimeUnit.MINUTES.toMillis(1)) { 352 return (updateAgeMillis / TimeUnit.MINUTES.toMillis(1)) * TimeUnit.MINUTES.toMillis(1); 353 } else { 354 return 0; 355 } 356 } 357 setCarrierInfoTextStyle( TextView carrierInfo, @AttrRes int colorId, Typeface typeface)358 private void setCarrierInfoTextStyle( 359 TextView carrierInfo, @AttrRes int colorId, Typeface typeface) { 360 carrierInfo.setTextColor(Utils.getColorAttr(getContext(), colorId)); 361 carrierInfo.setTypeface(typeface); 362 } 363 364 @VisibleForTesting getHistoricalUsageLevel()365 long getHistoricalUsageLevel() { 366 final DataUsageController controller = new DataUsageController(getContext()); 367 return controller.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()); 368 } 369 370 } 371