1 /*
2  * Copyright (C) 2019 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.car.settings.datausage;
18 
19 import android.car.drivingstate.CarUxRestrictions;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.PackageManager;
23 import android.net.INetworkPolicyManager;
24 import android.os.ServiceManager;
25 import android.telephony.SubscriptionInfo;
26 import android.telephony.SubscriptionManager;
27 import android.telephony.SubscriptionPlan;
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.RecurrenceRule;
34 
35 import androidx.annotation.Nullable;
36 import androidx.annotation.VisibleForTesting;
37 
38 import com.android.car.settings.R;
39 import com.android.car.settings.common.FragmentController;
40 import com.android.car.settings.common.Logger;
41 import com.android.car.settings.network.NetworkBasePreferenceController;
42 import com.android.car.settings.network.NetworkUtils;
43 import com.android.settingslib.net.DataUsageController;
44 import com.android.settingslib.utils.StringUtil;
45 
46 import java.util.List;
47 import java.util.concurrent.TimeUnit;
48 
49 /**
50  * Business logic for setting the {@link DataUsageSummaryPreference} with the current data usage and
51  * the appropriate summary text.
52  */
53 public class DataUsageSummaryPreferenceController extends
54         NetworkBasePreferenceController<DataUsageSummaryPreference> {
55     private static final Logger LOG = new Logger(DataUsageSummaryPreferenceController.class);
56 
57     private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1);
58     private static final long MILLIS_IN_AN_HOUR = TimeUnit.HOURS.toMillis(1);
59     private static final long MILLIS_IN_A_MINUTE = TimeUnit.MINUTES.toMillis(1);
60     private static final long MILLIS_IN_A_SECOND = TimeUnit.SECONDS.toMillis(1);
61     private static final int MAX_PROGRESS_BAR_VALUE = 1000;
62 
63     @VisibleForTesting
64     static final long WARNING_AGE = TimeUnit.HOURS.toMillis(6L);
65 
66     private final SubscriptionManager mSubscriptionManager;
67     private final DataUsageController mDataUsageController;
68 
69     /** Name of the carrier, or null if not available */
70     @Nullable
71     private CharSequence mCarrierName;
72     /** The number of registered plans, [0,N] */
73     private int mDataplanCount;
74     /** The time of the last update in milliseconds since the epoch, or -1 if unknown */
75     private long mSnapshotTime;
76     /** The size of the first registered plan if one exists. -1 if no information is available. */
77     private long mDataplanSize = -1;
78     /**
79      * Limit to track. Size of the first registered plan if one exists. Otherwise size of data limit
80      * or warning.
81      */
82     private long mDataplanTrackingThreshold;
83     /** The number of bytes used since the start of the cycle. */
84     private long mDataplanUse;
85     /** The ending time of the billing cycle in ms since the epoch */
86     private long mCycleEnd;
87     private Intent mManageSubscriptionIntent;
88 
DataUsageSummaryPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)89     public DataUsageSummaryPreferenceController(Context context, String preferenceKey,
90             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
91         this(context, preferenceKey, fragmentController, uxRestrictions,
92                 context.getSystemService(SubscriptionManager.class),
93                 new DataUsageController(context));
94     }
95 
96     @VisibleForTesting
DataUsageSummaryPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, SubscriptionManager subscriptionManager, DataUsageController dataUsageController)97     DataUsageSummaryPreferenceController(Context context, String preferenceKey,
98             FragmentController fragmentController, CarUxRestrictions uxRestrictions,
99             SubscriptionManager subscriptionManager, DataUsageController dataUsageController) {
100         super(context, preferenceKey, fragmentController, uxRestrictions);
101         mSubscriptionManager = subscriptionManager;
102         mDataUsageController = dataUsageController;
103     }
104 
105     @Override
getPreferenceType()106     protected Class<DataUsageSummaryPreference> getPreferenceType() {
107         return DataUsageSummaryPreference.class;
108     }
109 
110     @Override
getAvailabilityStatus()111     protected int getAvailabilityStatus() {
112         return NetworkUtils.hasSim(getTelephonyManager()) ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
113     }
114 
115     @Override
onCreateInternal()116     protected void onCreateInternal() {
117         getPreference().setMin(0);
118         getPreference().setMax(MAX_PROGRESS_BAR_VALUE);
119     }
120 
121     @Override
updateState(DataUsageSummaryPreference preference)122     protected void updateState(DataUsageSummaryPreference preference) {
123         DataUsageController.DataUsageInfo info = mDataUsageController.getDataUsageInfo(
124                 getNetworkTemplate());
125 
126         if (mSubscriptionManager != null) {
127             refreshDataplanInfo(info);
128         }
129 
130         preference.setTitle(getUsageText());
131         preference.setManageSubscriptionIntent(mManageSubscriptionIntent);
132 
133         // Carrier Info has special styling based on when it was last updated.
134         preference.setCarrierInfoText(getCarrierInfoText());
135         long updateAgeMillis = calculateTruncatedUpdateAge(mSnapshotTime);
136         if (updateAgeMillis <= WARNING_AGE) {
137             preference.setCarrierInfoTextStyle(R.style.DataUsageSummaryCarrierInfoTextAppearance);
138         } else {
139             preference.setCarrierInfoTextStyle(
140                     R.style.DataUsageSummaryCarrierInfoWarningTextAppearance);
141         }
142 
143         // Set the progress bar values.
144         preference.setMinLabel(DataUsageUtils.bytesToIecUnits(getContext(), /* byteValue= */ 0));
145         preference.setMaxLabel(
146                 DataUsageUtils.bytesToIecUnits(getContext(), mDataplanTrackingThreshold));
147         preference.setProgress(scaleUsage(mDataplanUse, mDataplanTrackingThreshold));
148     }
149 
getUsageText()150     private CharSequence getUsageText() {
151         Formatter.BytesResult usedResult = Formatter.formatBytes(getContext().getResources(),
152                 mDataplanUse, Formatter.FLAG_CALCULATE_ROUNDED | Formatter.FLAG_IEC_UNITS);
153         SpannableString usageNumberText = new SpannableString(usedResult.value);
154         int textSize = getContext().getResources().getDimensionPixelSize(
155                 R.dimen.usage_number_text_size);
156 
157         // Set the usage text (only the number) to the size defined by usage_number_text_size.
158         usageNumberText.setSpan(new AbsoluteSizeSpan(textSize), /* start= */ 0,
159                 usageNumberText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
160 
161         CharSequence template = getContext().getText(R.string.data_used_formatted);
162         CharSequence usageText = TextUtils.expandTemplate(template, usageNumberText,
163                 usedResult.units);
164         return usageText;
165     }
166 
getCarrierInfoText()167     private CharSequence getCarrierInfoText() {
168         if (mDataplanCount > 0 && mSnapshotTime >= 0L) {
169             long updateAgeMillis = calculateTruncatedUpdateAge(mSnapshotTime);
170 
171             int textResourceId;
172             CharSequence updateTime = null;
173             if (updateAgeMillis == 0) {
174                 if (mCarrierName != null) {
175                     textResourceId = R.string.carrier_and_update_now_text;
176                 } else {
177                     textResourceId = R.string.no_carrier_update_now_text;
178                 }
179             } else {
180                 if (mCarrierName != null) {
181                     textResourceId = R.string.carrier_and_update_text;
182                 } else {
183                     textResourceId = R.string.no_carrier_update_text;
184                 }
185                 updateTime = StringUtil.formatElapsedTime(getContext(),
186                         updateAgeMillis, /* withSeconds= */ false, /* collapseTimeUnit= */ false);
187             }
188             return TextUtils.expandTemplate(getContext().getText(textResourceId), mCarrierName,
189                     updateTime);
190         }
191 
192         return null;
193     }
194 
refreshDataplanInfo(DataUsageController.DataUsageInfo info)195     private void refreshDataplanInfo(DataUsageController.DataUsageInfo info) {
196         // Reset data before overwriting.
197         mCarrierName = null;
198         mDataplanCount = 0;
199         mSnapshotTime = -1L;
200         mDataplanSize = -1L;
201         mDataplanTrackingThreshold = getSummaryLimit(info);
202         mDataplanUse = info.usageLevel;
203         mCycleEnd = info.cycleEnd;
204 
205         SubscriptionInfo subInfo = mSubscriptionManager.getActiveSubscriptionInfo(getSubId());
206         if (subInfo != null) {
207             mCarrierName = subInfo.getCarrierName();
208             List<SubscriptionPlan> plans = mSubscriptionManager.getSubscriptionPlans(getSubId());
209             SubscriptionPlan primaryPlan = DataUsageUtils.getPrimaryPlan(mSubscriptionManager,
210                     getSubId());
211             if (primaryPlan != null) {
212                 mDataplanCount = plans.size();
213                 mDataplanSize = primaryPlan.getDataLimitBytes();
214                 if (mDataplanSize == SubscriptionPlan.BYTES_UNLIMITED) {
215                     mDataplanSize = -1L;
216                 }
217                 mDataplanTrackingThreshold = mDataplanSize;
218                 mDataplanUse = primaryPlan.getDataUsageBytes();
219 
220                 RecurrenceRule rule = primaryPlan.getCycleRule();
221                 if (rule != null && rule.start != null && rule.end != null) {
222                     mCycleEnd = rule.end.toEpochSecond() * MILLIS_IN_A_SECOND;
223                 }
224                 mSnapshotTime = primaryPlan.getDataUsageTime();
225             }
226         }
227         mManageSubscriptionIntent = createManageSubscriptionIntent(getSubId());
228     }
229 
230     /**
231      * Create an {@link Intent} that can be launched towards the carrier app
232      * that is currently defining the billing relationship plan through
233      * {@link INetworkPolicyManager#setSubscriptionPlans(int, SubscriptionPlan [], String)}.
234      *
235      * @return ready to launch Intent targeted towards the carrier app, or
236      *         {@code null} if no carrier app is defined, or if the defined
237      *         carrier app provides no management activity.
238      */
createManageSubscriptionIntent(int subId)239     private Intent createManageSubscriptionIntent(int subId) {
240         INetworkPolicyManager networkPolicyManager = INetworkPolicyManager.Stub.asInterface(
241                 ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
242         String owner = "";
243         try {
244             owner = networkPolicyManager.getSubscriptionPlansOwner(subId);
245         } catch (Exception ex) {
246             LOG.w("Fail to get subscription plan owner for subId " + subId, ex);
247         }
248 
249         if (TextUtils.isEmpty(owner)) {
250             return null;
251         }
252 
253         List<SubscriptionPlan> plans = mSubscriptionManager.getSubscriptionPlans(subId);
254         if (plans.isEmpty()) {
255             return null;
256         }
257 
258         Intent intent = new Intent(SubscriptionManager.ACTION_MANAGE_SUBSCRIPTION_PLANS);
259         intent.setPackage(owner);
260         intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId);
261 
262         if (getContext().getPackageManager().queryIntentActivities(intent,
263                 PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) {
264             return null;
265         }
266 
267         return intent;
268     }
269 
270     /** Scales the current usage to be an integer between 0 and {@link #MAX_PROGRESS_BAR_VALUE}. */
scaleUsage(long usage, long maxUsage)271     private int scaleUsage(long usage, long maxUsage) {
272         if (maxUsage == 0) {
273             return 0;
274         }
275         return (int) ((usage / (float) maxUsage) * MAX_PROGRESS_BAR_VALUE);
276     }
277 
278     /**
279      * Gets the max displayed limit based on {@link DataUsageController.DataUsageInfo}.
280      *
281      * @return the most appropriate limit for the data usage summary. Use the total usage when it
282      * is higher than the limit and warning level. Use the limit when it is set and less than usage.
283      * Otherwise use warning level.
284      */
getSummaryLimit(DataUsageController.DataUsageInfo info)285     private static long getSummaryLimit(DataUsageController.DataUsageInfo info) {
286         long limit = info.limitLevel;
287         if (limit <= 0) {
288             limit = info.warningLevel;
289         }
290         if (info.usageLevel > limit) {
291             limit = info.usageLevel;
292         }
293         return limit;
294     }
295 
296     /**
297      * Returns the time since the last carrier update, as defined by {@link #mSnapshotTime},
298      * truncated to the nearest day / hour / minute in milliseconds, or 0 if less than 1 min.
299      */
calculateTruncatedUpdateAge(long snapshotTime)300     private long calculateTruncatedUpdateAge(long snapshotTime) {
301         long updateAgeMillis = System.currentTimeMillis() - snapshotTime;
302 
303         // Round to nearest whole unit
304         if (updateAgeMillis >= MILLIS_IN_A_DAY) {
305             return (updateAgeMillis / MILLIS_IN_A_DAY) * MILLIS_IN_A_DAY;
306         } else if (updateAgeMillis >= MILLIS_IN_AN_HOUR) {
307             return (updateAgeMillis / MILLIS_IN_AN_HOUR) * MILLIS_IN_AN_HOUR;
308         } else if (updateAgeMillis >= MILLIS_IN_A_MINUTE) {
309             return (updateAgeMillis / MILLIS_IN_A_MINUTE) * MILLIS_IN_A_MINUTE;
310         } else {
311             return 0;
312         }
313     }
314 }
315