release-request-fbd98711-169e-4972-a5f2-db043df00e09-for-git_pi-release-4367572 snap-temp-L13500000107248413

Change-Id: I53cc5ffa8700c066372ae5db277a14ef3d866ea6
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1791d3f..76a9c68 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -23,7 +23,7 @@
 
     <application
         android:label="@string/app_name_settings_intelligence"
-        android:icon="@drawable/ic_launcher_settings_intelligence">
+        android:icon="@mipmap/ic_launcher">
         <service
             android:name=".suggestions.SuggestionService"
             android:exported="true"
diff --git a/res/drawable/ic_launcher_settings_intelligence.xml b/res/mipmap-anydpi/ic_launcher.xml
similarity index 100%
rename from res/drawable/ic_launcher_settings_intelligence.xml
rename to res/mipmap-anydpi/ic_launcher.xml
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java b/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java
new file mode 100644
index 0000000..400227b
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions;
+
+import android.content.Context;
+
+public class SuggestionDismissHandler {
+
+    private static final String IS_DISMISSED = "_is_dismissed";
+
+    private static SuggestionDismissHandler sDismissHandler;
+
+    private SuggestionDismissHandler() {}
+
+    public static SuggestionDismissHandler getInstance() {
+        if (sDismissHandler == null) {
+            sDismissHandler = new SuggestionDismissHandler();
+        }
+        return sDismissHandler;
+    }
+
+    public void markSuggestionDismissed(Context context, String id) {
+        SuggestionService.getSharedPrefs(context)
+                .edit()
+                .putBoolean(getDismissKey(id), true)
+                .apply();
+    }
+
+    public void markSuggestionNotDismissed(Context context, String id) {
+        SuggestionService.getSharedPrefs(context)
+                .edit()
+                .putBoolean(getDismissKey(id), false)
+                .apply();
+    }
+
+    public boolean isSuggestionDismissed(Context context, String id) {
+        return SuggestionService.getSharedPrefs(context)
+                .getBoolean(getDismissKey(id), false);
+    }
+
+    private static String getDismissKey(String id) {
+        return id + IS_DISMISSED;
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionParser.java b/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
new file mode 100644
index 0000000..c5af82e
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions;
+
+import static com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry
+        .CATEGORIES;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.service.settings.suggestions.Suggestion;
+import android.support.annotation.VisibleForTesting;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.settings.intelligence.suggestions.model.CandidateSuggestion;
+import com.android.settings.intelligence.suggestions.model.SuggestionCategory;
+import com.android.settings.intelligence.suggestions.model.SuggestionListBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Main parser to get suggestions from all providers.
+ * <p/>
+ * Copied from framework/packages/SettingsLib/src/.../SuggestionParser
+ */
+class SuggestionParser {
+
+    private static final String TAG = "SuggestionParser";
+    private static final String SETUP_TIME = "_setup_time";
+
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final SharedPreferences mSharedPrefs;
+    private final Map<String, Suggestion> mAddCache;
+
+
+    public SuggestionParser(Context context) {
+        mContext = context.getApplicationContext();
+        mPackageManager = context.getPackageManager();
+        mAddCache = new ArrayMap<>();
+        mSharedPrefs = SuggestionService.getSharedPrefs(mContext);
+    }
+
+    public List<Suggestion> getSuggestions() {
+        final SuggestionListBuilder suggestionBuilder = new SuggestionListBuilder();
+
+        for (SuggestionCategory category : CATEGORIES) {
+            if (category.isExclusive() && !isExclusiveCategoryExpired(category)) {
+                // If suggestions from an exclusive category are present, parsing is stopped
+                // and only suggestions from that category are displayed. Note that subsequent
+                // exclusive categories are also ignored.
+
+                // Read suggestion and force ignoreSuggestionDismissRule to be false so the rule
+                // defined from each suggestion itself is used.
+                final List<Suggestion> exclusiveSuggestions =
+                        readSuggestions(category, false /* ignoreDismissRule */);
+                if (!exclusiveSuggestions.isEmpty()) {
+                    suggestionBuilder.addSuggestions(category, exclusiveSuggestions);
+                    return suggestionBuilder.build();
+                }
+            } else {
+                // Either the category is not exclusive, or the exclusiveness expired so we should
+                // treat it as a normal category.
+                final List<Suggestion> suggestions =
+                        readSuggestions(category, true /* ignoreDismissRule */);
+                suggestionBuilder.addSuggestions(category, suggestions);
+            }
+        }
+        return suggestionBuilder.build();
+    }
+
+    @VisibleForTesting
+    List<Suggestion> readSuggestions(SuggestionCategory category, boolean ignoreDismissRule) {
+        final List<Suggestion> suggestions = new ArrayList<>();
+        final Intent probe = new Intent(Intent.ACTION_MAIN);
+        probe.addCategory(category.getCategory());
+        List<ResolveInfo> results = mPackageManager
+                .queryIntentActivities(probe, PackageManager.GET_META_DATA);
+        for (ResolveInfo resolved : results) {
+            final CandidateSuggestion candidate = new CandidateSuggestion(mContext, resolved,
+                    ignoreDismissRule);
+            if (!candidate.isEligible()) {
+                continue;
+            }
+
+            final String id = candidate.getId();
+            Suggestion suggestion = mAddCache.get(id);
+            if (suggestion == null) {
+                suggestion = candidate.toSuggestion();
+                mAddCache.put(id, suggestion);
+            }
+            if (!suggestions.contains(suggestion)) {
+                suggestions.add(suggestion);
+            }
+        }
+        return suggestions;
+    }
+
+    /**
+     * Whether or not the category's exclusiveness has expired.
+     */
+    private boolean isExclusiveCategoryExpired(SuggestionCategory category) {
+        final String keySetupTime = category.getCategory() + SETUP_TIME;
+        final long currentTime = System.currentTimeMillis();
+        if (!mSharedPrefs.contains(keySetupTime)) {
+            mSharedPrefs.edit()
+                    .putLong(keySetupTime, currentTime)
+                    .commit();
+        }
+        if (category.getExclusiveExpireDaysInMillis() < 0) {
+            // negative means never expires
+            return false;
+        }
+        final long setupTime = mSharedPrefs.getLong(keySetupTime, 0);
+        final long elapsedTime = currentTime - setupTime;
+        Log.d(TAG, "Day " + elapsedTime / DateUtils.DAY_IN_MILLIS + " for "
+                + category.getCategory());
+        return elapsedTime > category.getExclusiveExpireDaysInMillis();
+    }
+
+//
+//    /**
+//     * Gets text associated with the input key from the content provider.
+//     * @param context context
+//     * @param uriString URI for the content provider
+//     * @param providerMap Maps URI authorities to providers
+//     * @param key Key mapping to the text in bundle returned by the content provider
+//     * @return Text associated with the key, if returned by the content provider
+//     */
+//    public static String getTextFromUri(Context context, String uriString,
+//            Map<String, IContentProvider> providerMap, String key) {
+//        Bundle bundle = getBundleFromUri(context, uriString, providerMap);
+//        return (bundle != null) ? bundle.getString(key) : null;
+//    }
+//
+//    private static Bundle getBundleFromUri(Context context, String uriString,
+//            Map<String, IContentProvider> providerMap) {
+//        if (TextUtils.isEmpty(uriString)) {
+//            return null;
+//        }
+//        Uri uri = Uri.parse(uriString);
+//        String method = getMethodFromUri(uri);
+//        if (TextUtils.isEmpty(method)) {
+//            return null;
+//        }
+//        IContentProvider provider = getProviderFromUri(context, uri, providerMap);
+//        if (provider == null) {
+//            return null;
+//        }
+//        try {
+//            return provider.call(context.getPackageName(), method, uriString, null);
+//        } catch (RemoteException e) {
+//            return null;
+//        }
+//    }
+//
+//    private static IContentProvider getProviderFromUri(Context context, Uri uri,
+//            Map<String, IContentProvider> providerMap) {
+//        if (uri == null) {
+//            return null;
+//        }
+//        String authority = uri.getAuthority();
+//        if (TextUtils.isEmpty(authority)) {
+//            return null;
+//        }
+//        if (!providerMap.containsKey(authority)) {
+//            providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
+//        }
+//        return providerMap.get(authority);
+//    }
+//
+//    /** Returns the first path segment of the uri if it exists as the method, otherwise null. */
+//    static String getMethodFromUri(Uri uri) {
+//        if (uri == null) {
+//            return null;
+//        }
+//        List<String> pathSegments = uri.getPathSegments();
+//        if ((pathSegments == null) || pathSegments.isEmpty()) {
+//            return null;
+//        }
+//        return pathSegments.get(0);
+//    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionService.java b/src/com/android/settings/intelligence/suggestions/SuggestionService.java
index 6014ee8..b8ccef0 100644
--- a/src/com/android/settings/intelligence/suggestions/SuggestionService.java
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionService.java
@@ -16,26 +16,39 @@
 
 package com.android.settings.intelligence.suggestions;
 
+import android.content.Context;
+import android.content.SharedPreferences;
 import android.service.settings.suggestions.Suggestion;
 import android.util.Log;
 
-import java.util.ArrayList;
+import com.android.settings.intelligence.suggestions.ranking.SuggestionRanker;
+
 import java.util.List;
 
-public class SuggestionService extends android.service.settings.suggestions.SuggestionService  {
+public class SuggestionService extends android.service.settings.suggestions.SuggestionService {
 
     private static final String TAG = "SuggestionService";
 
+    private static final String SHARED_PREF_FILENAME = "suggestions";
+
     @Override
     public List<Suggestion> onGetSuggestions() {
-        final List<Suggestion> data = new ArrayList<>();
-        data.add(new Suggestion.Builder("test").build());
-        return data;
+        final SuggestionParser parser = new SuggestionParser(this);
+        final List<Suggestion> list = parser.getSuggestions();
+        SuggestionRanker.getInstance(this).rankSuggestions(list);
+        return list;
     }
 
     @Override
     public void onSuggestionDismissed(Suggestion suggestion) {
-        Log.d(TAG, "dismissing suggestion " + suggestion.getTitle());
+        final String id = suggestion.getId();
+        Log.d(TAG, "dismissing suggestion " + id);
+        SuggestionDismissHandler.getInstance()
+                .markSuggestionDismissed(this /* context */, id);
     }
 
+    public static SharedPreferences getSharedPrefs(Context context) {
+        return context.getApplicationContext()
+                .getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE);
+    }
 }
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityChecker.java
new file mode 100644
index 0000000..447dc77
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityChecker.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+public class AccountEligibilityChecker {
+    /**
+     * If defined, only display this optional step if an account of that type exists.
+     */
+    @VisibleForTesting
+    static final String META_DATA_REQUIRE_ACCOUNT = "com.android.settings.require_account";
+    private static final String TAG = "AccountEligibility";
+
+    public static boolean isEligible(Context context, String id, ResolveInfo info) {
+        final String requiredAccountType = info.activityInfo.metaData.
+                getString(META_DATA_REQUIRE_ACCOUNT);
+        if (requiredAccountType == null) {
+            return true;
+        }
+        AccountManager accountManager = AccountManager.get(context);
+        Account[] accounts = accountManager.getAccountsByType(requiredAccountType);
+        boolean satisfiesRequiredAccount = accounts.length > 0;
+        if (!satisfiesRequiredAccount) {
+            Log.i(TAG, id + " requires unavailable account type " + requiredAccountType);
+        }
+        return satisfiesRequiredAccount;
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityChecker.java
new file mode 100644
index 0000000..9318c98
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityChecker.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+public class ConnectivityEligibilityChecker {
+
+    /**
+     * If defined, only display this optional step if a connection is available.
+     */
+    @VisibleForTesting
+    static final String META_DATA_IS_CONNECTION_REQUIRED =
+            "com.android.settings.require_connection";
+    private static final String TAG = "ConnectivityEligibility";
+
+    public static boolean isEligible(Context context, String id, ResolveInfo info) {
+        final boolean isConnectionRequired =
+                info.activityInfo.metaData.getBoolean(META_DATA_IS_CONNECTION_REQUIRED);
+        if (!isConnectionRequired) {
+            return true;
+        }
+        final ConnectivityManager cm =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo netInfo = cm.getActiveNetworkInfo();
+        boolean satisfiesConnectivity = netInfo != null && netInfo.isConnectedOrConnecting();
+        if (!satisfiesConnectivity) {
+            Log.i(TAG, id + " is missing required connection.");
+        }
+        return satisfiesConnectivity;
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
new file mode 100644
index 0000000..a28ae3d
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ResolveInfo;
+import android.support.annotation.VisibleForTesting;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.suggestions.SuggestionDismissHandler;
+import com.android.settings.intelligence.suggestions.SuggestionService;
+
+public class DismissedChecker {
+
+    /**
+     * Allows suggestions to appear after a certain number of days, and to re-appear if dismissed.
+     * For instance:
+     * 0,10
+     * Will appear immediately, the 10 is ignored.
+     *
+     * 10
+     * Will appear after 10 days
+     */
+    @VisibleForTesting
+    static final String META_DATA_DISMISS_CONTROL = "com.android.settings.dismiss";
+
+    // Shared prefs keys for storing dismissed state.
+    // Index into current dismissed state.
+    private static final String SETUP_TIME = "_setup_time";
+    // Default dismiss rule for suggestions.
+
+    private static final int DEFAULT_FIRST_APPEAR_DAY = 0;
+
+    private static final String TAG = "DismissedChecker";
+
+    public static boolean isEligible(Context context, String id, ResolveInfo info,
+            boolean ignoreAppearRule) {
+        final SharedPreferences prefs = SuggestionService.getSharedPrefs(context);
+        final SuggestionDismissHandler dismissHandler = SuggestionDismissHandler.getInstance();
+        final String keySetupTime = id + SETUP_TIME;
+        if (!prefs.contains(keySetupTime)) {
+            prefs.edit()
+                    .putLong(keySetupTime, System.currentTimeMillis())
+                    .apply();
+        }
+
+        // Check if it's already manually dismissed
+        final boolean isDismissed = dismissHandler.isSuggestionDismissed(context, id);
+        if (isDismissed) {
+            return false;
+        }
+
+        // Parse when suggestion should first appear. return true to artificially hide suggestion
+        // before then.
+        int firstAppearDay = ignoreAppearRule
+                ? DEFAULT_FIRST_APPEAR_DAY
+                : parseAppearDay(info);
+        long firstAppearDayInMs = getEndTime(prefs.getLong(keySetupTime, 0), firstAppearDay);
+        if (System.currentTimeMillis() >= firstAppearDayInMs) {
+            // Dismiss timeout has passed, undismiss it.
+            dismissHandler.markSuggestionNotDismissed(context, id);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Parse the first int from a string formatted as "0,1,2..."
+     * The value means suggestion should first appear on Day X.
+     */
+    private static int parseAppearDay(ResolveInfo info) {
+        if (!info.activityInfo.metaData.containsKey(META_DATA_DISMISS_CONTROL)) {
+            return 0;
+        }
+
+        final Object firstAppearRule = info.activityInfo.metaData
+                .get(META_DATA_DISMISS_CONTROL);
+        if (firstAppearRule instanceof Integer) {
+            return (int) firstAppearRule;
+        } else {
+            try {
+                final String[] days = ((String) firstAppearRule).split(",");
+                return Integer.parseInt(days[0]);
+            } catch (Exception e) {
+                Log.w(TAG, "Failed to parse appear/dismiss rule, fall back to 0");
+                return 0;
+            }
+        }
+    }
+
+    private static long getEndTime(long startTime, int daysDelay) {
+        long days = daysDelay * DateUtils.DAY_IN_MILLIS;
+        return startTime + days;
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityChecker.java
new file mode 100644
index 0000000..1308515
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityChecker.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class FeatureEligibilityChecker {
+
+    private static final String TAG = "FeatureEligibility";
+
+    /**
+     * If defined, only returns this suggestion if the feature is supported.
+     */
+    @VisibleForTesting
+    static final String META_DATA_REQUIRE_FEATURE = "com.android.settings.require_feature";
+
+    public static boolean isEligible(Context context, String id, ResolveInfo info) {
+        final String featuresRequired = info.activityInfo.metaData
+                .getString(META_DATA_REQUIRE_FEATURE);
+        if (featuresRequired != null) {
+            for (String feature : featuresRequired.split(",")) {
+                if (TextUtils.isEmpty(feature)) {
+                    Log.i(TAG, "Found empty substring when parsing required features: "
+                            + featuresRequired);
+                } else if (!context.getPackageManager().hasSystemFeature(feature)) {
+                    Log.i(TAG, id + " requires unavailable feature " + feature);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
new file mode 100644
index 0000000..99efa4f
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.util.Log;
+
+public class ProviderEligibilityChecker {
+    /**
+     * If defined and not true, do not should optional step.
+     */
+    private static final String META_DATA_IS_SUPPORTED = "com.android.settings.is_supported";
+    private static final String TAG = "ProviderEligibility";
+
+    public static boolean isEligible(Context context, String id, ResolveInfo info) {
+        return isSystemApp(id, info)
+                && isEnabledInMetadata(context, id, info);
+    }
+
+    private static boolean isSystemApp(String id, ResolveInfo info) {
+        final boolean isSystemApp = info != null
+                && info.activityInfo != null
+                && info.activityInfo.applicationInfo != null
+                && (info.activityInfo.applicationInfo.flags
+                & ApplicationInfo.FLAG_SYSTEM) != 0;
+        if (!isSystemApp) {
+            Log.i(TAG, id + " is not system app, not eligible for suggestion");
+        }
+        return isSystemApp;
+    }
+
+    private static boolean isEnabledInMetadata(Context context, String id, ResolveInfo info) {
+        final int isSupportedResource =
+                info.activityInfo.metaData.getInt(META_DATA_IS_SUPPORTED);
+        try {
+            final Resources res = context.getPackageManager()
+                    .getResourcesForApplication(info.activityInfo.applicationInfo);
+            boolean isSupported =
+                    isSupportedResource != 0 ? res.getBoolean(isSupportedResource) : true;
+            if (!isSupported) {
+                Log.i(TAG, id + " requires unsupported resource " + isSupportedResource);
+            }
+            return isSupported;
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Cannot find resources for " + id, e);
+            return false;
+        } catch (Resources.NotFoundException e) {
+            Log.w(TAG, "Cannot find resources for " + id, e);
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java b/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
new file mode 100644
index 0000000..09fd02d
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.model;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.service.settings.suggestions.Suggestion;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker;
+import com.android.settings.intelligence.suggestions.eligibility.ConnectivityEligibilityChecker;
+import com.android.settings.intelligence.suggestions.eligibility.DismissedChecker;
+import com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker;
+import com.android.settings.intelligence.suggestions.eligibility.ProviderEligibilityChecker;
+
+/**
+ * A wrapper to {@link android.content.pm.ResolveInfo} that matches Suggestion signature.
+ * <p/>
+ * This class contains necessary metadata to eventually be
+ * processed into a {@link android.service.settings.suggestions.Suggestion}.
+ */
+public class CandidateSuggestion {
+
+    public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
+            "com.android.settings.icon_tintable";
+
+    public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
+
+    /**
+     * Name of the meta-data item that should be set in the AndroidManifest.xml
+     * to specify the summary text that should be displayed for the preference.
+     */
+    public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
+
+    /**
+     * Name of the meta-data item that should be set in the AndroidManifest.xml
+     * to specify the content provider providing the summary text that should be displayed for the
+     * preference.
+     *
+     * Summary provided by the content provider overrides any static summary.
+     */
+    public static final String META_DATA_PREFERENCE_SUMMARY_URI =
+            "com.android.settings.summary_uri";
+
+    public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
+            "com.android.settings.custom_view";
+
+    /**
+     * Name of the meta-data item that should be set in the AndroidManifest.xml
+     * to specify the icon that should be displayed for the preference.
+     */
+    public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
+
+    private static final String TAG = "CandidateSuggestion";
+
+    private final String mId;
+    private final Context mContext;
+    private final ResolveInfo mResolveInfo;
+    private final Intent mIntent;
+    private final boolean mIsEligible;
+    private final boolean mIgnoreAppearRule;
+
+    public CandidateSuggestion(Context context, ResolveInfo resolveInfo,
+            boolean ignoreAppearRule) {
+        mContext = context;
+        mIgnoreAppearRule = ignoreAppearRule;
+        mResolveInfo = resolveInfo;
+        mIntent = new Intent()
+                .setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
+        mId = generateId();
+        mIsEligible = initIsEligible();
+    }
+
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * Whether or not this candidate is eligible for display.
+     * <p/>
+     * Note: eligible doesn't mean it will be displayed.
+     */
+    public boolean isEligible() {
+        return mIsEligible;
+    }
+
+    public Suggestion toSuggestion() {
+        if (!mIsEligible) {
+            return null;
+        }
+        final Suggestion.Builder builder = new Suggestion.Builder(mId);
+        updateBuilder(builder);
+        return builder.build();
+    }
+
+    /**
+     * Checks device condition against suggestion requirement. Returns true if the suggestion is
+     * eligible.
+     * <p/>
+     * Note: eligible doesn't mean it will be displayed.
+     */
+    private boolean initIsEligible() {
+        if (!ProviderEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
+            return false;
+        }
+        if (!ConnectivityEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
+            return false;
+        }
+        if (!FeatureEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
+            return false;
+        }
+        if (!AccountEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
+            return false;
+        }
+        if (!DismissedChecker.isEligible(mContext, mId, mResolveInfo, mIgnoreAppearRule)) {
+            return false;
+        }
+        return true;
+    }
+
+    private void updateBuilder(Suggestion.Builder builder) {
+        final PackageManager pm = mContext.getPackageManager();
+        final ApplicationInfo applicationInfo = mResolveInfo.activityInfo.applicationInfo;
+
+        int icon = 0;
+        boolean iconTintable = false;
+        String title = null;
+        String summary = null;
+        RemoteViews remoteViews = null;
+
+        // Get the activity's meta-data
+        try {
+            final Resources res = pm.getResourcesForApplication(applicationInfo.packageName);
+            final Bundle metaData = mResolveInfo.activityInfo.metaData;
+
+            if (res != null && metaData != null) {
+                if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
+                    icon = metaData.getInt(META_DATA_PREFERENCE_ICON);
+                }
+                if (metaData.containsKey(META_DATA_PREFERENCE_ICON_TINTABLE)) {
+                    iconTintable = metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE);
+                }
+                if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
+                    if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
+                        title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
+                    } else {
+                        title = metaData.getString(META_DATA_PREFERENCE_TITLE);
+                    }
+                }
+                if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
+                    if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
+                        summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
+                    } else {
+                        summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
+                    }
+                }
+                if (metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)) {
+                    int layoutId = metaData.getInt(META_DATA_PREFERENCE_CUSTOM_VIEW);
+                    remoteViews = new RemoteViews(applicationInfo.packageName, layoutId);
+                }
+            }
+        } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
+            Log.d(TAG, "Couldn't find info", e);
+        }
+
+        // Set the preference title to the activity's label if no
+        // meta-data is found
+        if (TextUtils.isEmpty(title)) {
+            title = mResolveInfo.activityInfo.loadLabel(pm).toString();
+        }
+
+        if (icon == 0) {
+            icon = mResolveInfo.activityInfo.icon;
+        }
+        // TODO: Need to use ContentProvider to read dynamic title/summary etc.
+        final PendingIntent pendingIntent = PendingIntent
+                .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */);
+        builder.setTitle(title)
+                .setSummary(summary)
+                .setPendingIntent(pendingIntent);
+        // TODO: Need to extend Suggestion and set the following.
+        // set icon
+        // set icon tintable
+        // set remote view
+    }
+
+    private String generateId() {
+        return mIntent.getComponent().flattenToString();
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/model/SuggestionCategory.java b/src/com/android/settings/intelligence/suggestions/model/SuggestionCategory.java
new file mode 100644
index 0000000..2bf8f81
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/model/SuggestionCategory.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.model;
+
+/**
+ * A category for {@link android.service.settings.suggestions.Suggestion}.
+ * <p/>
+ * Each suggestion can sepcify it belongs to 1 or more categories. This metadata will be used to
+ * help determine when to show a suggestion to user.
+ */
+public class SuggestionCategory {
+    private final String mCategory;
+    private final boolean mExclusive;
+    private final long mExclusiveExpireDaysInMillis;
+
+    private SuggestionCategory(Builder builder) {
+        this.mCategory = builder.mCategory;
+        this.mExclusive = builder.mExclusive;
+        this.mExclusiveExpireDaysInMillis = builder.mExclusiveExpireDaysInMillis;
+    }
+
+    public String getCategory() {
+        return mCategory;
+    }
+
+    public boolean isExclusive() {
+        return mExclusive;
+    }
+
+    public long getExclusiveExpireDaysInMillis() {
+        return mExclusiveExpireDaysInMillis;
+    }
+
+    public static class Builder {
+        private String mCategory;
+        private boolean mExclusive;
+        private long mExclusiveExpireDaysInMillis;
+
+        public Builder setCategory(String category) {
+            mCategory = category;
+            return this;
+        }
+
+        public Builder setExclusive(boolean exclusive) {
+            mExclusive = exclusive;
+            return this;
+        }
+
+        public Builder setExclusiveExpireDaysInMillis(long exclusiveExpireDaysInMillis) {
+            mExclusiveExpireDaysInMillis = exclusiveExpireDaysInMillis;
+            return this;
+        }
+
+        public SuggestionCategory build() {
+            return new SuggestionCategory(this);
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistry.java b/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistry.java
new file mode 100644
index 0000000..7917a3e
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistry.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.model;
+
+import android.support.annotation.VisibleForTesting;
+import android.text.format.DateUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SuggestionCategoryRegistry {
+
+    @VisibleForTesting
+    static final String CATEGORY_KEY_DEFERRED_SETUP =
+            "com.android.settings.suggested.category.DEFERRED_SETUP";
+    @VisibleForTesting
+    static final String CATEGORY_KEY_FIRST_IMPRESSION =
+            "com.android.settings.suggested.category.FIRST_IMPRESSION";
+
+    public static final List<SuggestionCategory> CATEGORIES;
+
+    private static final long NEVER_EXPIRE = -1L;
+
+    // This is equivalent to packages/apps/settings/res/values/suggestion_ordering.xml
+    static {
+        CATEGORIES = new ArrayList<>();
+        CATEGORIES.add(buildCategory(CATEGORY_KEY_DEFERRED_SETUP,
+                true /* exclusive */, 14 * DateUtils.DAY_IN_MILLIS));
+        CATEGORIES.add(buildCategory(CATEGORY_KEY_FIRST_IMPRESSION,
+                true /* exclusive */, 14 * DateUtils.DAY_IN_MILLIS));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.LOCK_SCREEN",
+                false /* exclusive */, NEVER_EXPIRE));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.TRUST_AGENT",
+                false /* exclusive */, NEVER_EXPIRE));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.EMAIL",
+                false /* exclusive */, NEVER_EXPIRE));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.PARTNER_ACCOUNT",
+                false /* exclusive */, NEVER_EXPIRE));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.GESTURE",
+                false /* exclusive */, NEVER_EXPIRE));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.HOTWORD",
+                false /* exclusive */, NEVER_EXPIRE));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.DEFAULT",
+                false /* exclusive */, NEVER_EXPIRE));
+        CATEGORIES.add(buildCategory("com.android.settings.suggested.category.SETTINGS_ONLY",
+                false /* exclusive */, NEVER_EXPIRE));
+    }
+
+    private static SuggestionCategory buildCategory(String categoryName, boolean exclusive,
+            long expireDaysInMillis) {
+        return new SuggestionCategory.Builder()
+                .setCategory(categoryName)
+                .setExclusive(exclusive)
+                .setExclusiveExpireDaysInMillis(expireDaysInMillis)
+                .build();
+    }
+
+}
diff --git a/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilder.java b/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilder.java
new file mode 100644
index 0000000..d40373b
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.model;
+
+import android.service.settings.suggestions.Suggestion;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class SuggestionListBuilder {
+
+    private Map<SuggestionCategory, List<Suggestion>> mSuggestions;
+    private boolean isBuilt;
+
+    public SuggestionListBuilder() {
+        mSuggestions = new HashMap<>();
+    }
+
+    public void addSuggestions(SuggestionCategory category, List<Suggestion> suggestions) {
+        if (isBuilt) {
+            throw new IllegalStateException("Already built suggestion list, cannot add new ones");
+        }
+        mSuggestions.put(category, suggestions);
+    }
+
+    public List<Suggestion> build() {
+        isBuilt = true;
+        return dedupeSuggestions();
+    }
+
+    /**
+     * Filter suggestions list so they are all unique.
+     */
+    private List<Suggestion> dedupeSuggestions() {
+        final Set<String> ids = new ArraySet<>();
+        final List<Suggestion> suggestions = new ArrayList<>();
+        for (List<Suggestion> suggestionsInCategory : mSuggestions.values()) {
+            suggestions.addAll(suggestionsInCategory);
+        }
+        for (int i = suggestions.size() - 1; i >= 0; i--) {
+            final Suggestion suggestion = suggestions.get(i);
+            final String id = suggestion.getId();
+            if (ids.contains(id)) {
+                suggestions.remove(i);
+            } else {
+                ids.add(id);
+            }
+        }
+        return suggestions;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java b/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java
index 4772fae..f79c513 100644
--- a/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java
+++ b/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java
@@ -84,7 +84,7 @@
     }
 
     private void writePref(String prefKey, long value) {
-        mSharedPrefs.edit().putLong(prefKey, value).commit();
+        mSharedPrefs.edit().putLong(prefKey, value).apply();
     }
 
     private long readPref(String prefKey, Long defaultValue) {
diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk
index cb106ca..a27930e 100644
--- a/tests/robotests/Android.mk
+++ b/tests/robotests/Android.mk
@@ -13,7 +13,7 @@
 
 LOCAL_JAVA_LIBRARIES := \
     junit \
-    platform-robolectric-prebuilt \
+    platform-robolectric-3.4.2-prebuilt \
     sdk_vcurrent
 
 LOCAL_INSTRUMENTATION_FOR := SettingsIntelligence
@@ -37,4 +37,4 @@
 
 LOCAL_TEST_PACKAGE := SettingsIntelligence
 
-include prebuilts/misc/common/robolectric/run_robotests.mk
+include prebuilts/misc/common/robolectric/3.4.2/run_robotests.mk
diff --git a/tests/robotests/runners/android_mk/com/android/settings/intelligence/SettingsIntelligenceRobolectricTestRunner.java b/tests/robotests/runners/android_mk/com/android/settings/intelligence/SettingsIntelligenceRobolectricTestRunner.java
index e7f1d68..57ba8b8 100644
--- a/tests/robotests/runners/android_mk/com/android/settings/intelligence/SettingsIntelligenceRobolectricTestRunner.java
+++ b/tests/robotests/runners/android_mk/com/android/settings/intelligence/SettingsIntelligenceRobolectricTestRunner.java
@@ -16,13 +16,11 @@
 
 package com.android.settings.intelligence;
 
-import java.util.List;
 import org.junit.runners.model.InitializationError;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 import org.robolectric.manifest.AndroidManifest;
 import org.robolectric.res.Fs;
-import org.robolectric.res.ResourcePath;
 
 /**
  * Custom test runner for dealing with resources from multiple sources. This is needed because the
@@ -32,7 +30,8 @@
 public class SettingsIntelligenceRobolectricTestRunner extends RobolectricTestRunner {
 
     /** We don't actually want to change this behavior, so we just call super. */
-    public SettingsIntelligenceRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+    public SettingsIntelligenceRobolectricTestRunner(Class<?> testClass)
+            throws InitializationError {
         super(testClass);
     }
 
@@ -44,7 +43,7 @@
     @Override
     protected AndroidManifest getAppManifest(Config config) {
         // Using the manifest file's relative path, we can figure out the application directory.
-        final String appRoot = "vendor/unbundled_google/packages/Turbo";
+        final String appRoot = "packages/apps/SettingsIntelligence";
         final String manifestPath = appRoot + "/AndroidManifest.xml";
         final String resDir = appRoot + "tests/robotests/res";
         final String assetsDir = appRoot + config.assetDir();
@@ -56,28 +55,10 @@
                         Fs.fileFromPath(manifestPath),
                         Fs.fileFromPath(resDir),
                         Fs.fileFromPath(assetsDir)) {
-                    @Override
-                    public List<ResourcePath> getIncludedResourcePaths() {
-                        List<ResourcePath> paths = super.getIncludedResourcePaths();
-                        paths.add(
-                                new ResourcePath(
-                                        getPackageName(),
-                                        Fs.fileFromPath(
-                                                "./vendor/unbundled_google/packages/"
-                                                        + "Turbo/tests/robotests/res"),
-                                        null));
-                        paths.add(
-                                new ResourcePath(
-                                        getPackageName(),
-                                        Fs.fileFromPath(
-                                                "./vendor/unbundled_google/packages/Turbo/res"),
-                                        null));
-                        return paths;
-                    }
                 };
 
         // Set the package name to the renamed one
-        manifest.setPackageName("com.google.android.apps.turbo");
+        manifest.setPackageName("com.android.settings.intelligence");
         return manifest;
     }
 }
diff --git a/tests/robotests/runners/gradle/com/android/settings/intelligence/SettingsIntelligenceRobolectricTestRunner.java b/tests/robotests/runners/gradle/com/android/settings/intelligence/SettingsIntelligenceRobolectricTestRunner.java
new file mode 100644
index 0000000..69ebcf3
--- /dev/null
+++ b/tests/robotests/runners/gradle/com/android/settings/intelligence/SettingsIntelligenceRobolectricTestRunner.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.intelligence;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+public class SettingsIntelligenceRobolectricTestRunner extends RobolectricTestRunner {
+
+    /**
+     * Creates a runner to run {@code testClass}. Looks in your working directory for your
+     * AndroidManifest.xml file
+     * and res directory by default. Use the {@link Config} annotation to configure.
+     *
+     * @param testClass the test class to be run
+     * @throws InitializationError if junit says so
+     */
+    public SettingsIntelligenceRobolectricTestRunner(Class<?> testClass)
+            throws InitializationError {
+        super(testClass);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/SuggestionServiceTest.java b/tests/robotests/runners/gradle/com/android/settings/intelligence/TestConfig.java
similarity index 67%
rename from tests/robotests/src/com/android/settings/intelligence/SuggestionServiceTest.java
rename to tests/robotests/runners/gradle/com/android/settings/intelligence/TestConfig.java
index 747c128..6317252 100644
--- a/tests/robotests/src/com/android/settings/intelligence/SuggestionServiceTest.java
+++ b/tests/robotests/runners/gradle/com/android/settings/intelligence/TestConfig.java
@@ -13,19 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.settings.intelligence;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.annotation.Config;
+public class TestConfig {
 
-@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionServiceTest {
+    public static final String MANIFEST_PATH = "--default";
+    public static final int SDK_VERSION = 26;
 
-    @Test
-    public void testRun() {
-
-    }
 }
diff --git a/tests/robotests/src/android/service/settings/suggestions/Suggestion.java b/tests/robotests/src/android/service/settings/suggestions/Suggestion.java
index 2bb6192..f670f89 100644
--- a/tests/robotests/src/android/service/settings/suggestions/Suggestion.java
+++ b/tests/robotests/src/android/service/settings/suggestions/Suggestion.java
@@ -61,13 +61,6 @@
         mId = builder.mId;
     }
 
-    private Suggestion(Parcel in) {
-        mId = in.readString();
-        mTitle = in.readCharSequence();
-        mSummary = in.readCharSequence();
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader());
-    }
-
     /**
      * Builder class for {@link Suggestion}.
      */
diff --git a/tests/robotests/src/com/android/settings/intelligence/SuggestionServiceTest.java b/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java
similarity index 61%
copy from tests/robotests/src/com/android/settings/intelligence/SuggestionServiceTest.java
copy to tests/robotests/src/android/service/settings/suggestions/SuggestionService.java
index 747c128..e74f1fa 100644
--- a/tests/robotests/src/com/android/settings/intelligence/SuggestionServiceTest.java
+++ b/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java
@@ -14,18 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.settings.intelligence;
+package android.service.settings.suggestions;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.annotation.Config;
+import android.app.Service;
 
-@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionServiceTest {
+import java.util.List;
 
-    @Test
-    public void testRun() {
+/**
+ * Dupe to android.service.settings.suggestions.SuggestionService to get around robolectric problem.
+ */
+public abstract class SuggestionService extends Service {
 
-    }
+    public abstract List<Suggestion> onGetSuggestions();
+
+    public abstract void onSuggestionDismissed(Suggestion suggestion);
 }
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java
new file mode 100644
index 0000000..479d5c8
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions;
+
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_ICON;
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_SUMMARY;
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_SUMMARY_URI;
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_TITLE;
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestionTest.newInfo;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.service.settings.suggestions.Suggestion;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+import com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.util.List;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SuggestionParserTest {
+
+    private Context mContext;
+    private ShadowPackageManager mPackageManager;
+    private SuggestionParser mSuggestionParser;
+    private ResolveInfo mInfo1;
+    private ResolveInfo mInfo1Dupe;
+    private ResolveInfo mInfo2;
+    private ResolveInfo mInfo3;
+    private ResolveInfo mInfo4;
+
+    private Intent exclusiveIntent1;
+    private Intent exclusiveIntent2;
+    private Intent regularIntent;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
+        mSuggestionParser = new SuggestionParser(mContext);
+
+        mInfo1 = newInfo(mContext, "Class1", true /* systemApp */,
+                null /* summaryUri */, "title4", 0 /* titleResId */);
+        mInfo1Dupe = newInfo(mContext, "Class1", true /* systemApp */,
+                null /* summaryUri */, "title4", 0 /* titleResId */);
+        mInfo2 = newInfo(mContext, "Class2", true /* systemApp */,
+                null /* summaryUri */, "title4", 0 /* titleResId */);
+        mInfo3 = newInfo(mContext, "Class3", true /* systemApp */,
+                null /* summaryUri */, "title4", 0 /* titleResId */);
+        mInfo4 = newInfo(mContext, "Class4", true /* systemApp */,
+                null /* summaryUri */, "title4", 0 /* titleResId */);
+        mInfo4.activityInfo.applicationInfo.packageName = "ineligible";
+
+        exclusiveIntent1 = new Intent(Intent.ACTION_MAIN).addCategory(
+                SuggestionCategoryRegistry.CATEGORIES.get(0).getCategory());
+        exclusiveIntent2 = new Intent(Intent.ACTION_MAIN).addCategory(
+                SuggestionCategoryRegistry.CATEGORIES.get(1).getCategory());
+        regularIntent = new Intent(Intent.ACTION_MAIN).addCategory(
+                SuggestionCategoryRegistry.CATEGORIES.get(2).getCategory());
+    }
+
+    @Test
+    public void testGetSuggestions_exclusive() {
+        mPackageManager.addResolveInfoForIntent(exclusiveIntent1, mInfo1);
+        mPackageManager.addResolveInfoForIntent(exclusiveIntent1, mInfo1Dupe);
+        mPackageManager.addResolveInfoForIntent(exclusiveIntent2, mInfo2);
+        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo3);
+        final List<Suggestion> suggestions = mSuggestionParser.getSuggestions();
+
+        // info1
+        assertThat(suggestions).hasSize(1);
+    }
+
+    @Test
+    public void testGetSuggestion_onlyRegularCategoryAndNoDupe() {
+        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo1);
+        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo1Dupe);
+        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo2);
+        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo3);
+        mPackageManager.addResolveInfoForIntent(regularIntent, mInfo4);
+
+        final List<Suggestion> suggestions = mSuggestionParser.getSuggestions();
+
+        // info1, info2, info3 (info4 is skip because its package name is ineligible)
+        assertThat(suggestions).hasSize(3);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java
new file mode 100644
index 0000000..02bd455
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.service.settings.suggestions.Suggestion;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.android.controller.ServiceController;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SuggestionServiceTest {
+
+    private SuggestionService mService;
+    private ServiceController<SuggestionService> mServiceController;
+
+    @Before
+    public void setUp() {
+        mServiceController = Robolectric.buildService(SuggestionService.class);
+        mService = mServiceController.create().get();
+    }
+
+    @Test
+    public void getSuggestion_shouldReturnNonNull() {
+        assertThat(mService.onGetSuggestions()).isNotNull();
+    }
+
+    @Test
+    public void dismissSuggestion_shouldDismiss() {
+        final String id = "id1";
+        final Suggestion suggestion = new Suggestion.Builder(id).build();
+
+        // Not dismissed
+        assertThat(SuggestionDismissHandler.getInstance().isSuggestionDismissed(mService, id))
+                .isFalse();
+
+        // Dismiss
+        mService.onSuggestionDismissed(suggestion);
+
+        // Dismissed
+        assertThat(SuggestionDismissHandler.getInstance().isSuggestionDismissed(mService, id))
+                .isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java
new file mode 100644
index 0000000..033dc86
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import static com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker
+        .META_DATA_REQUIRE_ACCOUNT;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAccountManager;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class AccountEligibilityCheckerTest {
+
+    private static final String ID = "test";
+    private Context mContext;
+    private ResolveInfo mInfo;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mInfo = new ResolveInfo();
+        mInfo.activityInfo = new ActivityInfo();
+        mInfo.activityInfo.metaData = new Bundle();
+        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        mInfo.activityInfo.applicationInfo.packageName =
+                RuntimeEnvironment.application.getPackageName();
+    }
+
+    @After
+    public void tearDown() {
+        final ShadowAccountManager shadowAccountManager = Shadows.shadowOf(
+                AccountManager.get(mContext));
+        shadowAccountManager.removeAllAccounts();
+    }
+
+    @Test
+    public void isEligible_noAccountRequirement_shouldReturnTrue() {
+        assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_failRequirement_shouldReturnFalse() {
+        // Require android.com account but AccountManager doesn't have it.
+        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_ACCOUNT, "android.com");
+
+        assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isFalse();
+    }
+
+    @Test
+    public void isEligible_passRequirement_shouldReturnTrue() {
+        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_ACCOUNT, "android.com");
+        final Account account = new Account("TEST", "android.com");
+
+        final ShadowAccountManager shadowAccountManager = Shadows.shadowOf(
+                AccountManager.get(mContext));
+        shadowAccountManager.addAccount(account);
+
+        assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java
new file mode 100644
index 0000000..b6de5b9
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import static com.android.settings.intelligence.suggestions.eligibility
+        .ConnectivityEligibilityChecker.META_DATA_IS_CONNECTION_REQUIRED;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowConnectivityManager;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class ConnectivityEligibilityCheckerTest {
+
+    private static final String ID = "test";
+    private Context mContext;
+    private ResolveInfo mInfo;
+    private ShadowConnectivityManager mConnectivityManager;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mInfo = new ResolveInfo();
+        mInfo.activityInfo = new ActivityInfo();
+        mInfo.activityInfo.metaData = new Bundle();
+        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        mInfo.activityInfo.applicationInfo.packageName =
+                RuntimeEnvironment.application.getPackageName();
+
+        mConnectivityManager = Shadows.shadowOf(
+                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE));
+    }
+
+    @After
+    public void tearDown() {
+        mConnectivityManager.setActiveNetworkInfo(null);
+    }
+
+    @Test
+    public void isEligible_noRequirement_shouldReturnTrue() {
+        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isTrue();
+
+        mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, false);
+        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_hasConnection_shouldReturnTrue() {
+        mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, true);
+
+        final NetworkInfo networkInfo = mock(NetworkInfo.class);
+        when(networkInfo.isConnectedOrConnecting())
+                .thenReturn(true);
+
+        mConnectivityManager.setActiveNetworkInfo(networkInfo);
+
+        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_noConnection_shouldReturnFalse() {
+        mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, true);
+
+        mConnectivityManager.setActiveNetworkInfo(null);
+
+        assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java
new file mode 100644
index 0000000..7464d65
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+import static com.android.settings.intelligence.suggestions.eligibility.DismissedChecker
+        .META_DATA_DISMISS_CONTROL;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+import com.android.settings.intelligence.suggestions.SuggestionDismissHandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class DismissedCheckerTest {
+    private static final String ID = "test";
+    private Context mContext;
+    private ResolveInfo mInfo;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mInfo = new ResolveInfo();
+        mInfo.activityInfo = new ActivityInfo();
+        mInfo.activityInfo.metaData = new Bundle();
+        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        mInfo.activityInfo.applicationInfo.packageName =
+                RuntimeEnvironment.application.getPackageName();
+    }
+
+    @Test
+    public void isEligible_newSuggestion_noRule_shouldReturnTrue() {
+        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_newSuggestion_hasFutureRule_shouldReturnFalse() {
+        mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "10");
+
+        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, false /* ignoreAppearRule */))
+                .isFalse();
+    }
+
+    @Test
+    public void isEligible_newSuggestion_ignoreFutureRule_shouldReturnFalse() {
+        mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "10");
+
+        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_newSuggestion_hasPastRule_shouldReturnTrue() {
+        mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "-10");
+
+        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, false /* ignoreAppearRule */))
+                .isTrue();
+        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_dismissedSuggestion_shouldReturnFalse() {
+        SuggestionDismissHandler.getInstance().markSuggestionDismissed(mContext, ID);
+
+        assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
+                .isFalse();
+    }
+
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java
new file mode 100644
index 0000000..a2f2c23
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+
+import static com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker
+        .META_DATA_REQUIRE_FEATURE;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.FeatureInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowPackageManager;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class FeatureEligibilityCheckerTest {
+
+    private static final String ID = "test";
+    private Context mContext;
+    private ResolveInfo mInfo;
+    private ShadowPackageManager mPackageManager;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mInfo = new ResolveInfo();
+        mInfo.activityInfo = new ActivityInfo();
+        mInfo.activityInfo.metaData = new Bundle();
+        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        mInfo.activityInfo.applicationInfo.packageName =
+                RuntimeEnvironment.application.getPackageName();
+        mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
+    }
+
+    @After
+    public void tearDown() {
+        mPackageManager.clearSystemAvailableFeatures();
+    }
+
+    @Test
+    public void isEligible_noRequirement_shouldReturnTrue() {
+        assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_failRequirement_shouldReturnFalse() {
+        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_FEATURE, "test_feature");
+
+        assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isFalse();
+    }
+
+    @Test
+    public void isEligible_passRequirement_shouldReturnTrue() {
+        final FeatureInfo featureInfo = new FeatureInfo();
+        featureInfo.name = "fingerprint";
+        mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_FEATURE, featureInfo.name);
+        mPackageManager.addSystemAvailableFeature(featureInfo);
+
+        assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isFalse();
+    }
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java
new file mode 100644
index 0000000..16ddf8f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.eligibility;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class ProviderEligibilityCheckerTest {
+
+    private static final String ID = "test";
+    private Context mContext;
+    private ResolveInfo mInfo;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mInfo = new ResolveInfo();
+        mInfo.activityInfo = new ActivityInfo();
+        mInfo.activityInfo.metaData = new Bundle();
+        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        mInfo.activityInfo.applicationInfo.packageName =
+                RuntimeEnvironment.application.getPackageName();
+    }
+
+    @Test
+    public void isEligible_systemFlagSet_shouldReturnTrue() {
+        mInfo.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+
+        assertThat(ProviderEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isTrue();
+    }
+
+    @Test
+    public void isEligible_systemFlagNotSet_shouldReturnFalse() {
+        mInfo.activityInfo.applicationInfo.flags &= ~ApplicationInfo.FLAG_SYSTEM;
+
+        assertThat(ProviderEligibilityChecker.isEligible(mContext, ID, mInfo))
+                .isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java
new file mode 100644
index 0000000..240f1e0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.model;
+
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_ICON;
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_SUMMARY;
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_SUMMARY_URI;
+import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
+        .META_DATA_PREFERENCE_TITLE;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.service.settings.suggestions.Suggestion;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class CandidateSuggestionTest {
+
+    private static final String PACKAGE_NAME = "pkg";
+    private static final String CLASS_NAME = "class";
+
+    private Context mContext;
+    private ResolveInfo mInfo;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mInfo = new ResolveInfo();
+        mInfo.activityInfo = new ActivityInfo();
+        mInfo.activityInfo.metaData = new Bundle();
+        mInfo.activityInfo.packageName = PACKAGE_NAME;
+        mInfo.activityInfo.name = CLASS_NAME;
+
+        mInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        mInfo.activityInfo.applicationInfo.packageName =
+                RuntimeEnvironment.application.getPackageName();
+    }
+
+    @Test
+    public void getId_shouldUseComponentName() {
+        final CandidateSuggestion candidate =
+                new CandidateSuggestion(mContext, mInfo, true /* ignoreAppearRule */);
+
+        assertThat(candidate.getId())
+                .contains(PACKAGE_NAME + "/" + CLASS_NAME);
+    }
+
+    @Test
+    public void parseMetadata_eligibleSuggestion() {
+        final ResolveInfo info = newInfo(mContext, "class", true /* systemApp */,
+                null /*summaryUri */, "title", 0 /* titleResId */);
+        Suggestion suggestion = new CandidateSuggestion(
+                mContext, info, false /* ignoreAppearRule*/)
+                .toSuggestion();
+        assertThat(suggestion.getId()).isEqualTo(mContext.getPackageName()+"/class");
+        assertThat(suggestion.getTitle()).isEqualTo("title");
+        assertThat(suggestion.getSummary()).isEqualTo("static-summary");
+    }
+
+    @Test
+    public void parseMetadata_ineligibleSuggestion() {
+        final ResolveInfo info = newInfo(mContext, "class", false /* systemApp */,
+                null /*summaryUri */, "title", 0 /* titleResId */);
+        final CandidateSuggestion candidate = new CandidateSuggestion(
+                mContext, info, false /* ignoreAppearRule*/);
+
+        assertThat(candidate.isEligible()).isFalse();
+        assertThat(candidate.toSuggestion()).isNull();
+    }
+
+    public static ResolveInfo newInfo(Context context, String className, boolean systemApp,
+            String summaryUri, String title, int titleResId) {
+        final ResolveInfo info = new ResolveInfo();
+        info.activityInfo = new ActivityInfo();
+        info.activityInfo.applicationInfo = new ApplicationInfo();
+        if (systemApp) {
+            info.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+        }
+        info.activityInfo.packageName = context.getPackageName();
+        info.activityInfo.applicationInfo.packageName = info.activityInfo.packageName;
+        info.activityInfo.name = className;
+        info.activityInfo.metaData = new Bundle();
+        info.activityInfo.metaData.putInt(META_DATA_PREFERENCE_ICON, 314159);
+        info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY, "static-summary");
+        if (summaryUri != null) {
+            info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, summaryUri);
+        }
+        if (titleResId != 0) {
+            info.activityInfo.metaData.putInt(META_DATA_PREFERENCE_TITLE, titleResId);
+        } else if (title != null) {
+            info.activityInfo.metaData.putString(META_DATA_PREFERENCE_TITLE, title);
+        }
+        return info;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java
new file mode 100644
index 0000000..71041f8
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.model;
+
+import static com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry
+        .CATEGORIES;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SuggestionCategoryRegistryTest {
+
+    @Test
+    public void getCategories_shouldHave10Categories() {
+        assertThat(CATEGORIES)
+                .hasSize(10);
+    }
+
+    @Test
+    public void verifyExclusiveCategories() {
+        final List<String> exclusiveCategories = new ArrayList<>();
+        exclusiveCategories.add(SuggestionCategoryRegistry.CATEGORY_KEY_DEFERRED_SETUP);
+        exclusiveCategories.add(SuggestionCategoryRegistry.CATEGORY_KEY_FIRST_IMPRESSION);
+
+        int exclusiveCount = 0;
+        for (SuggestionCategory category : CATEGORIES) {
+            if (category.isExclusive()) {
+                exclusiveCount++;
+                assertThat(exclusiveCategories).contains(category.getCategory());
+            }
+        }
+        assertThat(exclusiveCount).isEqualTo(exclusiveCategories.size());
+    }
+
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java
new file mode 100644
index 0000000..cef6338
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.intelligence.suggestions.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.service.settings.suggestions.Suggestion;
+
+import com.android.settings.intelligence.SettingsIntelligenceRobolectricTestRunner;
+import com.android.settings.intelligence.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+
+@RunWith(SettingsIntelligenceRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SuggestionListBuilderTest {
+
+    private SuggestionListBuilder mBuilder;
+    private SuggestionCategory mCategory1;
+    private SuggestionCategory mCategory2;
+    private Suggestion mSuggestion1;
+    private Suggestion mSuggestion2;
+
+    @Before
+    public void setUp() {
+        mSuggestion1 = new Suggestion.Builder("id1")
+                .setTitle("title1")
+                .setSummary("summary1")
+                .build();
+        mCategory1 = SuggestionCategoryRegistry.CATEGORIES.get(3);
+        mCategory2 = SuggestionCategoryRegistry.CATEGORIES.get(4);
+        mSuggestion2 = new Suggestion.Builder("id2")
+                .setTitle("title2")
+                .setSummary("summary2")
+                .build();
+        mBuilder = new SuggestionListBuilder();
+    }
+
+    @Test
+    public void dedupe_shouldSkipSameSuggestion() {
+        mBuilder.addSuggestions(mCategory1, Arrays.asList(mSuggestion1));
+        mBuilder.addSuggestions(mCategory2, Arrays.asList(mSuggestion1));
+
+        assertThat(mBuilder.build()).hasSize(1);
+    }
+
+    @Test
+    public void dedupe_shouldContainDifferentSuggestion() {
+        mBuilder.addSuggestions(mCategory1, Arrays.asList(mSuggestion1, mSuggestion2));
+
+        assertThat(mBuilder.build()).hasSize(2);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java
index 7866ea3..839b7fd 100644
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java
+++ b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java
@@ -39,16 +39,15 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mFeatures = new HashMap<>();
-        mFeatures.put("pkg1", new HashMap<>());
-        mFeatures.put("pkg2", new HashMap<>());
-        mFeatures.put("pkg3", new HashMap<>());
-        mSuggestions = new ArrayList<Suggestion>() {
-            {
-                add(new Suggestion.Builder("pkg1").build());
-                add(new Suggestion.Builder("pkg2").build());
-                add(new Suggestion.Builder("pkg3").build());
-            }
-        };
+        mFeatures.put("pkg1", new HashMap<String, Double>());
+        mFeatures.put("pkg2", new HashMap<String, Double>());
+        mFeatures.put("pkg3", new HashMap<String, Double>());
+        mSuggestions = new ArrayList<>();
+
+        mSuggestions.add(new Suggestion.Builder("pkg1").build());
+        mSuggestions.add(new Suggestion.Builder("pkg2").build());
+        mSuggestions.add(new Suggestion.Builder("pkg3").build());
+
         mSuggestionFeaturizer = mock(SuggestionFeaturizer.class);
         mSuggestionRanker = new SuggestionRanker(mSuggestionFeaturizer);
         when(mSuggestionFeaturizer.featurize(mSuggestions)).thenReturn(mFeatures);
@@ -60,13 +59,11 @@
 
     @Test
     public void testRank() {
-        List<Suggestion> expectedOrderdList = new ArrayList<Suggestion>() {
-            {
-                add(mSuggestions.get(0)); // relevance = 0.9
-                add(mSuggestions.get(2)); // relevance = 0.5
-                add(mSuggestions.get(1)); // relevance = 0.1
-            }
-        };
+        final List<Suggestion> expectedOrderdList = new ArrayList<>();
+        expectedOrderdList.add(mSuggestions.get(0)); // relevance = 0.9
+        expectedOrderdList.add(mSuggestions.get(2)); // relevance = 0.5
+        expectedOrderdList.add(mSuggestions.get(1)); // relevance = 0.1
+
         mSuggestionRanker.rankSuggestions(mSuggestions);
         assertThat(mSuggestions).isEqualTo(expectedOrderdList);
     }