diff options
author | Fan Zhang <zhfan@google.com> | 2017-09-27 10:23:23 -0700 |
---|---|---|
committer | Fan Zhang <zhfan@google.com> | 2017-09-27 12:08:56 -0700 |
commit | 04c0816541080d46f70803b58ce8666098f7089a (patch) | |
tree | 5aaf3351e7587f8ee5b54e953abeae51cf314ad1 | |
parent | 3f7535a8c62e98f1e21e1029235967b2deda241e (diff) | |
download | android_packages_apps_SettingsIntelligence-04c0816541080d46f70803b58ce8666098f7089a.tar.gz android_packages_apps_SettingsIntelligence-04c0816541080d46f70803b58ce8666098f7089a.tar.bz2 android_packages_apps_SettingsIntelligence-04c0816541080d46f70803b58ce8666098f7089a.zip |
Sync to latest SettingsIntelligence source code
Test: builds & robotests
Change-Id: Ie83909a9747afd07c541c6236224f079da8a2dc9
32 files changed, 2035 insertions, 68 deletions
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 index 07e226d..07e226d 100644 --- a/res/drawable/ic_launcher_settings_intelligence.xml +++ b/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 @@ public class EventStore { } 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_STATIC_JAVA_LIBRARIES := \ LOCAL_JAVA_LIBRARIES := \ junit \ - platform-robolectric-prebuilt \ + platform-robolectric-3.4.2-prebuilt \ sdk_vcurrent LOCAL_INSTRUMENTATION_FOR := SettingsIntelligence @@ -37,4 +37,4 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ 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 @@ import org.robolectric.res.ResourcePath; 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 @@ public class SettingsIntelligenceRobolectricTestRunner extends RobolectricTestRu @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 @@ public class SettingsIntelligenceRobolectricTestRunner extends RobolectricTestRu 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 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; - -@RunWith(SettingsIntelligenceRobolectricTestRunner.class) -@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) -public class SuggestionServiceTest { +public class TestConfig { - @Test - public void testRun() { + public static final String MANIFEST_PATH = "--default"; + public static final int SDK_VERSION = 26; - } } 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 @@ public class Suggestion { 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/android/service/settings/suggestions/SuggestionService.java b/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java new file mode 100644 index 0000000..e74f1fa --- /dev/null +++ b/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java @@ -0,0 +1,31 @@ +/* + * 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 android.service.settings.suggestions; + +import android.app.Service; + +import java.util.List; + +/** + * 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 class SuggestionRankerTest { 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 @@ public class SuggestionRankerTest { @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); } |