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 package com.android.launcher3.icons.cache; 17 18 import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon; 19 import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON; 20 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 21 22 import android.content.ComponentName; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.database.sqlite.SQLiteDatabase; 33 import android.database.sqlite.SQLiteException; 34 import android.graphics.Bitmap; 35 import android.graphics.BitmapFactory; 36 import android.graphics.drawable.Drawable; 37 import android.os.Build; 38 import android.os.Handler; 39 import android.os.LocaleList; 40 import android.os.Looper; 41 import android.os.Process; 42 import android.os.UserHandle; 43 import android.text.TextUtils; 44 import android.util.Log; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 49 import com.android.launcher3.icons.BaseIconFactory; 50 import com.android.launcher3.icons.BitmapInfo; 51 import com.android.launcher3.icons.BitmapRenderer; 52 import com.android.launcher3.icons.GraphicsUtils; 53 import com.android.launcher3.util.ComponentKey; 54 import com.android.launcher3.util.SQLiteCacheHelper; 55 56 import java.util.AbstractMap; 57 import java.util.Collections; 58 import java.util.HashMap; 59 import java.util.HashSet; 60 import java.util.Map; 61 import java.util.Set; 62 import java.util.function.Supplier; 63 64 public abstract class BaseIconCache { 65 66 private static final String TAG = "BaseIconCache"; 67 private static final boolean DEBUG = false; 68 69 private static final int INITIAL_ICON_CACHE_CAPACITY = 50; 70 71 // Empty class name is used for storing package default entry. 72 public static final String EMPTY_CLASS_NAME = "."; 73 74 public static class CacheEntry extends BitmapInfo { 75 public CharSequence title = ""; 76 public CharSequence contentDescription = ""; 77 } 78 79 private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>(); 80 81 protected final Context mContext; 82 protected final PackageManager mPackageManager; 83 84 private final Map<ComponentKey, CacheEntry> mCache; 85 protected final Handler mWorkerHandler; 86 87 protected int mIconDpi; 88 protected IconDB mIconDb; 89 protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList(); 90 protected String mSystemState = ""; 91 92 private final String mDbFileName; 93 private final BitmapFactory.Options mDecodeOptions; 94 private final Looper mBgLooper; 95 BaseIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi, int iconPixelSize, boolean inMemoryCache)96 public BaseIconCache(Context context, String dbFileName, Looper bgLooper, 97 int iconDpi, int iconPixelSize, boolean inMemoryCache) { 98 mContext = context; 99 mDbFileName = dbFileName; 100 mPackageManager = context.getPackageManager(); 101 mBgLooper = bgLooper; 102 mWorkerHandler = new Handler(mBgLooper); 103 104 if (inMemoryCache) { 105 mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY); 106 } else { 107 // Use a dummy cache 108 mCache = new AbstractMap<ComponentKey, CacheEntry>() { 109 @Override 110 public Set<Entry<ComponentKey, CacheEntry>> entrySet() { 111 return Collections.emptySet(); 112 } 113 114 @Override 115 public CacheEntry put(ComponentKey key, CacheEntry value) { 116 return value; 117 } 118 }; 119 } 120 121 if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 122 mDecodeOptions = new BitmapFactory.Options(); 123 mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE; 124 } else { 125 mDecodeOptions = null; 126 } 127 128 updateSystemState(); 129 mIconDpi = iconDpi; 130 mIconDb = new IconDB(context, dbFileName, iconPixelSize); 131 } 132 133 /** 134 * Returns the persistable serial number for {@param user}. Subclass should implement proper 135 * caching strategy to avoid making binder call every time. 136 */ getSerialNumberForUser(UserHandle user)137 protected abstract long getSerialNumberForUser(UserHandle user); 138 139 /** 140 * Return true if the given app is an instant app and should be badged appropriately. 141 */ isInstantApp(ApplicationInfo info)142 protected abstract boolean isInstantApp(ApplicationInfo info); 143 144 /** 145 * Opens and returns an icon factory. The factory is recycled by the caller. 146 */ getIconFactory()147 protected abstract BaseIconFactory getIconFactory(); 148 updateIconParams(int iconDpi, int iconPixelSize)149 public void updateIconParams(int iconDpi, int iconPixelSize) { 150 mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize)); 151 } 152 updateIconParamsBg(int iconDpi, int iconPixelSize)153 private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) { 154 mIconDpi = iconDpi; 155 mDefaultIcons.clear(); 156 mIconDb.clear(); 157 mIconDb.close(); 158 mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize); 159 mCache.clear(); 160 } 161 getFullResIcon(Resources resources, int iconId)162 private Drawable getFullResIcon(Resources resources, int iconId) { 163 if (resources != null && iconId != 0) { 164 try { 165 return resources.getDrawableForDensity(iconId, mIconDpi); 166 } catch (Resources.NotFoundException e) { } 167 } 168 return getFullResDefaultActivityIcon(mIconDpi); 169 } 170 getFullResIcon(String packageName, int iconId)171 public Drawable getFullResIcon(String packageName, int iconId) { 172 try { 173 return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId); 174 } catch (PackageManager.NameNotFoundException e) { } 175 return getFullResDefaultActivityIcon(mIconDpi); 176 } 177 getFullResIcon(ActivityInfo info)178 public Drawable getFullResIcon(ActivityInfo info) { 179 try { 180 return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo), 181 info.getIconResource()); 182 } catch (PackageManager.NameNotFoundException e) { } 183 return getFullResDefaultActivityIcon(mIconDpi); 184 } 185 makeDefaultIcon(UserHandle user)186 private BitmapInfo makeDefaultIcon(UserHandle user) { 187 try (BaseIconFactory li = getIconFactory()) { 188 return li.makeDefaultIcon(user); 189 } 190 } 191 192 /** 193 * Remove any records for the supplied ComponentName. 194 */ remove(ComponentName componentName, UserHandle user)195 public synchronized void remove(ComponentName componentName, UserHandle user) { 196 mCache.remove(new ComponentKey(componentName, user)); 197 } 198 199 /** 200 * Remove any records for the supplied package name from memory. 201 */ removeFromMemCacheLocked(String packageName, UserHandle user)202 private void removeFromMemCacheLocked(String packageName, UserHandle user) { 203 HashSet<ComponentKey> forDeletion = new HashSet<>(); 204 for (ComponentKey key: mCache.keySet()) { 205 if (key.componentName.getPackageName().equals(packageName) 206 && key.user.equals(user)) { 207 forDeletion.add(key); 208 } 209 } 210 for (ComponentKey condemned: forDeletion) { 211 mCache.remove(condemned); 212 } 213 } 214 215 /** 216 * Removes the entries related to the given package in memory and persistent DB. 217 */ removeIconsForPkg(String packageName, UserHandle user)218 public synchronized void removeIconsForPkg(String packageName, UserHandle user) { 219 removeFromMemCacheLocked(packageName, user); 220 long userSerial = getSerialNumberForUser(user); 221 mIconDb.delete( 222 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", 223 new String[]{packageName + "/%", Long.toString(userSerial)}); 224 } 225 getUpdateHandler()226 public IconCacheUpdateHandler getUpdateHandler() { 227 updateSystemState(); 228 return new IconCacheUpdateHandler(this); 229 } 230 231 /** 232 * Refreshes the system state definition used to check the validity of the cache. It 233 * incorporates all the properties that can affect the cache like the list of enabled locale 234 * and system-version. 235 */ updateSystemState()236 private void updateSystemState() { 237 mLocaleList = mContext.getResources().getConfiguration().getLocales(); 238 mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT; 239 } 240 getIconSystemState(String packageName)241 protected String getIconSystemState(String packageName) { 242 return mSystemState; 243 } 244 245 /** 246 * Adds an entry into the DB and the in-memory cache. 247 * @param replaceExisting if true, it will recreate the bitmap even if it already exists in 248 * the memory. This is useful then the previous bitmap was created using 249 * old data. 250 * package private 251 */ addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic, PackageInfo info, long userSerial, boolean replaceExisting)252 protected synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic, 253 PackageInfo info, long userSerial, boolean replaceExisting) { 254 UserHandle user = cachingLogic.getUser(object); 255 ComponentName componentName = cachingLogic.getComponent(object); 256 257 final ComponentKey key = new ComponentKey(componentName, user); 258 CacheEntry entry = null; 259 if (!replaceExisting) { 260 entry = mCache.get(key); 261 // We can't reuse the entry if the high-res icon is not present. 262 if (entry == null || entry.icon == null || entry.isLowRes()) { 263 entry = null; 264 } 265 } 266 if (entry == null) { 267 entry = new CacheEntry(); 268 cachingLogic.loadIcon(mContext, object, entry); 269 } 270 // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded 271 // (e.g. fallback icon, default icon). So we drop here since there's no point in caching 272 // an empty entry. 273 if (entry.icon == null) return; 274 entry.title = cachingLogic.getLabel(object); 275 entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); 276 if (cachingLogic.addToMemCache()) mCache.put(key, entry); 277 278 ContentValues values = newContentValues(entry, entry.title.toString(), 279 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList)); 280 addIconToDB(values, componentName, info, userSerial); 281 } 282 283 /** 284 * Updates {@param values} to contain versioning information and adds it to the DB. 285 * @param values {@link ContentValues} containing icon & title 286 */ addIconToDB(ContentValues values, ComponentName key, PackageInfo info, long userSerial)287 private void addIconToDB(ContentValues values, ComponentName key, 288 PackageInfo info, long userSerial) { 289 values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); 290 values.put(IconDB.COLUMN_USER, userSerial); 291 values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); 292 values.put(IconDB.COLUMN_VERSION, info.versionCode); 293 mIconDb.insertOrReplace(values); 294 } 295 getDefaultIcon(UserHandle user)296 public synchronized BitmapInfo getDefaultIcon(UserHandle user) { 297 if (!mDefaultIcons.containsKey(user)) { 298 mDefaultIcons.put(user, makeDefaultIcon(user)); 299 } 300 return mDefaultIcons.get(user); 301 } 302 isDefaultIcon(Bitmap icon, UserHandle user)303 public boolean isDefaultIcon(Bitmap icon, UserHandle user) { 304 return getDefaultIcon(user).icon == icon; 305 } 306 307 /** 308 * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. 309 * This method is not thread safe, it must be called from a synchronized method. 310 */ cacheLocked( @onNull ComponentName componentName, @NonNull UserHandle user, @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, boolean usePackageIcon, boolean useLowResIcon)311 protected <T> CacheEntry cacheLocked( 312 @NonNull ComponentName componentName, @NonNull UserHandle user, 313 @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, 314 boolean usePackageIcon, boolean useLowResIcon) { 315 assertWorkerThread(); 316 ComponentKey cacheKey = new ComponentKey(componentName, user); 317 CacheEntry entry = mCache.get(cacheKey); 318 if (entry == null || (entry.isLowRes() && !useLowResIcon)) { 319 entry = new CacheEntry(); 320 if (cachingLogic.addToMemCache()) { 321 mCache.put(cacheKey, entry); 322 } 323 324 // Check the DB first. 325 T object = null; 326 boolean providerFetchedOnce = false; 327 328 if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { 329 object = infoProvider.get(); 330 providerFetchedOnce = true; 331 332 if (object != null) { 333 cachingLogic.loadIcon(mContext, object, entry); 334 } else { 335 if (usePackageIcon) { 336 CacheEntry packageEntry = getEntryForPackageLocked( 337 componentName.getPackageName(), user, false); 338 if (packageEntry != null) { 339 if (DEBUG) Log.d(TAG, "using package default icon for " + 340 componentName.toShortString()); 341 packageEntry.applyTo(entry); 342 entry.title = packageEntry.title; 343 entry.contentDescription = packageEntry.contentDescription; 344 } 345 } 346 if (entry.icon == null) { 347 if (DEBUG) Log.d(TAG, "using default icon for " + 348 componentName.toShortString()); 349 getDefaultIcon(user).applyTo(entry); 350 } 351 } 352 } 353 354 if (TextUtils.isEmpty(entry.title)) { 355 if (object == null && !providerFetchedOnce) { 356 object = infoProvider.get(); 357 providerFetchedOnce = true; 358 } 359 if (object != null) { 360 entry.title = cachingLogic.getLabel(object); 361 entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); 362 } 363 } 364 } 365 return entry; 366 } 367 clear()368 public synchronized void clear() { 369 assertWorkerThread(); 370 mIconDb.clear(); 371 } 372 373 /** 374 * Adds a default package entry in the cache. This entry is not persisted and will be removed 375 * when the cache is flushed. 376 */ cachePackageInstallInfo(String packageName, UserHandle user, Bitmap icon, CharSequence title)377 public synchronized void cachePackageInstallInfo(String packageName, UserHandle user, 378 Bitmap icon, CharSequence title) { 379 removeFromMemCacheLocked(packageName, user); 380 381 ComponentKey cacheKey = getPackageKey(packageName, user); 382 CacheEntry entry = mCache.get(cacheKey); 383 384 // For icon caching, do not go through DB. Just update the in-memory entry. 385 if (entry == null) { 386 entry = new CacheEntry(); 387 } 388 if (!TextUtils.isEmpty(title)) { 389 entry.title = title; 390 } 391 if (icon != null) { 392 BaseIconFactory li = getIconFactory(); 393 li.createIconBitmap(icon).applyTo(entry); 394 li.close(); 395 } 396 if (!TextUtils.isEmpty(title) && entry.icon != null) { 397 mCache.put(cacheKey, entry); 398 } 399 } 400 getPackageKey(String packageName, UserHandle user)401 private static ComponentKey getPackageKey(String packageName, UserHandle user) { 402 ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); 403 return new ComponentKey(cn, user); 404 } 405 406 /** 407 * Gets an entry for the package, which can be used as a fallback entry for various components. 408 * This method is not thread safe, it must be called from a synchronized method. 409 */ getEntryForPackageLocked(String packageName, UserHandle user, boolean useLowResIcon)410 protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user, 411 boolean useLowResIcon) { 412 assertWorkerThread(); 413 ComponentKey cacheKey = getPackageKey(packageName, user); 414 CacheEntry entry = mCache.get(cacheKey); 415 416 if (entry == null || (entry.isLowRes() && !useLowResIcon)) { 417 entry = new CacheEntry(); 418 boolean entryUpdated = true; 419 420 // Check the DB first. 421 if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { 422 try { 423 int flags = Process.myUserHandle().equals(user) ? 0 : 424 PackageManager.GET_UNINSTALLED_PACKAGES; 425 PackageInfo info = mPackageManager.getPackageInfo(packageName, flags); 426 ApplicationInfo appInfo = info.applicationInfo; 427 if (appInfo == null) { 428 throw new NameNotFoundException("ApplicationInfo is null"); 429 } 430 431 BaseIconFactory li = getIconFactory(); 432 // Load the full res icon for the application, but if useLowResIcon is set, then 433 // only keep the low resolution icon instead of the larger full-sized icon 434 BitmapInfo iconInfo = li.createBadgedIconBitmap( 435 appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion, 436 isInstantApp(appInfo)); 437 li.close(); 438 439 entry.title = appInfo.loadLabel(mPackageManager); 440 entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); 441 entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon; 442 entry.color = iconInfo.color; 443 444 // Add the icon in the DB here, since these do not get written during 445 // package updates. 446 ContentValues values = newContentValues( 447 iconInfo, entry.title.toString(), packageName, null); 448 addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user)); 449 450 } catch (NameNotFoundException e) { 451 if (DEBUG) Log.d(TAG, "Application not installed " + packageName); 452 entryUpdated = false; 453 } 454 } 455 456 // Only add a filled-out entry to the cache 457 if (entryUpdated) { 458 mCache.put(cacheKey, entry); 459 } 460 } 461 return entry; 462 } 463 getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes)464 private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) { 465 Cursor c = null; 466 try { 467 c = mIconDb.query( 468 lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES, 469 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", 470 new String[]{ 471 cacheKey.componentName.flattenToString(), 472 Long.toString(getSerialNumberForUser(cacheKey.user))}); 473 if (c.moveToNext()) { 474 // Set the alpha to be 255, so that we never have a wrong color 475 entry.color = setColorAlphaBound(c.getInt(0), 255); 476 entry.title = c.getString(1); 477 if (entry.title == null) { 478 entry.title = ""; 479 entry.contentDescription = ""; 480 } else { 481 entry.contentDescription = mPackageManager.getUserBadgedLabel( 482 entry.title, cacheKey.user); 483 } 484 485 if (lowRes) { 486 entry.icon = LOW_RES_ICON; 487 } else { 488 byte[] data = c.getBlob(2); 489 try { 490 entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length, 491 mDecodeOptions); 492 } catch (Exception e) { } 493 } 494 return true; 495 } 496 } catch (SQLiteException e) { 497 Log.d(TAG, "Error reading icon cache", e); 498 } finally { 499 if (c != null) { 500 c.close(); 501 } 502 } 503 return false; 504 } 505 506 /** 507 * Returns a cursor for an arbitrary query to the cache db 508 */ queryCacheDb(String[] columns, String selection, String[] selectionArgs)509 public synchronized Cursor queryCacheDb(String[] columns, String selection, 510 String[] selectionArgs) { 511 return mIconDb.query(columns, selection, selectionArgs); 512 } 513 514 /** 515 * Cache class to store the actual entries on disk 516 */ 517 public static final class IconDB extends SQLiteCacheHelper { 518 private static final int RELEASE_VERSION = 27; 519 520 public static final String TABLE_NAME = "icons"; 521 public static final String COLUMN_ROWID = "rowid"; 522 public static final String COLUMN_COMPONENT = "componentName"; 523 public static final String COLUMN_USER = "profileId"; 524 public static final String COLUMN_LAST_UPDATED = "lastUpdated"; 525 public static final String COLUMN_VERSION = "version"; 526 public static final String COLUMN_ICON = "icon"; 527 public static final String COLUMN_ICON_COLOR = "icon_color"; 528 public static final String COLUMN_LABEL = "label"; 529 public static final String COLUMN_SYSTEM_STATE = "system_state"; 530 public static final String COLUMN_KEYWORDS = "keywords"; 531 532 public static final String[] COLUMNS_HIGH_RES = new String[] { 533 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON }; 534 public static final String[] COLUMNS_LOW_RES = new String[] { 535 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL }; 536 IconDB(Context context, String dbFileName, int iconPixelSize)537 public IconDB(Context context, String dbFileName, int iconPixelSize) { 538 super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME); 539 } 540 541 @Override onCreateTable(SQLiteDatabase db)542 protected void onCreateTable(SQLiteDatabase db) { 543 db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" 544 + COLUMN_COMPONENT + " TEXT NOT NULL, " 545 + COLUMN_USER + " INTEGER NOT NULL, " 546 + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " 547 + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " 548 + COLUMN_ICON + " BLOB, " 549 + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " 550 + COLUMN_LABEL + " TEXT, " 551 + COLUMN_SYSTEM_STATE + " TEXT, " 552 + COLUMN_KEYWORDS + " TEXT, " 553 + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " 554 + ");"); 555 } 556 } 557 newContentValues(BitmapInfo bitmapInfo, String label, String packageName, @Nullable String keywords)558 private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, 559 String packageName, @Nullable String keywords) { 560 ContentValues values = new ContentValues(); 561 values.put(IconDB.COLUMN_ICON, 562 bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon)); 563 values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color); 564 565 values.put(IconDB.COLUMN_LABEL, label); 566 values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName)); 567 values.put(IconDB.COLUMN_KEYWORDS, keywords); 568 return values; 569 } 570 assertWorkerThread()571 private void assertWorkerThread() { 572 if (Looper.myLooper() != mBgLooper) { 573 throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper()); 574 } 575 } 576 } 577