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 17 package com.android.settings.dashboard; 18 19 import static android.content.Intent.EXTRA_USER; 20 21 import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_CHECKED_STATE; 22 import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR; 23 import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE; 24 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; 25 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; 26 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_PROVIDER_ICON; 27 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_IS_CHECKED; 28 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_ON_CHECKED_CHANGED; 29 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; 30 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; 31 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; 32 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; 33 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; 34 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; 35 36 import android.app.PendingIntent; 37 import android.app.settings.SettingsEnums; 38 import android.content.ComponentName; 39 import android.content.Context; 40 import android.content.IContentProvider; 41 import android.content.Intent; 42 import android.content.pm.PackageManager; 43 import android.graphics.drawable.Drawable; 44 import android.graphics.drawable.Icon; 45 import android.net.Uri; 46 import android.os.Bundle; 47 import android.os.UserHandle; 48 import android.provider.Settings; 49 import android.text.TextUtils; 50 import android.util.ArrayMap; 51 import android.util.Log; 52 import android.util.Pair; 53 import android.widget.Toast; 54 55 import androidx.annotation.VisibleForTesting; 56 import androidx.fragment.app.FragmentActivity; 57 import androidx.preference.Preference; 58 import androidx.preference.TwoStatePreference; 59 60 import com.android.settings.R; 61 import com.android.settings.SettingsActivity; 62 import com.android.settings.Utils; 63 import com.android.settings.activityembedding.ActivityEmbeddingRulesController; 64 import com.android.settings.activityembedding.ActivityEmbeddingUtils; 65 import com.android.settings.dashboard.profileselector.ProfileSelectDialog; 66 import com.android.settings.homepage.TopLevelHighlightMixin; 67 import com.android.settings.homepage.TopLevelSettings; 68 import com.android.settings.overlay.FeatureFactory; 69 import com.android.settingslib.PrimarySwitchPreference; 70 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 71 import com.android.settingslib.drawer.ActivityTile; 72 import com.android.settingslib.drawer.CategoryKey; 73 import com.android.settingslib.drawer.DashboardCategory; 74 import com.android.settingslib.drawer.Tile; 75 import com.android.settingslib.drawer.TileUtils; 76 import com.android.settingslib.utils.ThreadUtils; 77 import com.android.settingslib.widget.AdaptiveIcon; 78 79 import com.google.common.collect.Iterables; 80 81 import java.util.ArrayList; 82 import java.util.List; 83 import java.util.Map; 84 85 /** 86 * Impl for {@code DashboardFeatureProvider}. 87 */ 88 public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { 89 90 private static final String TAG = "DashboardFeatureImpl"; 91 private static final String DASHBOARD_TILE_PREF_KEY_PREFIX = "dashboard_tile_pref_"; 92 private static final String META_DATA_KEY_INTENT_ACTION = "com.android.settings.intent.action"; 93 94 protected final Context mContext; 95 96 private final MetricsFeatureProvider mMetricsFeatureProvider; 97 private final CategoryManager mCategoryManager; 98 private final PackageManager mPackageManager; 99 DashboardFeatureProviderImpl(Context context)100 public DashboardFeatureProviderImpl(Context context) { 101 mContext = context.getApplicationContext(); 102 mCategoryManager = CategoryManager.get(context); 103 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 104 mPackageManager = context.getPackageManager(); 105 } 106 107 @Override getTilesForCategory(String key)108 public DashboardCategory getTilesForCategory(String key) { 109 return mCategoryManager.getTilesByCategory(mContext, key); 110 } 111 112 @Override getAllCategories()113 public List<DashboardCategory> getAllCategories() { 114 return mCategoryManager.getCategories(mContext); 115 } 116 117 @Override getDashboardKeyForTile(Tile tile)118 public String getDashboardKeyForTile(Tile tile) { 119 if (tile == null) { 120 return null; 121 } 122 if (tile.hasKey()) { 123 return tile.getKey(mContext); 124 } 125 final StringBuilder sb = new StringBuilder(DASHBOARD_TILE_PREF_KEY_PREFIX); 126 final ComponentName component = tile.getIntent().getComponent(); 127 sb.append(component.getClassName()); 128 return sb.toString(); 129 } 130 131 @Override bindPreferenceToTileAndGetObservers(FragmentActivity activity, DashboardFragment fragment, boolean forceRoundedIcon, Preference pref, Tile tile, String key, int baseOrder)132 public List<DynamicDataObserver> bindPreferenceToTileAndGetObservers(FragmentActivity activity, 133 DashboardFragment fragment, boolean forceRoundedIcon, Preference pref, Tile tile, 134 String key, int baseOrder) { 135 if (pref == null) { 136 return null; 137 } 138 if (!TextUtils.isEmpty(key)) { 139 pref.setKey(key); 140 } else { 141 pref.setKey(getDashboardKeyForTile(tile)); 142 } 143 final List<DynamicDataObserver> outObservers = new ArrayList<>(); 144 DynamicDataObserver observer = bindTitleAndGetObserver(pref, tile); 145 if (observer != null) { 146 outObservers.add(observer); 147 } 148 observer = bindSummaryAndGetObserver(pref, tile); 149 if (observer != null) { 150 outObservers.add(observer); 151 } 152 observer = bindSwitchAndGetObserver(pref, tile); 153 if (observer != null) { 154 outObservers.add(observer); 155 } 156 bindIcon(pref, tile, forceRoundedIcon); 157 158 if (tile.hasPendingIntent()) { 159 // Pending intent cannot be launched within the settings app panel, and will thus always 160 // be executed directly. 161 pref.setOnPreferenceClickListener(preference -> { 162 launchPendingIntentOrSelectProfile(activity, tile, fragment.getMetricsCategory()); 163 return true; 164 }); 165 } else if (tile instanceof ActivityTile) { 166 final int sourceMetricsCategory = fragment.getMetricsCategory(); 167 final Bundle metadata = tile.getMetaData(); 168 String clsName = null; 169 String action = null; 170 if (metadata != null) { 171 clsName = metadata.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); 172 action = metadata.getString(META_DATA_KEY_INTENT_ACTION); 173 } 174 if (!TextUtils.isEmpty(clsName)) { 175 pref.setFragment(clsName); 176 } else { 177 final Intent intent = new Intent(tile.getIntent()); 178 intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 179 sourceMetricsCategory); 180 if (action != null) { 181 intent.setAction(action); 182 } 183 // Register the rule for injected apps. 184 if (fragment instanceof TopLevelSettings) { 185 ActivityEmbeddingRulesController.registerTwoPanePairRuleForSettingsHome( 186 mContext, 187 new ComponentName(tile.getPackageName(), tile.getComponentName()), 188 action, 189 true /* clearTop */); 190 } 191 pref.setOnPreferenceClickListener(preference -> { 192 TopLevelHighlightMixin highlightMixin = null; 193 boolean isDuplicateClick = false; 194 if (fragment instanceof TopLevelSettings 195 && ActivityEmbeddingUtils.isEmbeddingActivityEnabled(mContext)) { 196 // Highlight the preference whenever it's clicked 197 final TopLevelSettings topLevelSettings = (TopLevelSettings) fragment; 198 highlightMixin = topLevelSettings.getHighlightMixin(); 199 isDuplicateClick = topLevelSettings.isDuplicateClick(preference); 200 topLevelSettings.setHighlightPreferenceKey(key); 201 } 202 launchIntentOrSelectProfile(activity, tile, intent, sourceMetricsCategory, 203 highlightMixin, isDuplicateClick); 204 return true; 205 }); 206 } 207 } 208 209 if (tile.hasOrder()) { 210 final String skipOffsetPackageName = activity.getPackageName(); 211 final int order = tile.getOrder(); 212 boolean shouldSkipBaseOrderOffset = TextUtils.equals( 213 skipOffsetPackageName, tile.getIntent().getComponent().getPackageName()); 214 if (shouldSkipBaseOrderOffset || baseOrder == Preference.DEFAULT_ORDER) { 215 pref.setOrder(order); 216 } else { 217 pref.setOrder(order + baseOrder); 218 } 219 } 220 return outObservers.isEmpty() ? null : outObservers; 221 } 222 223 @Override openTileIntent(FragmentActivity activity, Tile tile)224 public void openTileIntent(FragmentActivity activity, Tile tile) { 225 if (tile == null) { 226 Intent intent = new Intent(Settings.ACTION_SETTINGS) 227 .setPackage(mContext.getPackageName()) 228 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 229 mContext.startActivity(intent); 230 return; 231 } 232 final Intent intent = new Intent(tile.getIntent()) 233 .putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 234 SettingsEnums.DASHBOARD_SUMMARY) 235 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 236 launchIntentOrSelectProfile(activity, tile, intent, SettingsEnums.DASHBOARD_SUMMARY, 237 /* highlightMixin= */ null, /* isDuplicateClick= */ false); 238 } 239 createDynamicDataObserver(String method, Uri uri, Preference pref)240 private DynamicDataObserver createDynamicDataObserver(String method, Uri uri, Preference pref) { 241 return new DynamicDataObserver() { 242 @Override 243 public Uri getUri() { 244 return uri; 245 } 246 247 @Override 248 public void onDataChanged() { 249 switch (method) { 250 case METHOD_GET_DYNAMIC_TITLE: 251 refreshTitle(uri, pref, this); 252 break; 253 case METHOD_GET_DYNAMIC_SUMMARY: 254 refreshSummary(uri, pref, this); 255 break; 256 case METHOD_IS_CHECKED: 257 refreshSwitch(uri, pref, this); 258 break; 259 } 260 } 261 }; 262 } 263 264 private DynamicDataObserver bindTitleAndGetObserver(Preference preference, Tile tile) { 265 final CharSequence title = tile.getTitle(mContext.getApplicationContext()); 266 if (title != null) { 267 preference.setTitle(title); 268 return null; 269 } 270 if (tile.getMetaData() != null && tile.getMetaData().containsKey( 271 META_DATA_PREFERENCE_TITLE_URI)) { 272 // Set a placeholder title before starting to fetch real title, this is necessary 273 // to avoid preference height change. 274 if (preference.getTitle() == null) { 275 preference.setTitle(R.string.summary_placeholder); 276 } 277 278 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_TITLE_URI, 279 METHOD_GET_DYNAMIC_TITLE); 280 return createDynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference); 281 } 282 return null; 283 } 284 285 private void refreshTitle(Uri uri, Preference preference, DynamicDataObserver observer) { 286 ThreadUtils.postOnBackgroundThread(() -> { 287 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 288 final String titleFromUri = TileUtils.getTextFromUri( 289 mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE); 290 if (!TextUtils.equals(titleFromUri, preference.getTitle())) { 291 observer.post(() -> preference.setTitle(titleFromUri)); 292 } 293 }); 294 } 295 296 private DynamicDataObserver bindSummaryAndGetObserver(Preference preference, Tile tile) { 297 final CharSequence summary = tile.getSummary(mContext); 298 if (summary != null) { 299 preference.setSummary(summary); 300 } else if (tile.getMetaData() != null 301 && tile.getMetaData().containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { 302 // Set a placeholder summary before starting to fetch real summary, this is necessary 303 // to avoid preference height change. 304 if (preference.getSummary() == null) { 305 preference.setSummary(R.string.summary_placeholder); 306 } 307 308 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SUMMARY_URI, 309 METHOD_GET_DYNAMIC_SUMMARY); 310 return createDynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference); 311 } 312 return null; 313 } 314 315 private void refreshSummary(Uri uri, Preference preference, DynamicDataObserver observer) { 316 ThreadUtils.postOnBackgroundThread(() -> { 317 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 318 final String summaryFromUri = TileUtils.getTextFromUri( 319 mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); 320 if (!TextUtils.equals(summaryFromUri, preference.getSummary())) { 321 observer.post(() -> preference.setSummary(summaryFromUri)); 322 } 323 }); 324 } 325 326 private DynamicDataObserver bindSwitchAndGetObserver(Preference preference, Tile tile) { 327 if (!tile.hasSwitch()) { 328 return null; 329 } 330 331 final Uri onCheckedChangedUri = TileUtils.getCompleteUri(tile, 332 META_DATA_PREFERENCE_SWITCH_URI, METHOD_ON_CHECKED_CHANGED); 333 preference.setOnPreferenceChangeListener((pref, newValue) -> { 334 onCheckedChanged(onCheckedChangedUri, pref, (boolean) newValue); 335 return true; 336 }); 337 338 final Uri isCheckedUri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SWITCH_URI, 339 METHOD_IS_CHECKED); 340 setSwitchEnabled(preference, false); 341 return createDynamicDataObserver(METHOD_IS_CHECKED, isCheckedUri, preference); 342 } 343 344 private void onCheckedChanged(Uri uri, Preference pref, boolean checked) { 345 setSwitchEnabled(pref, false); 346 ThreadUtils.postOnBackgroundThread(() -> { 347 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 348 final Bundle result = TileUtils.putBooleanToUriAndGetResult(mContext, uri, providerMap, 349 EXTRA_SWITCH_CHECKED_STATE, checked); 350 351 ThreadUtils.postOnMainThread(() -> { 352 setSwitchEnabled(pref, true); 353 final boolean error = result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR); 354 if (!error) { 355 return; 356 } 357 358 setSwitchChecked(pref, !checked); 359 final String errorMsg = result.getString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE); 360 if (!TextUtils.isEmpty(errorMsg)) { 361 Toast.makeText(mContext, errorMsg, Toast.LENGTH_SHORT).show(); 362 } 363 }); 364 }); 365 } 366 367 private void refreshSwitch(Uri uri, Preference preference, DynamicDataObserver observer) { 368 ThreadUtils.postOnBackgroundThread(() -> { 369 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 370 final boolean checked = TileUtils.getBooleanFromUri(mContext, uri, providerMap, 371 EXTRA_SWITCH_CHECKED_STATE); 372 observer.post(() -> { 373 setSwitchChecked(preference, checked); 374 setSwitchEnabled(preference, true); 375 }); 376 }); 377 } 378 379 private void setSwitchChecked(Preference pref, boolean checked) { 380 if (pref instanceof PrimarySwitchPreference primarySwitchPreference) { 381 primarySwitchPreference.setChecked(checked); 382 } else if (pref instanceof TwoStatePreference twoStatePreference) { 383 twoStatePreference.setChecked(checked); 384 } 385 } 386 387 private void setSwitchEnabled(Preference pref, boolean enabled) { 388 if (pref instanceof PrimarySwitchPreference primarySwitchPreference) { 389 primarySwitchPreference.setSwitchEnabled(enabled); 390 } else { 391 pref.setEnabled(enabled); 392 } 393 } 394 395 @VisibleForTesting 396 void bindIcon(Preference preference, Tile tile, boolean forceRoundedIcon) { 397 // Icon provided by the content provider overrides any static icon. 398 if (tile.getMetaData() != null 399 && tile.getMetaData().containsKey(META_DATA_PREFERENCE_ICON_URI)) { 400 // Reserve the icon space to avoid preference padding change. 401 preference.setIconSpaceReserved(true); 402 403 ThreadUtils.postOnBackgroundThread(() -> { 404 final Intent intent = tile.getIntent(); 405 String packageName = null; 406 if (!TextUtils.isEmpty(intent.getPackage())) { 407 packageName = intent.getPackage(); 408 } else if (intent.getComponent() != null) { 409 packageName = intent.getComponent().getPackageName(); 410 } 411 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 412 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_ICON_URI, 413 METHOD_GET_PROVIDER_ICON); 414 final Pair<String, Integer> iconInfo = TileUtils.getIconFromUri( 415 mContext, packageName, uri, providerMap); 416 if (iconInfo == null) { 417 Log.w(TAG, "Failed to get icon from uri " + uri); 418 return; 419 } 420 final Icon icon = Icon.createWithResource(iconInfo.first, iconInfo.second); 421 ThreadUtils.postOnMainThread(() -> { 422 setPreferenceIcon(preference, tile, forceRoundedIcon, iconInfo.first, icon); 423 }); 424 }); 425 return; 426 } 427 428 // Use preference context instead here when get icon from Tile, as we are using the context 429 // to get the style to tint the icon. Using mContext here won't get the correct style. 430 final Icon tileIcon = tile.getIcon(preference.getContext()); 431 if (tileIcon == null) { 432 return; 433 } 434 setPreferenceIcon(preference, tile, forceRoundedIcon, tile.getPackageName(), tileIcon); 435 } 436 437 private void setPreferenceIcon(Preference preference, Tile tile, boolean forceRoundedIcon, 438 String iconPackage, Icon icon) { 439 Drawable iconDrawable = icon.loadDrawable(preference.getContext()); 440 if (iconDrawable == null) { 441 Log.w(TAG, "Set null preference icon for: " + iconPackage); 442 preference.setIcon(null); 443 return; 444 } 445 if (TextUtils.equals(tile.getCategory(), CategoryKey.CATEGORY_HOMEPAGE)) { 446 iconDrawable.setTint(Utils.getHomepageIconColor(preference.getContext())); 447 } 448 449 if (forceRoundedIcon && !TextUtils.equals(mContext.getPackageName(), iconPackage)) { 450 iconDrawable = new AdaptiveIcon(mContext, iconDrawable, 451 R.dimen.dashboard_tile_foreground_image_inset); 452 ((AdaptiveIcon) iconDrawable).setBackgroundColor(mContext, tile); 453 } 454 preference.setIcon(iconDrawable); 455 } 456 457 private void launchPendingIntentOrSelectProfile(FragmentActivity activity, Tile tile, 458 int sourceMetricCategory) { 459 ProfileSelectDialog.updatePendingIntentsIfNeeded(mContext, tile); 460 461 if (tile.pendingIntentMap.isEmpty()) { 462 Log.w(TAG, "Cannot resolve pendingIntent, skipping. " + tile.getIntent()); 463 return; 464 } 465 466 mMetricsFeatureProvider.logSettingsTileClick(tile.getKey(mContext), sourceMetricCategory); 467 468 // Launch the pending intent directly if there's only one available. 469 if (tile.pendingIntentMap.size() == 1) { 470 PendingIntent pendingIntent = Iterables.getOnlyElement(tile.pendingIntentMap.values()); 471 try { 472 pendingIntent.send(); 473 } catch (PendingIntent.CanceledException e) { 474 Log.w(TAG, "Failed executing pendingIntent. " + pendingIntent.getIntent(), e); 475 } 476 return; 477 } 478 479 ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile, 480 sourceMetricCategory, /* onShowListener= */ null, 481 /* onDismissListener= */ null, /* onCancelListener= */ null); 482 } 483 484 private void launchIntentOrSelectProfile(FragmentActivity activity, Tile tile, Intent intent, 485 int sourceMetricCategory, TopLevelHighlightMixin highlightMixin, 486 boolean isDuplicateClick) { 487 if (!isIntentResolvable(intent)) { 488 Log.w(TAG, "Cannot resolve intent, skipping. " + intent); 489 return; 490 } 491 ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile); 492 493 if (tile.userHandle == null || tile.isPrimaryProfileOnly()) { 494 if (!isDuplicateClick) { 495 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 496 activity.startActivity(intent); 497 } 498 } else if (tile.userHandle.size() == 1) { 499 if (!isDuplicateClick) { 500 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 501 activity.startActivityAsUser(intent, tile.userHandle.get(0)); 502 } 503 } else { 504 final UserHandle userHandle = intent.getParcelableExtra(EXTRA_USER); 505 if (userHandle != null && tile.userHandle.contains(userHandle)) { 506 if (!isDuplicateClick) { 507 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 508 activity.startActivityAsUser(intent, userHandle); 509 } 510 return; 511 } 512 513 final List<UserHandle> resolvableUsers = getResolvableUsers(intent, tile); 514 if (resolvableUsers.size() == 1) { 515 if (!isDuplicateClick) { 516 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 517 activity.startActivityAsUser(intent, resolvableUsers.get(0)); 518 } 519 return; 520 } 521 522 // Show the profile select dialog regardless of the duplicate click. 523 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 524 ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile, 525 sourceMetricCategory, /* onShowListener= */ highlightMixin, 526 /* onDismissListener= */ highlightMixin, 527 /* onCancelListener= */ highlightMixin); 528 } 529 } 530 531 private boolean isIntentResolvable(Intent intent) { 532 return mPackageManager.resolveActivity(intent, 0) != null; 533 } 534 535 private List<UserHandle> getResolvableUsers(Intent intent, Tile tile) { 536 final ArrayList<UserHandle> eligibleUsers = new ArrayList<>(); 537 for (UserHandle user : tile.userHandle) { 538 if (mPackageManager.resolveActivityAsUser(intent, 0, user.getIdentifier()) != null) { 539 eligibleUsers.add(user); 540 } 541 } 542 return eligibleUsers; 543 } 544 } 545