diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 54f2e860d27..6a2c01bcab6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -800,6 +800,22 @@ android:resource="@string/wallpaper_suggestion_summary" /> + + + + + + + + + + diff --git a/res/drawable/ic_new_releases_24dp.xml b/res/drawable/ic_new_releases_24dp.xml new file mode 100644 index 00000000000..b8fbb201601 --- /dev/null +++ b/res/drawable/ic_new_releases_24dp.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 28252c52345..81859c61183 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8913,4 +8913,6 @@ " • " + + diff --git a/src/com/android/settings/dashboard/DashboardSummary.java b/src/com/android/settings/dashboard/DashboardSummary.java index 7bd11f50a26..9c0bfd873b0 100644 --- a/src/com/android/settings/dashboard/DashboardSummary.java +++ b/src/com/android/settings/dashboard/DashboardSummary.java @@ -60,7 +60,6 @@ public class DashboardSummary extends InstrumentedFragment private static final int MAX_WAIT_MILLIS = 700; private static final String TAG = "DashboardSummary"; - private static final String SUGGESTIONS = "suggestions"; private static final String EXTRA_SCROLL_POSITION = "scroll_position"; @@ -98,7 +97,7 @@ public class DashboardSummary extends InstrumentedFragment mConditionManager = ConditionManager.get(activity, false); getLifecycle().addObserver(mConditionManager); mSuggestionParser = new SuggestionParser(activity, - activity.getSharedPreferences(SUGGESTIONS, 0), R.xml.suggestion_ordering); + mSuggestionFeatureProvider.getSharedPrefs(activity), R.xml.suggestion_ordering); mSuggestionsChecks = new SuggestionsChecks(getContext()); if (DEBUG_TIMING) { Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime) @@ -276,8 +275,6 @@ public class DashboardSummary extends InstrumentedFragment for (int i = 0; i < suggestions.size(); i++) { Tile suggestion = suggestions.get(i); if (mSuggestionsChecks.isSuggestionComplete(suggestion)) { - mSuggestionFeatureProvider.dismissSuggestion( - context, mSuggestionParser, suggestion); suggestions.remove(i--); } } diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java index 7cb37533121..5dc8892eca9 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java @@ -18,6 +18,7 @@ package com.android.settings.dashboard.suggestions; import android.content.ComponentName; import android.content.Context; +import android.content.SharedPreferences; import android.support.annotation.NonNull; import com.android.settingslib.drawer.Tile; @@ -33,12 +34,14 @@ public interface SuggestionFeatureProvider { */ boolean isSmartSuggestionEnabled(Context context); - /** Return true if {@code suggestion} is managed by this provider. */ - boolean isPresent(@NonNull ComponentName suggestion); - /** Return true if the suggestion has already been completed and does not need to be shown */ boolean isSuggestionCompleted(Context context, @NonNull ComponentName suggestion); + /** + * Returns the {@link SharedPreferences} that holds metadata for suggestions. + */ + SharedPreferences getSharedPrefs(Context context); + /** * Ranks the list of suggestions in place. * diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java index a2289e1a425..638f85f94f2 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java @@ -18,6 +18,7 @@ package com.android.settings.dashboard.suggestions; import android.content.ComponentName; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.support.annotation.NonNull; import android.util.Log; @@ -25,6 +26,7 @@ import android.util.Log; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.core.instrumentation.MetricsFeatureProvider; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.support.NewDeviceIntroSuggestionActivity; import com.android.settingslib.drawer.Tile; import com.android.settingslib.suggestions.SuggestionParser; @@ -35,6 +37,8 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider private static final String TAG = "SuggestionFeature"; private static final int EXCLUSIVE_SUGGESTION_MAX_COUNT = 3; + private static final String SHARED_PREF_FILENAME = "suggestions"; + private final SuggestionRanker mSuggestionRanker; private final MetricsFeatureProvider mMetricsFeatureProvider; @@ -44,16 +48,19 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider } @Override - public boolean isPresent(@NonNull ComponentName component) { + public boolean isSuggestionCompleted(Context context, @NonNull ComponentName component) { + final String className = component.getClassName(); + if (className.equals(NewDeviceIntroSuggestionActivity.class.getName())) { + return NewDeviceIntroSuggestionActivity.isSuggestionComplete(context); + } return false; } @Override - public boolean isSuggestionCompleted(Context context, @NonNull ComponentName component) { - return false; + public SharedPreferences getSharedPrefs(Context context) { + return context.getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE); } - public SuggestionFeatureProviderImpl(Context context) { final Context appContext = context.getApplicationContext(); mSuggestionRanker = new SuggestionRanker( diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionsChecks.java b/src/com/android/settings/dashboard/suggestions/SuggestionsChecks.java index ab845a32b7d..5c4edc6a7bb 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionsChecks.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionsChecks.java @@ -79,13 +79,10 @@ public class SuggestionsChecks { return isCameraLiftTriggerEnabled(); } - SuggestionFeatureProvider provider = + final SuggestionFeatureProvider provider = FeatureFactory.getFactory(mContext).getSuggestionFeatureProvider(mContext); - if (provider != null && provider.isPresent(component)) { - return provider.isSuggestionCompleted(mContext, component); - } - return false; + return provider.isSuggestionCompleted(mContext, component); } private boolean isDeviceSecured() { diff --git a/src/com/android/settings/search/SearchFragment.java b/src/com/android/settings/search/SearchFragment.java index 87df62ec85f..d1645c3b2ce 100644 --- a/src/com/android/settings/search/SearchFragment.java +++ b/src/com/android/settings/search/SearchFragment.java @@ -303,7 +303,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result, Pair... logTaggedData) { logSearchResultClicked(resultViewHolder, result, logTaggedData); - + mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); mSavedQueryController.saveQuery(mQuery); mResultClickCount++; } @@ -397,9 +397,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O TextUtils.isEmpty(mQuery) ? 0 : mQuery.length())); mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_RESULT, + resultViewHolder.getClickActionMetricName(), resultName, taggedData.toArray(new Pair[0])); - mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); } } diff --git a/src/com/android/settings/support/NewDeviceIntroSuggestionActivity.java b/src/com/android/settings/support/NewDeviceIntroSuggestionActivity.java new file mode 100644 index 00000000000..d301099ec13 --- /dev/null +++ b/src/com/android/settings/support/NewDeviceIntroSuggestionActivity.java @@ -0,0 +1,119 @@ +/* + * 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.support; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; +import com.android.settings.overlay.FeatureFactory; + +import java.util.List; + +public class NewDeviceIntroSuggestionActivity extends Activity { + + private static final String TAG = "NewDeviceIntroSugg"; + @VisibleForTesting + static final String PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME = + "pref_new_device_intro_suggestion_first_display_time_ms"; + @VisibleForTesting + static final String PREF_KEY_SUGGGESTION_COMPLETE = + "pref_new_device_intro_suggestion_complete"; + @VisibleForTesting + static final long PERMANENT_DISMISS_THRESHOLD = DateUtils.DAY_IN_MILLIS * 14; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent intent = getLaunchIntent(this); + if (intent != null) { + final SuggestionFeatureProvider featureProvider = FeatureFactory.getFactory(this) + .getSuggestionFeatureProvider(this); + final SharedPreferences prefs = featureProvider.getSharedPrefs(this); + prefs.edit().putBoolean(PREF_KEY_SUGGGESTION_COMPLETE, true).commit(); + startActivity(intent); + } + finish(); + } + + public static boolean isSuggestionComplete(Context context) { + return isExpired(context) || hasLaunchedBefore(context) || !canOpenUrlInBrowser(context); + } + + private static boolean isExpired(Context context) { + final SuggestionFeatureProvider featureProvider = FeatureFactory.getFactory(context) + .getSuggestionFeatureProvider(context); + final SharedPreferences prefs = featureProvider.getSharedPrefs(context); + final long currentTimeMs = System.currentTimeMillis(); + final long firstDisplayTimeMs; + + if (!prefs.contains(PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME)) { + firstDisplayTimeMs = currentTimeMs; + prefs.edit().putLong(PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME, currentTimeMs).commit(); + } else { + firstDisplayTimeMs = prefs.getLong(PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME, -1); + } + + final long dismissTimeMs = firstDisplayTimeMs + PERMANENT_DISMISS_THRESHOLD; + + final boolean expired = currentTimeMs > dismissTimeMs; + + Log.d(TAG, "is suggestion expired: " + expired); + return expired; + } + + private static boolean canOpenUrlInBrowser(Context context) { + final Intent intent = getLaunchIntent(context); + if (intent == null) { + // No url/intent to launch. + return false; + } + // Make sure we can handle the intent. + final List resolveInfos = + context.getPackageManager().queryIntentActivities(intent, 0); + return resolveInfos != null && resolveInfos.size() != 0; + } + + private static boolean hasLaunchedBefore(Context context) { + final SuggestionFeatureProvider featureProvider = FeatureFactory.getFactory(context) + .getSuggestionFeatureProvider(context); + final SharedPreferences prefs = featureProvider.getSharedPrefs(context); + return prefs.getBoolean(PREF_KEY_SUGGGESTION_COMPLETE, false); + } + + @VisibleForTesting + static Intent getLaunchIntent(Context context) { + final String url = context.getString(R.string.new_device_suggestion_intro_url); + if (TextUtils.isEmpty(url)) { + return null; + } + return new Intent() + .setAction(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(Uri.parse(url)); + } +} diff --git a/src/com/android/settings/support/SupportDashboardActivity.java b/src/com/android/settings/support/SupportDashboardActivity.java index 068d4e13e85..d3fcf9a8a28 100644 --- a/src/com/android/settings/support/SupportDashboardActivity.java +++ b/src/com/android/settings/support/SupportDashboardActivity.java @@ -68,6 +68,8 @@ public class SupportDashboardActivity extends Activity implements Indexable { SearchIndexableRaw data = new SearchIndexableRaw(context); data.title = context.getString(R.string.page_tab_title_support); data.screenTitle = context.getString(R.string.settings_label); + data.summaryOn = context.getString(R.string.support_summary); + data.iconResId = R.drawable.ic_help; data.intentTargetPackage = context.getPackageName(); data.intentTargetClass = SupportDashboardActivity.class.getName(); data.intentAction = Intent.ACTION_MAIN; diff --git a/tests/robotests/res/values-mcc999/strings.xml b/tests/robotests/res/values-mcc999/strings.xml new file mode 100644 index 00000000000..fb14b44c87d --- /dev/null +++ b/tests/robotests/res/values-mcc999/strings.xml @@ -0,0 +1,20 @@ + + + + + http://www.com.android.settings.test.com + diff --git a/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java index 6adc895d570..ea4f7f3b259 100644 --- a/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java @@ -18,8 +18,8 @@ package com.android.settings.search; import android.app.LoaderManager; -import android.content.Intent; import android.content.Context; +import android.content.Intent; import android.content.Loader; import android.os.Bundle; import android.util.Pair; @@ -404,6 +404,8 @@ public class SearchFragmentTest { // Should log result name, result count, clicked rank, etc. final SearchViewHolder resultViewHolder = mock(SearchViewHolder.class); + when(resultViewHolder.getClickActionMetricName()) + .thenReturn(MetricsProto.MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_RESULT); ResultPayload payLoad = new ResultPayload( (new Intent()).putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, "test_setting")); SearchResult searchResult = new SearchResult.Builder() diff --git a/tests/robotests/src/com/android/settings/support/NewDeviceIntroSuggestionActivityTest.java b/tests/robotests/src/com/android/settings/support/NewDeviceIntroSuggestionActivityTest.java new file mode 100644 index 00000000000..33927135aa0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/support/NewDeviceIntroSuggestionActivityTest.java @@ -0,0 +1,121 @@ +/* + * 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.support; + + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ResolveInfo; + +import com.android.settings.R; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.testutils.FakeFeatureFactory; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.res.builder.RobolectricPackageManager; + +import static com.android.settings.support.NewDeviceIntroSuggestionActivity + .PERMANENT_DISMISS_THRESHOLD; +import static com.android.settings.support.NewDeviceIntroSuggestionActivity + .PREF_KEY_SUGGGESTION_COMPLETE; +import static com.android.settings.support.NewDeviceIntroSuggestionActivity + .PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME; +import static com.android.settings.support.NewDeviceIntroSuggestionActivity.isSuggestionComplete; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class NewDeviceIntroSuggestionActivityTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mMockContext; + + private FakeFeatureFactory mFeatureFactory; + private Context mContext; + private RobolectricPackageManager mRobolectricPackageManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mFeatureFactory = FakeFeatureFactory.setupForTest(mMockContext); + mContext = RuntimeEnvironment.application; + mRobolectricPackageManager = RuntimeEnvironment.getRobolectricPackageManager(); + + when(mFeatureFactory.suggestionsFeatureProvider.getSharedPrefs(any(Context.class))) + .thenReturn(getSharedPreferences()); + } + + @Test + public void isSuggestionComplete_suggestionExpired_shouldReturnTrue() { + final long currentTime = System.currentTimeMillis(); + + getSharedPreferences().edit().putLong(PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME, + currentTime - 2 * PERMANENT_DISMISS_THRESHOLD); + assertThat(isSuggestionComplete(mContext)) + .isTrue(); + } + + @Test + public void isSuggestionComplete_noUrl_shouldReturnTrue() { + assertThat(mContext.getString(R.string.new_device_suggestion_intro_url)) + .isEqualTo(""); + assertThat(isSuggestionComplete(mContext)) + .isTrue(); + } + + // Use a non-default resource qualifier to load the test string in + // res/values-mcc999/strings.xml. + @Config(qualifiers = "mcc999") + @Test + public void isSuggestionComplete_alreadyLaunchedBefore_shouldReturnTrue() { + assertThat(mContext.getString(R.string.new_device_suggestion_intro_url)) + .startsWith("http"); + getSharedPreferences().edit().putBoolean(PREF_KEY_SUGGGESTION_COMPLETE, true).commit(); + + assertThat(isSuggestionComplete(mContext)) + .isTrue(); + } + + // Use a non-default resource qualifier to load the test string in + // res/values-mcc999/strings.xml. + @Config(qualifiers = "mcc999") + @Test + public void isSuggestionComplete_notExpiredAndCanOpenUrlInBrowser_shouldReturnFalse() { + assertThat(mContext.getString(R.string.new_device_suggestion_intro_url)) + .startsWith("http"); + + final Intent intent = NewDeviceIntroSuggestionActivity.getLaunchIntent(mContext); + mRobolectricPackageManager.addResolveInfoForIntent(intent, new ResolveInfo()); + assertThat(isSuggestionComplete(mContext)).isFalse(); + } + + private SharedPreferences getSharedPreferences() { + return mContext.getSharedPreferences("test_new_device_sugg", Context.MODE_PRIVATE); + } + +}