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.datetime.timezone;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.icu.text.DateFormat;
22 import android.icu.text.DisplayContext;
23 import android.icu.text.SimpleDateFormat;
24 import android.icu.util.Calendar;
25 import android.icu.util.TimeZone;
26 
27 import androidx.annotation.VisibleForTesting;
28 
29 import com.android.settings.R;
30 import com.android.settings.core.BasePreferenceController;
31 
32 import java.time.Instant;
33 import java.time.zone.ZoneOffsetTransition;
34 import java.time.zone.ZoneRules;
35 import java.util.Date;
36 import java.util.Locale;
37 
38 public class TimeZoneInfoPreferenceController extends BasePreferenceController {
39 
40     @VisibleForTesting
41     Date mDate;
42     private TimeZoneInfo mTimeZoneInfo;
43     private final DateFormat mDateFormat;
44 
TimeZoneInfoPreferenceController(Context context, String key)45     public TimeZoneInfoPreferenceController(Context context, String key) {
46         super(context, key);
47         mDateFormat = DateFormat.getDateInstance(SimpleDateFormat.LONG);
48         mDateFormat.setContext(DisplayContext.CAPITALIZATION_NONE);
49         mDate = new Date();
50     }
51 
52     @Override
getAvailabilityStatus()53     public int getAvailabilityStatus() {
54         return mTimeZoneInfo != null ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE;
55     }
56 
57     @Override
getSummary()58     public CharSequence getSummary() {
59         return mTimeZoneInfo == null ? "" : formatInfo(mTimeZoneInfo);
60     }
61 
setTimeZoneInfo(TimeZoneInfo timeZoneInfo)62     public void setTimeZoneInfo(TimeZoneInfo timeZoneInfo) {
63         mTimeZoneInfo = timeZoneInfo;
64     }
65 
formatOffsetAndName(TimeZoneInfo item)66     private CharSequence formatOffsetAndName(TimeZoneInfo item) {
67         String name = item.getGenericName();
68         if (name == null) {
69             if (item.getTimeZone().inDaylightTime(mDate)) {
70                 name = item.getDaylightName();
71             } else {
72                 name = item.getStandardName();
73             }
74         }
75         if (name == null) {
76             return item.getGmtOffset().toString();
77         } else {
78             return SpannableUtil.getResourcesText(mContext.getResources(),
79                     R.string.zone_info_offset_and_name, item.getGmtOffset(),
80                     name);
81         }
82     }
83 
formatInfo(TimeZoneInfo item)84     private CharSequence formatInfo(TimeZoneInfo item) {
85         final CharSequence offsetAndName = formatOffsetAndName(item);
86         final TimeZone timeZone = item.getTimeZone();
87         ZoneOffsetTransition nextDstTransition = null;
88         if (timeZone.observesDaylightTime()) {
89             nextDstTransition = findNextDstTransition(item);
90         }
91         if (nextDstTransition == null || !timeZone.observesDaylightTime()) {
92             return SpannableUtil.getResourcesText(mContext.getResources(),
93                 R.string.zone_info_footer_no_dst, offsetAndName);
94         }
95 
96         final boolean toDst = getDSTSavings(timeZone, nextDstTransition.getInstant()) != 0;
97         String timeType = toDst ? item.getDaylightName() : item.getStandardName();
98         if (timeType == null) {
99             // Fall back to generic "summer time" and "standard time" if the time zone has no
100             // specific names.
101             timeType = toDst ?
102                     mContext.getString(R.string.zone_time_type_dst) :
103                     mContext.getString(R.string.zone_time_type_standard);
104 
105         }
106         final Calendar transitionTime = Calendar.getInstance(timeZone);
107         transitionTime.setTimeInMillis(nextDstTransition.getInstant().toEpochMilli());
108         final String date = mDateFormat.format(transitionTime);
109         return createFooterString(offsetAndName, timeType, date);
110     }
111 
112     /**
113      * @param offsetAndName {@Spannable} styled text information should be preserved. See
114      * {@link #formatInfo} and {@link com.android.settingslib.datetime.ZoneGetter#getGmtOffsetText}.
115      *
116      */
createFooterString(CharSequence offsetAndName, String timeType, String date)117     private CharSequence createFooterString(CharSequence offsetAndName, String timeType,
118             String date) {
119         Resources res = mContext.getResources();
120         Locale locale = res.getConfiguration().getLocales().get(0);
121         CharSequence secondSentence = SpannableUtil.titleCaseSentences(locale,
122                 SpannableUtil.getResourcesText(res, R.string.zone_info_footer_second_sentence,
123                 timeType, date));
124 
125         return SpannableUtil.titleCaseSentences(locale, SpannableUtil.getResourcesText(res,
126             R.string.zone_info_footer_first_sentence, offsetAndName, secondSentence));
127     }
128 
findNextDstTransition(TimeZoneInfo timeZoneInfo)129     private ZoneOffsetTransition findNextDstTransition(TimeZoneInfo timeZoneInfo) {
130         TimeZone timeZone = timeZoneInfo.getTimeZone();
131         ZoneRules zoneRules = timeZoneInfo.getJavaTimeZone().toZoneId().getRules();
132 
133         Instant from = mDate.toInstant();
134 
135         ZoneOffsetTransition transition;
136         while (true) { // Find next transition with different DST offsets
137             transition = zoneRules.nextTransition(from);
138             if (transition == null) {
139                 break;
140             }
141             Instant to = transition.getInstant();
142             if (getDSTSavings(timeZone, from) != getDSTSavings(timeZone, to)) {
143                 break;
144             }
145             from = to;
146         }
147 
148         return transition;
149     }
150 
getDSTSavings(TimeZone timeZone, Instant instant)151     private static int getDSTSavings(TimeZone timeZone, Instant instant) {
152         int[] offsets = new int[2];
153         timeZone.getOffset(instant.toEpochMilli(), false /* local time */, offsets);
154         return offsets[1];
155     }
156 }
157