1 /* 2 * Copyright (C) 2016 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.deskclock.data; 18 19 import android.content.Context; 20 import androidx.annotation.VisibleForTesting; 21 import android.util.ArrayMap; 22 23 import com.android.deskclock.R; 24 25 import java.text.DateFormatSymbols; 26 import java.util.Arrays; 27 import java.util.Calendar; 28 import java.util.Collections; 29 import java.util.List; 30 import java.util.Map; 31 32 import static java.util.Calendar.DAY_OF_WEEK; 33 import static java.util.Calendar.FRIDAY; 34 import static java.util.Calendar.MONDAY; 35 import static java.util.Calendar.SATURDAY; 36 import static java.util.Calendar.SUNDAY; 37 import static java.util.Calendar.THURSDAY; 38 import static java.util.Calendar.TUESDAY; 39 import static java.util.Calendar.WEDNESDAY; 40 41 /** 42 * This class is responsible for encoding a weekly repeat cycle in a {@link #getBits bitset}. It 43 * also converts between those bits and the {@link Calendar#DAY_OF_WEEK} values for easier mutation 44 * and querying. 45 */ 46 public final class Weekdays { 47 48 /** 49 * The preferred starting day of the week can differ by locale. This enumerated value is used to 50 * describe the preferred ordering. 51 */ 52 public enum Order { 53 SAT_TO_FRI(SATURDAY, SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY), 54 SUN_TO_SAT(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY), 55 MON_TO_SUN(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY); 56 57 private final List<Integer> mCalendarDays; 58 Order(Integer... calendarDays)59 Order(Integer... calendarDays) { 60 mCalendarDays = Arrays.asList(calendarDays); 61 } 62 getCalendarDays()63 public List<Integer> getCalendarDays() { 64 return mCalendarDays; 65 } 66 } 67 68 /** All valid bits set. */ 69 private static final int ALL_DAYS = 0x7F; 70 71 /** An instance with all weekdays in the weekly repeat cycle. */ 72 public static final Weekdays ALL = Weekdays.fromBits(ALL_DAYS); 73 74 /** An instance with no weekdays in the weekly repeat cycle. */ 75 public static final Weekdays NONE = Weekdays.fromBits(0); 76 77 /** Maps calendar weekdays to the bit masks that represent them in this class. */ 78 private static final Map<Integer, Integer> sCalendarDayToBit; 79 static { 80 final Map<Integer, Integer> map = new ArrayMap<>(7); map.put(MONDAY, 0x01)81 map.put(MONDAY, 0x01); map.put(TUESDAY, 0x02)82 map.put(TUESDAY, 0x02); map.put(WEDNESDAY, 0x04)83 map.put(WEDNESDAY, 0x04); map.put(THURSDAY, 0x08)84 map.put(THURSDAY, 0x08); map.put(FRIDAY, 0x10)85 map.put(FRIDAY, 0x10); map.put(SATURDAY, 0x20)86 map.put(SATURDAY, 0x20); map.put(SUNDAY, 0x40)87 map.put(SUNDAY, 0x40); 88 sCalendarDayToBit = Collections.unmodifiableMap(map); 89 } 90 91 /** An encoded form of a weekly repeat schedule. */ 92 private final int mBits; 93 Weekdays(int bits)94 private Weekdays(int bits) { 95 // Mask off the unused bits. 96 mBits = ALL_DAYS & bits; 97 } 98 99 /** 100 * @param bits {@link #getBits bits} representing the encoded weekly repeat schedule 101 * @return a Weekdays instance representing the same repeat schedule as the {@code bits} 102 */ fromBits(int bits)103 public static Weekdays fromBits(int bits) { 104 return new Weekdays(bits); 105 } 106 107 /** 108 * @param calendarDays an array containing any or all of the following values 109 * <ul> 110 * <li>{@link Calendar#SUNDAY}</li> 111 * <li>{@link Calendar#MONDAY}</li> 112 * <li>{@link Calendar#TUESDAY}</li> 113 * <li>{@link Calendar#WEDNESDAY}</li> 114 * <li>{@link Calendar#THURSDAY}</li> 115 * <li>{@link Calendar#FRIDAY}</li> 116 * <li>{@link Calendar#SATURDAY}</li> 117 * </ul> 118 * @return a Weekdays instance representing the given {@code calendarDays} 119 */ fromCalendarDays(int... calendarDays)120 public static Weekdays fromCalendarDays(int... calendarDays) { 121 int bits = 0; 122 for (int calendarDay : calendarDays) { 123 final Integer bit = sCalendarDayToBit.get(calendarDay); 124 if (bit != null) { 125 bits = bits | bit; 126 } 127 } 128 return new Weekdays(bits); 129 } 130 131 /** 132 * @param calendarDay any of the following values 133 * <ul> 134 * <li>{@link Calendar#SUNDAY}</li> 135 * <li>{@link Calendar#MONDAY}</li> 136 * <li>{@link Calendar#TUESDAY}</li> 137 * <li>{@link Calendar#WEDNESDAY}</li> 138 * <li>{@link Calendar#THURSDAY}</li> 139 * <li>{@link Calendar#FRIDAY}</li> 140 * <li>{@link Calendar#SATURDAY}</li> 141 * </ul> 142 * @param on {@code true} if the {@code calendarDay} is on; {@code false} otherwise 143 * @return a WeekDays instance with the {@code calendarDay} mutated 144 */ setBit(int calendarDay, boolean on)145 public Weekdays setBit(int calendarDay, boolean on) { 146 final Integer bit = sCalendarDayToBit.get(calendarDay); 147 if (bit == null) { 148 return this; 149 } 150 return new Weekdays(on ? (mBits | bit) : (mBits & ~bit)); 151 } 152 153 /** 154 * @param calendarDay any of the following values 155 * <ul> 156 * <li>{@link Calendar#SUNDAY}</li> 157 * <li>{@link Calendar#MONDAY}</li> 158 * <li>{@link Calendar#TUESDAY}</li> 159 * <li>{@link Calendar#WEDNESDAY}</li> 160 * <li>{@link Calendar#THURSDAY}</li> 161 * <li>{@link Calendar#FRIDAY}</li> 162 * <li>{@link Calendar#SATURDAY}</li> 163 * </ul> 164 * @return {@code true} if the given {@code calendarDay} 165 */ isBitOn(int calendarDay)166 public boolean isBitOn(int calendarDay) { 167 final Integer bit = sCalendarDayToBit.get(calendarDay); 168 if (bit == null) { 169 throw new IllegalArgumentException(calendarDay + " is not a valid weekday"); 170 } 171 return (mBits & bit) > 0; 172 } 173 174 /** 175 * @return the weekly repeat schedule encoded as an integer 176 */ getBits()177 public int getBits() { return mBits; } 178 179 /** 180 * @return {@code true} iff at least one weekday is enabled in the repeat schedule 181 */ isRepeating()182 public boolean isRepeating() { return mBits != 0; } 183 184 /** 185 * Note: only the day-of-week is read from the {@code time}. The time fields 186 * are not considered in this computation. 187 * 188 * @param time a timestamp relative to which the answer is given 189 * @return the number of days between the given {@code time} and the previous enabled weekday 190 * which is always between 1 and 7 inclusive; {@code -1} if no weekdays are enabled 191 */ getDistanceToPreviousDay(Calendar time)192 public int getDistanceToPreviousDay(Calendar time) { 193 int calendarDay = time.get(DAY_OF_WEEK); 194 for (int count = 1; count <= 7; count++) { 195 calendarDay--; 196 if (calendarDay < Calendar.SUNDAY) { 197 calendarDay = Calendar.SATURDAY; 198 } 199 if (isBitOn(calendarDay)) { 200 return count; 201 } 202 } 203 204 return -1; 205 } 206 207 /** 208 * Note: only the day-of-week is read from the {@code time}. The time fields 209 * are not considered in this computation. 210 * 211 * @param time a timestamp relative to which the answer is given 212 * @return the number of days between the given {@code time} and the next enabled weekday which 213 * is always between 0 and 6 inclusive; {@code -1} if no weekdays are enabled 214 */ getDistanceToNextDay(Calendar time)215 public int getDistanceToNextDay(Calendar time) { 216 int calendarDay = time.get(DAY_OF_WEEK); 217 for (int count = 0; count < 7; count++) { 218 if (isBitOn(calendarDay)) { 219 return count; 220 } 221 222 calendarDay++; 223 if (calendarDay > Calendar.SATURDAY) { 224 calendarDay = Calendar.SUNDAY; 225 } 226 } 227 228 return -1; 229 } 230 231 @Override equals(Object o)232 public boolean equals(Object o) { 233 if (this == o) return true; 234 if (o == null || getClass() != o.getClass()) return false; 235 236 final Weekdays weekdays = (Weekdays) o; 237 return mBits == weekdays.mBits; 238 } 239 240 @Override hashCode()241 public int hashCode() { 242 return mBits; 243 } 244 245 @Override toString()246 public String toString() { 247 final StringBuilder builder = new StringBuilder(19); 248 builder.append("["); 249 if (isBitOn(MONDAY)) { 250 builder.append(builder.length() > 1 ? " M" : "M"); 251 } 252 if (isBitOn(TUESDAY)) { 253 builder.append(builder.length() > 1 ? " T" : "T"); 254 } 255 if (isBitOn(WEDNESDAY)) { 256 builder.append(builder.length() > 1 ? " W" : "W"); 257 } 258 if (isBitOn(THURSDAY)) { 259 builder.append(builder.length() > 1 ? " Th" : "Th"); 260 } 261 if (isBitOn(FRIDAY)) { 262 builder.append(builder.length() > 1 ? " F" : "F"); 263 } 264 if (isBitOn(SATURDAY)) { 265 builder.append(builder.length() > 1 ? " Sa" : "Sa"); 266 } 267 if (isBitOn(SUNDAY)) { 268 builder.append(builder.length() > 1 ? " Su" : "Su"); 269 } 270 builder.append("]"); 271 return builder.toString(); 272 } 273 274 /** 275 * @param context for accessing resources 276 * @param order the order in which to present the weekdays 277 * @return the enabled weekdays in the given {@code order} 278 */ toString(Context context, Order order)279 public String toString(Context context, Order order) { 280 return toString(context, order, false /* forceLongNames */); 281 } 282 283 /** 284 * @param context for accessing resources 285 * @param order the order in which to present the weekdays 286 * @return the enabled weekdays in the given {@code order} in a manner that 287 * is most appropriate for talk-back 288 */ toAccessibilityString(Context context, Order order)289 public String toAccessibilityString(Context context, Order order) { 290 return toString(context, order, true /* forceLongNames */); 291 } 292 293 @VisibleForTesting getCount()294 int getCount() { 295 int count = 0; 296 for (int calendarDay = SUNDAY; calendarDay <= SATURDAY; calendarDay++) { 297 if (isBitOn(calendarDay)) { 298 count++; 299 } 300 } 301 return count; 302 } 303 304 /** 305 * @param context for accessing resources 306 * @param order the order in which to present the weekdays 307 * @param forceLongNames if {@code true} the un-abbreviated weekdays are used 308 * @return the enabled weekdays in the given {@code order} 309 */ toString(Context context, Order order, boolean forceLongNames)310 private String toString(Context context, Order order, boolean forceLongNames) { 311 if (!isRepeating()) { 312 return ""; 313 } 314 315 if (mBits == ALL_DAYS) { 316 return context.getString(R.string.every_day); 317 } 318 319 final boolean longNames = forceLongNames || getCount() <= 1; 320 final DateFormatSymbols dfs = new DateFormatSymbols(); 321 final String[] weekdays = longNames ? dfs.getWeekdays() : dfs.getShortWeekdays(); 322 323 final String separator = context.getString(R.string.day_concat); 324 325 final StringBuilder builder = new StringBuilder(40); 326 for (int calendarDay : order.getCalendarDays()) { 327 if (isBitOn(calendarDay)) { 328 if (builder.length() > 0) { 329 builder.append(separator); 330 } 331 builder.append(weekdays[calendarDay]); 332 } 333 } 334 return builder.toString(); 335 } 336 }