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