Add a suggestion: showing new device features

- New suggestion activity
- Removed useless api SuggestionFeatureProvider.isPresent().

- Also updated support activity search indexing icon and summary

Change-Id: Ib52cf26a985f57bf0aac918606b10f75bd024639
Fix: 62034077
Fix: 62196070
Test: make RunSettingsRoboTests
This commit is contained in:
Fan Zhang
2017-05-31 17:47:37 -07:00
parent 75c1fcb7fb
commit 9011a67431
13 changed files with 331 additions and 20 deletions

View File

@@ -800,6 +800,22 @@
android:resource="@string/wallpaper_suggestion_summary" />
</activity>
<activity android:name=".support.NewDeviceIntroSuggestionActivity"
android:label="@string/new_device_suggestion_title"
android:icon="@drawable/ic_new_releases_24dp"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.android.settings.suggested.category.FIRST_IMPRESSION" />
</intent-filter>
<meta-data android:name="com.android.settings.dismiss"
android:value="0,4" />
<meta-data android:name="com.android.settings.title"
android:resource="@string/new_device_suggestion_title" />
<meta-data android:name="com.android.settings.summary"
android:resource="@string/new_device_suggestion_summary" />
</activity>
<activity android:name="Settings$ZenModeScheduleRuleSettingsActivity"
android:exported="true"
android:taskAffinity="">

View File

@@ -0,0 +1,26 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:tint="?android:attr/colorControlNormal">
<path android:fillColor="#FF000000"
android:pathData="M23,12l-2.44,-2.78 0.34,-3.68 -3.61,-0.82 -1.89,-3.18L12,3 8.6,1.54 6.71,4.72l-3.61,0.81 0.34,3.68L1,12l2.44,2.78 -0.34,3.69 3.61,0.82 1.89,3.18L12,21l3.4,1.46 1.89,-3.18 3.61,-0.82 -0.34,-3.68L23,12zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>

View File

@@ -8913,4 +8913,6 @@
<!-- The divider symbol between different parts of the notification header including spaces. not translatable [CHAR LIMIT=3] -->
<string name="notification_header_divider_symbol_with_spaces" translatable="false">" • "</string>
<!-- url for new device suggestion -->
<string name="new_device_suggestion_intro_url" translatable="false"></string>
</resources>

View File

@@ -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--);
}
}

View File

@@ -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.
*

View File

@@ -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(

View File

@@ -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() {

View File

@@ -303,7 +303,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result,
Pair<Integer, Object>... 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);
}
}

View File

@@ -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<ResolveInfo> 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));
}
}

View File

@@ -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;

View File

@@ -0,0 +1,20 @@
<!--
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.
-->
<resources>
<!-- url for new device suggestion -->
<string name="new_device_suggestion_intro_url" translatable="false">http://www.com.android.settings.test.com</string>
</resources>

View File

@@ -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()

View File

@@ -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);
}
}