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.settingslib.utils;
18 
19 import android.content.Context;
20 import android.icu.text.DateFormat;
21 import android.icu.text.MeasureFormat;
22 import android.icu.text.MeasureFormat.FormatWidth;
23 import android.icu.util.Measure;
24 import android.icu.util.MeasureUnit;
25 import android.text.TextUtils;
26 
27 import androidx.annotation.Nullable;
28 
29 import com.android.settingslib.R;
30 
31 import java.time.Instant;
32 import java.util.Date;
33 import java.util.Locale;
34 import java.util.concurrent.TimeUnit;
35 
36 /** Utility class for keeping power related strings consistent**/
37 public class PowerUtil {
38 
39     private static final long SEVEN_MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(7);
40     private static final long FIFTEEN_MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(15);
41     private static final long ONE_DAY_MILLIS = TimeUnit.DAYS.toMillis(1);
42     private static final long TWO_DAYS_MILLIS = TimeUnit.DAYS.toMillis(2);
43     private static final long ONE_HOUR_MILLIS = TimeUnit.HOURS.toMillis(1);
44 
45     /**
46      * This method produces the text used in various places throughout the system to describe the
47      * remaining battery life of the phone in a consistent manner.
48      *
49      * @param context
50      * @param drainTimeMs The estimated time remaining before the phone dies in milliseconds.
51      * @param percentageString An optional percentage of battery remaining string.
52      * @param basedOnUsage Whether this estimate is based on usage or simple extrapolation.
53      * @return a properly formatted and localized string describing how much time remains
54      * before the battery runs out.
55      */
getBatteryRemainingStringFormatted(Context context, long drainTimeMs, @Nullable String percentageString, boolean basedOnUsage)56     public static String getBatteryRemainingStringFormatted(Context context, long drainTimeMs,
57             @Nullable String percentageString, boolean basedOnUsage) {
58         if (drainTimeMs > 0) {
59             if (drainTimeMs <= SEVEN_MINUTES_MILLIS) {
60                 // show a imminent shutdown warning if less than 7 minutes remain
61                 return getShutdownImminentString(context, percentageString);
62             } else if (drainTimeMs <= FIFTEEN_MINUTES_MILLIS) {
63                 // show a less than 15 min remaining warning if appropriate
64                 CharSequence timeString = StringUtil.formatElapsedTime(context,
65                         FIFTEEN_MINUTES_MILLIS,
66                         false /* withSeconds */);
67                 return getUnderFifteenString(context, timeString, percentageString);
68             } else if (drainTimeMs >= TWO_DAYS_MILLIS) {
69                 // just say more than two day if over 48 hours
70                 return getMoreThanTwoDaysString(context, percentageString);
71             } else if (drainTimeMs >= ONE_DAY_MILLIS) {
72                 // show remaining days & hours if more than a day
73                 return getMoreThanOneDayString(context, drainTimeMs,
74                         percentageString, basedOnUsage);
75             } else {
76                 // show the time of day we think you'll run out
77                 return getRegularTimeRemainingString(context, drainTimeMs,
78                         percentageString, basedOnUsage);
79             }
80         }
81         return null;
82     }
83 
84     /**
85      * Method to produce a shortened string describing the remaining battery. Suitable for Quick
86      * Settings and other areas where space is constrained.
87      *
88      * @param context context to fetch descriptions from
89      * @param drainTimeMs The estimated time remaining before the phone dies in milliseconds.
90      *
91      * @return a properly formatted and localized short string describing how much time remains
92      * before the battery runs out.
93      */
94     @Nullable
getBatteryRemainingShortStringFormatted( Context context, long drainTimeMs)95     public static String getBatteryRemainingShortStringFormatted(
96             Context context, long drainTimeMs) {
97         if (drainTimeMs <= 0) {
98             return null;
99         }
100 
101         if (drainTimeMs <= ONE_DAY_MILLIS) {
102             return getRegularTimeRemainingShortString(context, drainTimeMs);
103         } else {
104             return getMoreThanOneDayShortString(context, drainTimeMs,
105                 R.string.power_remaining_duration_only_short);
106         }
107     }
108 
109     /**
110      * This method produces the text used in Settings battery tip to describe the effect after
111      * use the tip.
112      *
113      * @param context
114      * @param drainTimeMs The estimated time remaining before the phone dies in milliseconds.
115      * @return a properly formatted and localized string
116      */
getBatteryTipStringFormatted(Context context, long drainTimeMs)117     public static String getBatteryTipStringFormatted(Context context, long drainTimeMs) {
118         if (drainTimeMs <= 0) {
119             return null;
120         }
121         if (drainTimeMs <= ONE_DAY_MILLIS) {
122             return context.getString(R.string.power_suggestion_extend_battery,
123                 getDateTimeStringFromMs(context, drainTimeMs));
124         } else {
125             return getMoreThanOneDayShortString(context, drainTimeMs,
126                 R.string.power_remaining_only_more_than_subtext);
127         }
128     }
129 
getShutdownImminentString(Context context, String percentageString)130     private static String getShutdownImminentString(Context context, String percentageString) {
131         return TextUtils.isEmpty(percentageString)
132                 ? context.getString(R.string.power_remaining_duration_only_shutdown_imminent)
133                 : context.getString(
134                         R.string.power_remaining_duration_shutdown_imminent,
135                         percentageString);
136     }
137 
getUnderFifteenString(Context context, CharSequence timeString, String percentageString)138     private static String getUnderFifteenString(Context context, CharSequence timeString,
139             String percentageString) {
140         return TextUtils.isEmpty(percentageString)
141                 ? context.getString(R.string.power_remaining_less_than_duration_only, timeString)
142                 : context.getString(
143                         R.string.power_remaining_less_than_duration,
144                         timeString,
145                         percentageString);
146 
147     }
148 
getMoreThanOneDayString(Context context, long drainTimeMs, String percentageString, boolean basedOnUsage)149     private static String getMoreThanOneDayString(Context context, long drainTimeMs,
150             String percentageString, boolean basedOnUsage) {
151         final long roundedTimeMs = roundTimeToNearestThreshold(drainTimeMs, ONE_HOUR_MILLIS);
152         CharSequence timeString = StringUtil.formatElapsedTime(context,
153                 roundedTimeMs,
154                 false /* withSeconds */);
155 
156         if (TextUtils.isEmpty(percentageString)) {
157             int id = basedOnUsage
158                     ? R.string.power_remaining_duration_only_enhanced
159                     : R.string.power_remaining_duration_only;
160             return context.getString(id, timeString);
161         } else {
162             int id = basedOnUsage
163                     ? R.string.power_discharging_duration_enhanced
164                     : R.string.power_discharging_duration;
165             return context.getString(id, timeString, percentageString);
166         }
167     }
168 
getMoreThanOneDayShortString(Context context, long drainTimeMs, int resId)169     private static String getMoreThanOneDayShortString(Context context, long drainTimeMs,
170             int resId) {
171         final long roundedTimeMs = roundTimeToNearestThreshold(drainTimeMs, ONE_HOUR_MILLIS);
172         CharSequence timeString = StringUtil.formatElapsedTime(context, roundedTimeMs,
173                 false /* withSeconds */);
174 
175         return context.getString(resId, timeString);
176     }
177 
getMoreThanTwoDaysString(Context context, String percentageString)178     private static String getMoreThanTwoDaysString(Context context, String percentageString) {
179         final Locale currentLocale = context.getResources().getConfiguration().getLocales().get(0);
180         final MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.SHORT);
181 
182         final Measure daysMeasure = new Measure(2, MeasureUnit.DAY);
183 
184         return TextUtils.isEmpty(percentageString)
185                 ? context.getString(R.string.power_remaining_only_more_than_subtext,
186                         frmt.formatMeasures(daysMeasure))
187                 : context.getString(
188                         R.string.power_remaining_more_than_subtext,
189                         frmt.formatMeasures(daysMeasure),
190                         percentageString);
191     }
192 
getRegularTimeRemainingString(Context context, long drainTimeMs, String percentageString, boolean basedOnUsage)193     private static String getRegularTimeRemainingString(Context context, long drainTimeMs,
194             String percentageString, boolean basedOnUsage) {
195 
196         CharSequence timeString = getDateTimeStringFromMs(context, drainTimeMs);
197 
198         if (TextUtils.isEmpty(percentageString)) {
199             int id = basedOnUsage
200                     ? R.string.power_discharge_by_only_enhanced
201                     : R.string.power_discharge_by_only;
202             return context.getString(id, timeString);
203         } else {
204             int id = basedOnUsage
205                     ? R.string.power_discharge_by_enhanced
206                     : R.string.power_discharge_by;
207             return context.getString(id, timeString, percentageString);
208         }
209     }
210 
getDateTimeStringFromMs(Context context, long drainTimeMs)211     private static CharSequence getDateTimeStringFromMs(Context context, long drainTimeMs) {
212         // Get the time of day we think device will die rounded to the nearest 15 min.
213         final long roundedTimeOfDayMs =
214                 roundTimeToNearestThreshold(
215                         System.currentTimeMillis() + drainTimeMs,
216                         FIFTEEN_MINUTES_MILLIS);
217 
218         // convert the time to a properly formatted string.
219         String skeleton = android.text.format.DateFormat.getTimeFormatString(context);
220         DateFormat fmt = DateFormat.getInstanceForSkeleton(skeleton);
221         Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs));
222         return fmt.format(date);
223     }
224 
getRegularTimeRemainingShortString(Context context, long drainTimeMs)225     private static String getRegularTimeRemainingShortString(Context context, long drainTimeMs) {
226         // Get the time of day we think device will die rounded to the nearest 15 min.
227         final long roundedTimeOfDayMs =
228                 roundTimeToNearestThreshold(
229                         System.currentTimeMillis() + drainTimeMs,
230                         FIFTEEN_MINUTES_MILLIS);
231 
232         // convert the time to a properly formatted string.
233         String skeleton = android.text.format.DateFormat.getTimeFormatString(context);
234         DateFormat fmt = DateFormat.getInstanceForSkeleton(skeleton);
235         Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs));
236         CharSequence timeString = fmt.format(date);
237 
238         return context.getString(R.string.power_discharge_by_only_short, timeString);
239     }
240 
convertUsToMs(long timeUs)241     public static long convertUsToMs(long timeUs) {
242         return timeUs / 1000;
243     }
244 
convertMsToUs(long timeMs)245     public static long convertMsToUs(long timeMs) {
246         return timeMs * 1000;
247     }
248 
249     /**
250      * Rounds a time to the nearest multiple of the provided threshold. Note: This function takes
251      * the absolute value of the inputs since it is only meant to be used for times, not general
252      * purpose rounding.
253      *
254      * ex: roundTimeToNearestThreshold(41, 24) = 48
255      * @param drainTime The amount to round
256      * @param threshold The value to round to a multiple of
257      * @return The rounded value as a long
258      */
roundTimeToNearestThreshold(long drainTime, long threshold)259     public static long roundTimeToNearestThreshold(long drainTime, long threshold) {
260         long time = Math.abs(drainTime);
261         long multiple = Math.abs(threshold);
262         final long remainder = time % multiple;
263         if (remainder < multiple / 2) {
264             return time - remainder;
265         } else {
266             return time - remainder + multiple;
267         }
268     }
269 }