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