1 /* 2 * Copyright (C) 2015 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 package android.hardware.radio; 17 18 import android.annotation.NonNull; 19 import android.annotation.SystemApi; 20 import android.content.ContentResolver; 21 import android.graphics.Bitmap; 22 import android.graphics.BitmapFactory; 23 import android.net.Uri; 24 import android.os.Bundle; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.text.TextUtils; 28 import android.util.ArrayMap; 29 import android.util.Log; 30 import android.util.SparseArray; 31 32 import java.util.ArrayList; 33 import java.util.Set; 34 35 /** 36 * Contains meta data about a radio program such as station name, song title, artist etc... 37 * @hide 38 */ 39 @SystemApi 40 public final class RadioMetadata implements Parcelable { 41 private static final String TAG = "RadioMetadata"; 42 43 /** 44 * The RDS Program Information. 45 */ 46 public static final String METADATA_KEY_RDS_PI = "android.hardware.radio.metadata.RDS_PI"; 47 48 /** 49 * The RDS Program Service. 50 */ 51 public static final String METADATA_KEY_RDS_PS = "android.hardware.radio.metadata.RDS_PS"; 52 53 /** 54 * The RDS PTY. 55 */ 56 public static final String METADATA_KEY_RDS_PTY = "android.hardware.radio.metadata.RDS_PTY"; 57 58 /** 59 * The RBDS PTY. 60 */ 61 public static final String METADATA_KEY_RBDS_PTY = "android.hardware.radio.metadata.RBDS_PTY"; 62 63 /** 64 * The RBDS Radio Text. 65 */ 66 public static final String METADATA_KEY_RDS_RT = "android.hardware.radio.metadata.RDS_RT"; 67 68 /** 69 * The song title. 70 */ 71 public static final String METADATA_KEY_TITLE = "android.hardware.radio.metadata.TITLE"; 72 73 /** 74 * The artist name. 75 */ 76 public static final String METADATA_KEY_ARTIST = "android.hardware.radio.metadata.ARTIST"; 77 78 /** 79 * The album name. 80 */ 81 public static final String METADATA_KEY_ALBUM = "android.hardware.radio.metadata.ALBUM"; 82 83 /** 84 * The music genre. 85 */ 86 public static final String METADATA_KEY_GENRE = "android.hardware.radio.metadata.GENRE"; 87 88 /** 89 * The radio station icon {@link Bitmap}. 90 */ 91 public static final String METADATA_KEY_ICON = "android.hardware.radio.metadata.ICON"; 92 93 /** 94 * The artwork for the song/album {@link Bitmap}. 95 */ 96 public static final String METADATA_KEY_ART = "android.hardware.radio.metadata.ART"; 97 98 /** 99 * The clock. 100 */ 101 public static final String METADATA_KEY_CLOCK = "android.hardware.radio.metadata.CLOCK"; 102 103 104 private static final int METADATA_TYPE_INVALID = -1; 105 private static final int METADATA_TYPE_INT = 0; 106 private static final int METADATA_TYPE_TEXT = 1; 107 private static final int METADATA_TYPE_BITMAP = 2; 108 private static final int METADATA_TYPE_CLOCK = 3; 109 110 private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE; 111 112 static { 113 METADATA_KEYS_TYPE = new ArrayMap<String, Integer>(); METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PI, METADATA_TYPE_TEXT)114 METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PI, METADATA_TYPE_TEXT); METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PS, METADATA_TYPE_TEXT)115 METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PS, METADATA_TYPE_TEXT); METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PTY, METADATA_TYPE_INT)116 METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PTY, METADATA_TYPE_INT); METADATA_KEYS_TYPE.put(METADATA_KEY_RBDS_PTY, METADATA_TYPE_INT)117 METADATA_KEYS_TYPE.put(METADATA_KEY_RBDS_PTY, METADATA_TYPE_INT); METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_RT, METADATA_TYPE_TEXT)118 METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_RT, METADATA_TYPE_TEXT); METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT)119 METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT); METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT)120 METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT); METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT)121 METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT); METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT)122 METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT); METADATA_KEYS_TYPE.put(METADATA_KEY_ICON, METADATA_TYPE_BITMAP)123 METADATA_KEYS_TYPE.put(METADATA_KEY_ICON, METADATA_TYPE_BITMAP); METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP)124 METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); METADATA_KEYS_TYPE.put(METADATA_KEY_CLOCK, METADATA_TYPE_CLOCK)125 METADATA_KEYS_TYPE.put(METADATA_KEY_CLOCK, METADATA_TYPE_CLOCK); 126 } 127 128 // keep in sync with: system/media/radio/include/system/radio_metadata.h 129 private static final int NATIVE_KEY_INVALID = -1; 130 private static final int NATIVE_KEY_RDS_PI = 0; 131 private static final int NATIVE_KEY_RDS_PS = 1; 132 private static final int NATIVE_KEY_RDS_PTY = 2; 133 private static final int NATIVE_KEY_RBDS_PTY = 3; 134 private static final int NATIVE_KEY_RDS_RT = 4; 135 private static final int NATIVE_KEY_TITLE = 5; 136 private static final int NATIVE_KEY_ARTIST = 6; 137 private static final int NATIVE_KEY_ALBUM = 7; 138 private static final int NATIVE_KEY_GENRE = 8; 139 private static final int NATIVE_KEY_ICON = 9; 140 private static final int NATIVE_KEY_ART = 10; 141 private static final int NATIVE_KEY_CLOCK = 11; 142 143 private static final SparseArray<String> NATIVE_KEY_MAPPING; 144 145 static { 146 NATIVE_KEY_MAPPING = new SparseArray<String>(); NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PI, METADATA_KEY_RDS_PI)147 NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PI, METADATA_KEY_RDS_PI); NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PS, METADATA_KEY_RDS_PS)148 NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PS, METADATA_KEY_RDS_PS); NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PTY, METADATA_KEY_RDS_PTY)149 NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PTY, METADATA_KEY_RDS_PTY); NATIVE_KEY_MAPPING.put(NATIVE_KEY_RBDS_PTY, METADATA_KEY_RBDS_PTY)150 NATIVE_KEY_MAPPING.put(NATIVE_KEY_RBDS_PTY, METADATA_KEY_RBDS_PTY); NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_RT, METADATA_KEY_RDS_RT)151 NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_RT, METADATA_KEY_RDS_RT); NATIVE_KEY_MAPPING.put(NATIVE_KEY_TITLE, METADATA_KEY_TITLE)152 NATIVE_KEY_MAPPING.put(NATIVE_KEY_TITLE, METADATA_KEY_TITLE); NATIVE_KEY_MAPPING.put(NATIVE_KEY_ARTIST, METADATA_KEY_ARTIST)153 NATIVE_KEY_MAPPING.put(NATIVE_KEY_ARTIST, METADATA_KEY_ARTIST); NATIVE_KEY_MAPPING.put(NATIVE_KEY_ALBUM, METADATA_KEY_ALBUM)154 NATIVE_KEY_MAPPING.put(NATIVE_KEY_ALBUM, METADATA_KEY_ALBUM); NATIVE_KEY_MAPPING.put(NATIVE_KEY_GENRE, METADATA_KEY_GENRE)155 NATIVE_KEY_MAPPING.put(NATIVE_KEY_GENRE, METADATA_KEY_GENRE); NATIVE_KEY_MAPPING.put(NATIVE_KEY_ICON, METADATA_KEY_ICON)156 NATIVE_KEY_MAPPING.put(NATIVE_KEY_ICON, METADATA_KEY_ICON); NATIVE_KEY_MAPPING.put(NATIVE_KEY_ART, METADATA_KEY_ART)157 NATIVE_KEY_MAPPING.put(NATIVE_KEY_ART, METADATA_KEY_ART); NATIVE_KEY_MAPPING.put(NATIVE_KEY_CLOCK, METADATA_KEY_CLOCK)158 NATIVE_KEY_MAPPING.put(NATIVE_KEY_CLOCK, METADATA_KEY_CLOCK); 159 } 160 161 /** 162 * Provides a Clock that can be used to describe time as provided by the Radio. 163 * 164 * The clock is defined by the seconds since epoch at the UTC + 0 timezone 165 * and timezone offset from UTC + 0 represented in number of minutes. 166 * 167 * @hide 168 */ 169 @SystemApi 170 public static final class Clock implements Parcelable { 171 private final long mUtcEpochSeconds; 172 private final int mTimezoneOffsetMinutes; 173 describeContents()174 public int describeContents() { 175 return 0; 176 } 177 writeToParcel(Parcel out, int flags)178 public void writeToParcel(Parcel out, int flags) { 179 out.writeLong(mUtcEpochSeconds); 180 out.writeInt(mTimezoneOffsetMinutes); 181 } 182 183 public static final Parcelable.Creator<Clock> CREATOR 184 = new Parcelable.Creator<Clock>() { 185 public Clock createFromParcel(Parcel in) { 186 return new Clock(in); 187 } 188 189 public Clock[] newArray(int size) { 190 return new Clock[size]; 191 } 192 }; 193 Clock(long utcEpochSeconds, int timezoneOffsetMinutes)194 public Clock(long utcEpochSeconds, int timezoneOffsetMinutes) { 195 mUtcEpochSeconds = utcEpochSeconds; 196 mTimezoneOffsetMinutes = timezoneOffsetMinutes; 197 } 198 Clock(Parcel in)199 private Clock(Parcel in) { 200 mUtcEpochSeconds = in.readLong(); 201 mTimezoneOffsetMinutes = in.readInt(); 202 } 203 getUtcEpochSeconds()204 public long getUtcEpochSeconds() { 205 return mUtcEpochSeconds; 206 } 207 getTimezoneOffsetMinutes()208 public int getTimezoneOffsetMinutes() { 209 return mTimezoneOffsetMinutes; 210 } 211 } 212 213 private final Bundle mBundle; 214 RadioMetadata()215 RadioMetadata() { 216 mBundle = new Bundle(); 217 } 218 RadioMetadata(Bundle bundle)219 private RadioMetadata(Bundle bundle) { 220 mBundle = new Bundle(bundle); 221 } 222 RadioMetadata(Parcel in)223 private RadioMetadata(Parcel in) { 224 mBundle = in.readBundle(); 225 } 226 227 /** 228 * Returns {@code true} if the given key is contained in the meta data 229 * 230 * @param key a String key 231 * @return {@code true} if the key exists in this meta data, {@code false} otherwise 232 */ containsKey(String key)233 public boolean containsKey(String key) { 234 return mBundle.containsKey(key); 235 } 236 237 /** 238 * Returns the text value associated with the given key as a String, or null 239 * if the key is not found in the meta data. 240 * 241 * @param key The key the value is stored under 242 * @return a String value, or null 243 */ getString(String key)244 public String getString(String key) { 245 return mBundle.getString(key); 246 } 247 248 /** 249 * Returns the value associated with the given key, 250 * or 0 if the key is not found in the meta data. 251 * 252 * @param key The key the value is stored under 253 * @return an int value 254 */ getInt(String key)255 public int getInt(String key) { 256 return mBundle.getInt(key, 0); 257 } 258 259 /** 260 * Returns a {@link Bitmap} for the given key or null if the key is not found in the meta data. 261 * 262 * @param key The key the value is stored under 263 * @return a {@link Bitmap} or null 264 */ getBitmap(String key)265 public Bitmap getBitmap(String key) { 266 Bitmap bmp = null; 267 try { 268 bmp = mBundle.getParcelable(key); 269 } catch (Exception e) { 270 // ignore, value was not a bitmap 271 Log.w(TAG, "Failed to retrieve a key as Bitmap.", e); 272 } 273 return bmp; 274 } 275 getClock(String key)276 public Clock getClock(String key) { 277 Clock clock = null; 278 try { 279 clock = mBundle.getParcelable(key); 280 } catch (Exception e) { 281 // ignore, value was not a clock. 282 Log.w(TAG, "Failed to retrieve a key as Clock.", e); 283 } 284 return clock; 285 } 286 287 @Override describeContents()288 public int describeContents() { 289 return 0; 290 } 291 292 @Override writeToParcel(Parcel dest, int flags)293 public void writeToParcel(Parcel dest, int flags) { 294 dest.writeBundle(mBundle); 295 } 296 297 /** 298 * Returns the number of fields in this meta data. 299 * 300 * @return the number of fields in the meta data. 301 */ size()302 public int size() { 303 return mBundle.size(); 304 } 305 306 /** 307 * Returns a Set containing the Strings used as keys in this meta data. 308 * 309 * @return a Set of String keys 310 */ keySet()311 public Set<String> keySet() { 312 return mBundle.keySet(); 313 } 314 315 /** 316 * Helper for getting the String key used by {@link RadioMetadata} from the 317 * corrsponding native integer key. 318 * 319 * @param editorKey The key used by the editor 320 * @return the key used by this class or null if no mapping exists 321 * @hide 322 */ getKeyFromNativeKey(int nativeKey)323 public static String getKeyFromNativeKey(int nativeKey) { 324 return NATIVE_KEY_MAPPING.get(nativeKey, null); 325 } 326 327 public static final Parcelable.Creator<RadioMetadata> CREATOR = 328 new Parcelable.Creator<RadioMetadata>() { 329 @Override 330 public RadioMetadata createFromParcel(Parcel in) { 331 return new RadioMetadata(in); 332 } 333 334 @Override 335 public RadioMetadata[] newArray(int size) { 336 return new RadioMetadata[size]; 337 } 338 }; 339 340 /** 341 * Use to build RadioMetadata objects. 342 */ 343 public static final class Builder { 344 private final Bundle mBundle; 345 346 /** 347 * Create an empty Builder. Any field that should be included in the 348 * {@link RadioMetadata} must be added. 349 */ Builder()350 public Builder() { 351 mBundle = new Bundle(); 352 } 353 354 /** 355 * Create a Builder using a {@link RadioMetadata} instance to set the 356 * initial values. All fields in the source meta data will be included in 357 * the new meta data. Fields can be overwritten by adding the same key. 358 * 359 * @param source 360 */ Builder(RadioMetadata source)361 public Builder(RadioMetadata source) { 362 mBundle = new Bundle(source.mBundle); 363 } 364 365 /** 366 * Create a Builder using a {@link RadioMetadata} instance to set 367 * initial values, but replace bitmaps with a scaled down copy if they 368 * are larger than maxBitmapSize. 369 * 370 * @param source The original meta data to copy. 371 * @param maxBitmapSize The maximum height/width for bitmaps contained 372 * in the meta data. 373 * @hide 374 */ Builder(RadioMetadata source, int maxBitmapSize)375 public Builder(RadioMetadata source, int maxBitmapSize) { 376 this(source); 377 for (String key : mBundle.keySet()) { 378 Object value = mBundle.get(key); 379 if (value != null && value instanceof Bitmap) { 380 Bitmap bmp = (Bitmap) value; 381 if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) { 382 putBitmap(key, scaleBitmap(bmp, maxBitmapSize)); 383 } 384 } 385 } 386 } 387 388 /** 389 * Put a String value into the meta data. Custom keys may be used, but if 390 * the METADATA_KEYs defined in this class are used they may only be one 391 * of the following: 392 * <ul> 393 * <li>{@link #METADATA_KEY_RDS_PI}</li> 394 * <li>{@link #METADATA_KEY_RDS_PS}</li> 395 * <li>{@link #METADATA_KEY_RDS_RT}</li> 396 * <li>{@link #METADATA_KEY_TITLE}</li> 397 * <li>{@link #METADATA_KEY_ARTIST}</li> 398 * <li>{@link #METADATA_KEY_ALBUM}</li> 399 * <li>{@link #METADATA_KEY_GENRE}</li> 400 * </ul> 401 * 402 * @param key The key for referencing this value 403 * @param value The String value to store 404 * @return the same Builder instance 405 */ putString(String key, String value)406 public Builder putString(String key, String value) { 407 if (!METADATA_KEYS_TYPE.containsKey(key) || 408 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { 409 throw new IllegalArgumentException("The " + key 410 + " key cannot be used to put a String"); 411 } 412 mBundle.putString(key, value); 413 return this; 414 } 415 416 /** 417 * Put an int value into the meta data. Custom keys may be used, but if 418 * the METADATA_KEYs defined in this class are used they may only be one 419 * of the following: 420 * <ul> 421 * <li>{@link #METADATA_KEY_RDS_PTY}</li> 422 * <li>{@link #METADATA_KEY_RBDS_PTY}</li> 423 * </ul> 424 * 425 * @param key The key for referencing this value 426 * @param value The int value to store 427 * @return the same Builder instance 428 */ putInt(String key, int value)429 public Builder putInt(String key, int value) { 430 if (!METADATA_KEYS_TYPE.containsKey(key) || 431 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_INT) { 432 throw new IllegalArgumentException("The " + key 433 + " key cannot be used to put a long"); 434 } 435 mBundle.putInt(key, value); 436 return this; 437 } 438 439 /** 440 * Put a {@link Bitmap} into the meta data. Custom keys may be used, but 441 * if the METADATA_KEYs defined in this class are used they may only be 442 * one of the following: 443 * <ul> 444 * <li>{@link #METADATA_KEY_ICON}</li> 445 * <li>{@link #METADATA_KEY_ART}</li> 446 * </ul> 447 * <p> 448 * 449 * @param key The key for referencing this value 450 * @param value The Bitmap to store 451 * @return the same Builder instance 452 */ putBitmap(String key, Bitmap value)453 public Builder putBitmap(String key, Bitmap value) { 454 if (!METADATA_KEYS_TYPE.containsKey(key) || 455 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { 456 throw new IllegalArgumentException("The " + key 457 + " key cannot be used to put a Bitmap"); 458 } 459 mBundle.putParcelable(key, value); 460 return this; 461 } 462 463 /** 464 * Put a {@link RadioMetadata.Clock} into the meta data. Custom keys may be used, but if the 465 * METADATA_KEYs defined in this class are used they may only be one of the following: 466 * <ul> 467 * <li>{@link #MEADATA_KEY_CLOCK}</li> 468 * </ul> 469 * 470 * @param utcSecondsSinceEpoch Number of seconds since epoch for UTC + 0 timezone. 471 * @param timezoneOffsetInMinutes Offset of timezone from UTC + 0 in minutes. 472 * @return the same Builder instance. 473 */ putClock(String key, long utcSecondsSinceEpoch, int timezoneOffsetMinutes)474 public Builder putClock(String key, long utcSecondsSinceEpoch, int timezoneOffsetMinutes) { 475 if (!METADATA_KEYS_TYPE.containsKey(key) || 476 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) { 477 throw new IllegalArgumentException("The " + key 478 + " key cannot be used to put a RadioMetadata.Clock."); 479 } 480 mBundle.putParcelable(key, new Clock(utcSecondsSinceEpoch, timezoneOffsetMinutes)); 481 return this; 482 } 483 484 /** 485 * Creates a {@link RadioMetadata} instance with the specified fields. 486 * 487 * @return a new {@link RadioMetadata} object 488 */ build()489 public RadioMetadata build() { 490 return new RadioMetadata(mBundle); 491 } 492 scaleBitmap(Bitmap bmp, int maxSize)493 private Bitmap scaleBitmap(Bitmap bmp, int maxSize) { 494 float maxSizeF = maxSize; 495 float widthScale = maxSizeF / bmp.getWidth(); 496 float heightScale = maxSizeF / bmp.getHeight(); 497 float scale = Math.min(widthScale, heightScale); 498 int height = (int) (bmp.getHeight() * scale); 499 int width = (int) (bmp.getWidth() * scale); 500 return Bitmap.createScaledBitmap(bmp, width, height, true); 501 } 502 } 503 putIntFromNative(int nativeKey, int value)504 int putIntFromNative(int nativeKey, int value) { 505 String key = getKeyFromNativeKey(nativeKey); 506 if (!METADATA_KEYS_TYPE.containsKey(key) || 507 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_INT) { 508 return -1; 509 } 510 mBundle.putInt(key, value); 511 return 0; 512 } 513 putStringFromNative(int nativeKey, String value)514 int putStringFromNative(int nativeKey, String value) { 515 String key = getKeyFromNativeKey(nativeKey); 516 if (!METADATA_KEYS_TYPE.containsKey(key) || 517 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { 518 return -1; 519 } 520 mBundle.putString(key, value); 521 return 0; 522 } 523 putBitmapFromNative(int nativeKey, byte[] value)524 int putBitmapFromNative(int nativeKey, byte[] value) { 525 String key = getKeyFromNativeKey(nativeKey); 526 if (!METADATA_KEYS_TYPE.containsKey(key) || 527 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { 528 return -1; 529 } 530 Bitmap bmp = null; 531 try { 532 bmp = BitmapFactory.decodeByteArray(value, 0, value.length); 533 if (bmp != null) { 534 mBundle.putParcelable(key, bmp); 535 return 0; 536 } 537 } catch (Exception e) { 538 } 539 return -1; 540 } 541 putClockFromNative(int nativeKey, long utcEpochSeconds, int timezoneOffsetInMinutes)542 int putClockFromNative(int nativeKey, long utcEpochSeconds, int timezoneOffsetInMinutes) { 543 Log.d(TAG, "putClockFromNative()"); 544 String key = getKeyFromNativeKey(nativeKey); 545 if (!METADATA_KEYS_TYPE.containsKey(key) || 546 METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) { 547 return -1; 548 } 549 mBundle.putParcelable(key, new RadioMetadata.Clock( 550 utcEpochSeconds, timezoneOffsetInMinutes)); 551 return 0; 552 } 553 } 554