Separate suggestions and conditions.
This is the initial change for updating the suggestion cards:
- add a feature flag to swap between the new and current UI
- change suggestions to a standalone dashboard item. It becomes a
horizontal scroll list that won't collapse/expand.
- the expand/collapse logic now only control the conditions list
- add draft for the new suggestion UI elements, but detail to fine-tune
to match the UI spec will come in following changes.
Bug: 70573674
Test: make RunSettingsRoboTests
Change-Id: I00c901e2598b26a34288fc73fd6031cc26a29ac6
diff --git a/res/drawable/ic_suggestion_close_button.xml b/res/drawable/ic_suggestion_close_button.xml
new file mode 100644
index 0000000..615b215
--- /dev/null
+++ b/res/drawable/ic_suggestion_close_button.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2018 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18.3,5.71a0.996,0.996 0,0 0,-1.41 0L12,10.59 7.11,5.7A0.996,0.996 0,1 0,5.7 7.11L10.59,12 5.7,16.89a0.996,0.996 0,1 0,1.41 1.41L12,13.41l4.89,4.89a0.996,0.996 0,1 0,1.41 -1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z"/>
+</vector>
diff --git a/res/layout/condition_container.xml b/res/layout/condition_container.xml
new file mode 100644
index 0000000..808c4ac
--- /dev/null
+++ b/res/layout/condition_container.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ style="@style/SuggestionConditionStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:paddingBottom="@dimen/dashboard_padding_bottom">
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:cardUseCompatPadding="true"
+ app:cardElevation="2dp">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/data"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/material_grey_300"
+ android:scrollbars="none"/>
+
+ </android.support.v7.widget.CardView>
+
+</FrameLayout>
diff --git a/res/layout/suggestion_container.xml b/res/layout/suggestion_container.xml
new file mode 100644
index 0000000..2aa1043
--- /dev/null
+++ b/res/layout/suggestion_container.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/SuggestionConditionStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="24dp"
+ android:layout_centerVertical="true"
+ android:gravity="start"
+ android:text="@string/suggestions_title_v2"
+ android:textAppearance="@style/TextAppearance.SuggestionHeader" />
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginEnd="24dp"
+ android:layout_centerVertical="true"
+ android:gravity="end"
+ android:textAppearance="@style/TextAppearance.SuggestionHeader" />
+
+ </LinearLayout>
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/suggestion_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingBottom="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:scrollbars="none"/>
+
+</LinearLayout>
diff --git a/res/layout/suggestion_tile_v2.xml b/res/layout/suggestion_tile_v2.xml
new file mode 100644
index 0000000..e180897
--- /dev/null
+++ b/res/layout/suggestion_tile_v2.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<android.support.v7.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/suggestion_card"
+ android:layout_width="328dp"
+ android:layout_height="wrap_content"
+ app:cardUseCompatPadding="true"
+ app:cardElevation="2dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="112dp"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/dashboard_tile_image_size"
+ android:layout_height="@dimen/dashboard_tile_image_size"
+ android:layout_centerHorizontal = "true"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="8dp" />
+
+ <ImageView
+ android:id="@+id/close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:src="@drawable/ic_suggestion_close_button"/>
+
+ </RelativeLayout>
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:singleLine="true"
+ android:layout_marginLeft="12dp"
+ android:layout_marginRight="12dp"
+ android:textAppearance="@style/TextAppearance.TileTitle"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="@style/TextAppearance.SuggestionSummary" />
+
+ </LinearLayout>
+
+</android.support.v7.widget.CardView>
diff --git a/res/layout/suggestion_tile_with_button_v2.xml b/res/layout/suggestion_tile_with_button_v2.xml
new file mode 100644
index 0000000..01be236
--- /dev/null
+++ b/res/layout/suggestion_tile_with_button_v2.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<android.support.v7.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/suggestion_card"
+ android:layout_width="328dp"
+ android:layout_height="wrap_content"
+ app:cardUseCompatPadding="true"
+ app:cardElevation="2dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="112dp"
+ android:orientation="vertical"
+ android:background="@android:color/white">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/dashboard_tile_image_size"
+ android:layout_height="@dimen/dashboard_tile_image_size"
+ android:layout_centerHorizontal = "true"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="8dp" />
+
+ <ImageView
+ android:id="@+id/close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:src="@drawable/ic_suggestion_close_button"/>
+
+ </RelativeLayout>
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.TileTitle"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="@style/TextAppearance.SuggestionSummary" />
+
+ <Button
+ android:id="@android:id/primary"
+ style="@style/ActionPrimaryButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:text="@string/suggestion_button_text" />
+
+ </LinearLayout>
+
+</android.support.v7.widget.CardView>
diff --git a/res/values-sw400dp/dimens.xml b/res/values-sw400dp/dimens.xml
index 35a25d8..4f13e09 100755
--- a/res/values-sw400dp/dimens.xml
+++ b/res/values-sw400dp/dimens.xml
@@ -21,4 +21,11 @@
<dimen name="support_escalation_card_padding_start">56dp</dimen>
<dimen name="support_escalation_card_padding_end">56dp</dimen>
+
+ <!-- Suggestion cards-->
+ <dimen name="suggestion_card_width_one_card">380dp</dimen>
+ <dimen name="suggestion_card_width_two_cards">184dp</dimen>
+ <dimen name="suggestion_card_width_multiple_cards">176dp</dimen>
+ <dimen name="suggestion_card_padding_bottom_one_card">22dp</dimen>
+
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 00d9705..f8205e3 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -300,4 +300,11 @@
<dimen name="suggestion_condition_header_padding_collapsed">10dp</dimen>
<dimen name="suggestion_condition_header_padding_expanded">5dp</dimen>
+ <!-- Suggestion cards-->
+ <dimen name="suggestion_card_width_one_card">328dp</dimen>
+ <dimen name="suggestion_card_width_two_cards">158dp</dimen>
+ <dimen name="suggestion_card_width_multiple_cards">152dp</dimen>
+ <dimen name="suggestion_card_margin_end">12dp</dimen>
+ <dimen name="suggestion_card_padding_bottom_one_card">16dp</dimen>
+
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cfefa55..c9ecf9d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8401,6 +8401,9 @@
<string name="condition_summary" translatable="false"><xliff:g name="count" example="3">%1$d</xliff:g></string>
<!-- Title for the suggestions section on the dashboard [CHAR LIMIT=30] -->
+ <string name="suggestions_title_v2">Suggested for You</string>
+
+ <!-- Title for the suggestions section on the dashboard [CHAR LIMIT=30] -->
<string name="suggestions_title">Suggestions</string>
<!-- Summary for the suggestions section on the dashboard, representing number of suggestions. [CHAR LIMIT=10] -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 579ee48..9555d5e 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -316,6 +316,13 @@
<style name="TextAppearance.RecentsTitle" parent="TextAppearance.CategoryTitle" />
<style name="TextAppearance.ResultTitle" parent="TextAppearance.CategoryTitle" />
+ <style name="TextAppearance.SuggestionHeader"
+ parent="@android:style/TextAppearance.Material.Subhead">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/colorAccent</item>
+ </style>
+
<style name="TextAppearance.SuggestionTitle"
parent="@android:style/TextAppearance.Material.Subhead">
<item name="android:fontFamily">sans-serif-medium</item>
diff --git a/src/com/android/settings/core/FeatureFlags.java b/src/com/android/settings/core/FeatureFlags.java
index 7d9b331..dd9d316 100644
--- a/src/com/android/settings/core/FeatureFlags.java
+++ b/src/com/android/settings/core/FeatureFlags.java
@@ -27,4 +27,5 @@
public static final String BATTERY_DISPLAY_APP_LIST = "settings_battery_display_app_list";
public static final String SECURITY_SETTINGS_V2 = "settings_security_settings_v2";
public static final String ZONE_PICKER_V2 = "settings_zone_picker_v2";
+ public static final String SUGGESTION_UI_V2 = "settings_suggestion_ui_v2";
}
diff --git a/src/com/android/settings/dashboard/DashboardAdapterV2.java b/src/com/android/settings/dashboard/DashboardAdapterV2.java
new file mode 100644
index 0000000..cc511c5
--- /dev/null
+++ b/src/com/android/settings/dashboard/DashboardAdapterV2.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2018 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.dashboard;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.service.settings.suggestions.Suggestion;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.R;
+import com.android.settings.R.id;
+import com.android.settings.core.instrumentation.MetricsFeatureProvider;
+import com.android.settings.dashboard.DashboardDataV2.ConditionHeaderData;
+import com.android.settings.dashboard.conditional.Condition;
+import com.android.settings.dashboard.conditional.ConditionAdapterV2;
+import com.android.settings.dashboard.suggestions.SuggestionAdapterV2;
+import com.android.settings.dashboard.suggestions.SuggestionControllerMixin;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
+import com.android.settingslib.drawer.DashboardCategory;
+import com.android.settingslib.drawer.Tile;
+
+import java.util.List;
+
+public class DashboardAdapterV2 extends RecyclerView.Adapter<DashboardAdapterV2.DashboardItemHolder>
+ implements SummaryLoader.SummaryConsumer, SuggestionAdapterV2.Callback, LifecycleObserver,
+ OnSaveInstanceState {
+ public static final String TAG = "DashboardAdapterV2";
+ private static final String STATE_CATEGORY_LIST = "category_list";
+
+ @VisibleForTesting
+ static final String STATE_CONDITION_EXPANDED = "condition_expanded";
+
+ private final IconCache mCache;
+ private final Context mContext;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+ private final DashboardFeatureProvider mDashboardFeatureProvider;
+ private boolean mFirstFrameDrawn;
+ private RecyclerView mRecyclerView;
+ private SuggestionAdapterV2 mSuggestionAdapter;
+
+ @VisibleForTesting
+ DashboardDataV2 mDashboardData;
+
+ private View.OnClickListener mTileClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ //TODO: get rid of setTag/getTag
+ mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag());
+ }
+ };
+
+ public DashboardAdapterV2(Context context, Bundle savedInstanceState,
+ List<Condition> conditions, SuggestionControllerMixin suggestionControllerMixin,
+ Lifecycle lifecycle) {
+
+ DashboardCategory category = null;
+ boolean conditionExpanded = false;
+
+ mContext = context;
+ final FeatureFactory factory = FeatureFactory.getFactory(context);
+ mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
+ mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
+ mCache = new IconCache(context);
+ mSuggestionAdapter = new SuggestionAdapterV2(mContext, suggestionControllerMixin,
+ savedInstanceState, this /* callback */, lifecycle);
+
+ setHasStableIds(true);
+
+ if (savedInstanceState != null) {
+ category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST);
+ conditionExpanded = savedInstanceState.getBoolean(
+ STATE_CONDITION_EXPANDED, conditionExpanded);
+ }
+
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+
+ mDashboardData = new DashboardDataV2.Builder()
+ .setConditions(conditions)
+ .setSuggestions(mSuggestionAdapter.getSuggestions())
+ .setCategory(category)
+ .setConditionExpanded(conditionExpanded)
+ .build();
+ }
+
+ public void setSuggestions(List<Suggestion> data) {
+ final DashboardDataV2 prevData = mDashboardData;
+ mDashboardData = new DashboardDataV2.Builder(prevData)
+ .setSuggestions(data)
+ .build();
+ notifyDashboardDataChanged(prevData);
+ }
+
+ public void setCategory(DashboardCategory category) {
+ tintIcons(category);
+ final DashboardDataV2 prevData = mDashboardData;
+ Log.d(TAG, "adapter setCategory called");
+ mDashboardData = new DashboardDataV2.Builder(prevData)
+ .setCategory(category)
+ .build();
+ notifyDashboardDataChanged(prevData);
+ }
+
+ public void setConditions(List<Condition> conditions) {
+ final DashboardDataV2 prevData = mDashboardData;
+ Log.d(TAG, "adapter setConditions called");
+ mDashboardData = new DashboardDataV2.Builder(prevData)
+ .setConditions(conditions)
+ .build();
+ notifyDashboardDataChanged(prevData);
+ }
+
+ @Override
+ public void onSuggestionClosed(Suggestion suggestion) {
+ final List<Suggestion> list = mDashboardData.getSuggestions();
+ if (list == null || list.size() == 0) {
+ return;
+ }
+ if (list.size() == 1) {
+ // The only suggestion is dismissed, and the the empty suggestion container will
+ // remain as the dashboard item. Need to refresh the dashboard list.
+ final DashboardDataV2 prevData = mDashboardData;
+ mDashboardData = new DashboardDataV2.Builder(prevData)
+ .setSuggestions(null)
+ .build();
+ notifyDashboardDataChanged(prevData);
+ } else {
+ mSuggestionAdapter.removeSuggestion(suggestion);
+ }
+ }
+
+ @Override
+ public void notifySummaryChanged(Tile tile) {
+ final int position = mDashboardData.getPositionByTile(tile);
+ if (position != DashboardDataV2.POSITION_NOT_FOUND) {
+ // Since usually tile in parameter and tile in mCategories are same instance,
+ // which is hard to be detected by DiffUtil, so we notifyItemChanged directly.
+ notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position));
+ }
+ }
+
+ @Override
+ public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
+ if (viewType == R.layout.suggestion_condition_header) {
+ return new ConditionHeaderHolder(view);
+ }
+ if (viewType == R.layout.condition_container) {
+ return new ConditionContainerHolder(view);
+ }
+ if (viewType == R.layout.suggestion_container) {
+ return new SuggestionContainerHolder(view);
+ }
+ return new DashboardItemHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(DashboardItemHolder holder, int position) {
+ final int type = mDashboardData.getItemTypeByPosition(position);
+ switch (type) {
+ case R.layout.dashboard_tile:
+ final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position);
+ onBindTile((DashboardItemHolder) holder, tile);
+ holder.itemView.setTag(tile);
+ holder.itemView.setOnClickListener(mTileClickListener);
+ break;
+ case R.layout.suggestion_container:
+ onBindSuggestion((SuggestionContainerHolder) holder, position);
+ break;
+ case R.layout.condition_container:
+ onBindCondition((ConditionContainerHolder) holder, position);
+ break;
+ case R.layout.suggestion_condition_header:
+ onBindConditionHeader((ConditionHeaderHolder) holder,
+ (ConditionHeaderData) mDashboardData.getItemEntityByPosition(position));
+ break;
+ case R.layout.suggestion_condition_footer:
+ holder.itemView.setOnClickListener(v -> {
+ mMetricsFeatureProvider.action(mContext,
+ MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false);
+ DashboardDataV2 prevData = mDashboardData;
+ mDashboardData = new DashboardDataV2.Builder(prevData).
+ setConditionExpanded(false).build();
+ notifyDashboardDataChanged(prevData);
+ scrollToTopOfConditions();
+ });
+ break;
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mDashboardData.getItemIdByPosition(position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mDashboardData.getItemTypeByPosition(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDashboardData.size();
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+ // save the view so that we can scroll it when expanding/collapsing the suggestion and
+ // conditions.
+ mRecyclerView = recyclerView;
+ }
+
+ public Object getItem(long itemId) {
+ return mDashboardData.getItemEntityById(itemId);
+ }
+
+ public Suggestion getSuggestion(int position) {
+ return mSuggestionAdapter.getSuggestion(position);
+ }
+
+ @VisibleForTesting
+ void notifyDashboardDataChanged(DashboardDataV2 prevData) {
+ if (mFirstFrameDrawn && prevData != null) {
+ final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardDataV2
+ .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList()));
+ diffResult.dispatchUpdatesTo(this);
+ } else {
+ mFirstFrameDrawn = true;
+ notifyDataSetChanged();
+ }
+ }
+
+ @VisibleForTesting
+ void onBindConditionHeader(final ConditionHeaderHolder holder, ConditionHeaderData data) {
+ holder.icon.setImageIcon(data.conditionIcons.get(0));
+ if (data.conditionCount == 1) {
+ holder.title.setText(data.title);
+ holder.summary.setText(null);
+ holder.icons.setVisibility(View.INVISIBLE);
+ } else {
+ holder.title.setText(null);
+ holder.summary.setText(
+ mContext.getString(R.string.condition_summary, data.conditionCount));
+ updateConditionIcons(data.conditionIcons, holder.icons);
+ holder.icons.setVisibility(View.VISIBLE);
+ }
+
+ holder.itemView.setOnClickListener(v -> {
+ mMetricsFeatureProvider.action(mContext,
+ MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true);
+ final DashboardDataV2 prevData = mDashboardData;
+ mDashboardData = new DashboardDataV2.Builder(prevData)
+ .setConditionExpanded(true).build();
+ notifyDashboardDataChanged(prevData);
+ scrollToTopOfConditions();
+ });
+ }
+
+ @VisibleForTesting
+ void onBindCondition(final ConditionContainerHolder holder, int position) {
+ final ConditionAdapterV2 adapter = new ConditionAdapterV2(mContext,
+ (List<Condition>) mDashboardData.getItemEntityByPosition(position),
+ mDashboardData.isConditionExpanded());
+ adapter.addDismissHandling(holder.data);
+ holder.data.setAdapter(adapter);
+ holder.data.setLayoutManager(new LinearLayoutManager(mContext));
+ }
+
+ @VisibleForTesting
+ void onBindSuggestion(final SuggestionContainerHolder holder, int position) {
+ // If there is suggestions to show, it will be at position 0 as we don't show the suggestion
+ // header anymore.
+ final List<Suggestion> suggestions = mDashboardData.getSuggestions();
+ final int suggestionCount = suggestions.size();
+ if (suggestions != null && suggestionCount > 0) {
+ holder.summary.setText(""+suggestionCount);
+ mSuggestionAdapter.setSuggestions(suggestions);
+ holder.data.setAdapter(mSuggestionAdapter);
+ }
+ final LinearLayoutManager layoutManager = new LinearLayoutManager(mContext);
+ layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
+ holder.data.setLayoutManager(layoutManager);
+ }
+
+ private void onBindTile(DashboardItemHolder holder, Tile tile) {
+ holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
+ holder.title.setText(tile.title);
+ if (!TextUtils.isEmpty(tile.summary)) {
+ holder.summary.setText(tile.summary);
+ holder.summary.setVisibility(View.VISIBLE);
+ } else {
+ holder.summary.setVisibility(View.GONE);
+ }
+ }
+
+ private void tintIcons(DashboardCategory category) {
+ if (!mDashboardFeatureProvider.shouldTintIcon()) {
+ return;
+ }
+ // TODO: Better place for tinting?
+ final TypedArray a = mContext.obtainStyledAttributes(new int[]{
+ android.R.attr.colorControlNormal});
+ final int tintColor = a.getColor(0, mContext.getColor(R.color.fallback_tintColor));
+ a.recycle();
+ if (category != null) {
+ for (Tile tile : category.getTiles()) {
+ if (tile.isIconTintable) {
+ // If this drawable is tintable, tint it to match the color.
+ tile.icon.setTint(tintColor);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ final DashboardCategory category = mDashboardData.getCategory();
+ if (category != null) {
+ outState.putParcelable(STATE_CATEGORY_LIST, category);
+ }
+ outState.putBoolean(STATE_CONDITION_EXPANDED, mDashboardData.isConditionExpanded());
+ }
+
+ private void updateConditionIcons(List<Icon> icons, ViewGroup parent) {
+ if (icons == null || icons.size() < 2) {
+ parent.setVisibility(View.INVISIBLE);
+ return;
+ }
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ parent.removeAllViews();
+ for (int i = 1, size = icons.size(); i < size; i++) {
+ ImageView icon = (ImageView) inflater.inflate(
+ R.layout.condition_header_icon, parent, false);
+ icon.setImageIcon(icons.get(i));
+ parent.addView(icon);
+ }
+ parent.setVisibility(View.VISIBLE);
+ }
+
+ private void scrollToTopOfConditions() {
+ mRecyclerView.scrollToPosition(mDashboardData.hasSuggestion() ? 1 : 0);
+ }
+
+ public static class IconCache {
+ private final Context mContext;
+ private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>();
+
+ public IconCache(Context context) {
+ mContext = context;
+ }
+
+ public Drawable getIcon(Icon icon) {
+ if (icon == null) {
+ return null;
+ }
+ Drawable drawable = mMap.get(icon);
+ if (drawable == null) {
+ drawable = icon.loadDrawable(mContext);
+ mMap.put(icon, drawable);
+ }
+ return drawable;
+ }
+ }
+
+ public static class DashboardItemHolder extends RecyclerView.ViewHolder {
+ public final ImageView icon;
+ public final TextView title;
+ public final TextView summary;
+
+ public DashboardItemHolder(View itemView) {
+ super(itemView);
+ icon = itemView.findViewById(android.R.id.icon);
+ title = itemView.findViewById(android.R.id.title);
+ summary = itemView.findViewById(android.R.id.summary);
+ }
+ }
+
+ public static class ConditionHeaderHolder extends DashboardItemHolder {
+ public final LinearLayout icons;
+ public final ImageView expandIndicator;
+
+ public ConditionHeaderHolder(View itemView) {
+ super(itemView);
+ icons = itemView.findViewById(id.additional_icons);
+ expandIndicator = itemView.findViewById(id.expand_indicator);
+ }
+ }
+
+ public static class ConditionContainerHolder extends DashboardItemHolder {
+ public final RecyclerView data;
+
+ public ConditionContainerHolder(View itemView) {
+ super(itemView);
+ data = itemView.findViewById(id.data);
+ }
+ }
+
+ public static class SuggestionContainerHolder extends DashboardItemHolder {
+ public final RecyclerView data;
+
+ public SuggestionContainerHolder(View itemView) {
+ super(itemView);
+ data = itemView.findViewById(id.suggestion_list);
+ }
+ }
+
+}
diff --git a/src/com/android/settings/dashboard/DashboardDataV2.java b/src/com/android/settings/dashboard/DashboardDataV2.java
new file mode 100644
index 0000000..e25ee05
--- /dev/null
+++ b/src/com/android/settings/dashboard/DashboardDataV2.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2018 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.dashboard;
+
+import android.annotation.IntDef;
+import android.graphics.drawable.Icon;
+import android.service.settings.suggestions.Suggestion;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.util.DiffUtil;
+import android.text.TextUtils;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.conditional.Condition;
+import com.android.settingslib.drawer.DashboardCategory;
+import com.android.settingslib.drawer.Tile;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Description about data list used in the DashboardAdapter. In the data list each item can be
+ * Condition, suggestion or category tile.
+ * <p>
+ * ItemsData has inner class Item, which represents the Item in data list.
+ */
+public class DashboardDataV2 {
+ public static final int POSITION_NOT_FOUND = -1;
+ public static final int MAX_SUGGESTION_COUNT = 4;
+
+ // stable id for different type of items.
+ @VisibleForTesting
+ static final int STABLE_ID_SUGGESTION_CONTAINER = 0;
+ static final int STABLE_ID_SUGGESTION_CONDITION_DIVIDER = 1;
+ @VisibleForTesting
+ static final int STABLE_ID_CONDITION_HEADER = 2;
+ @VisibleForTesting
+ static final int STABLE_ID_CONDITION_FOOTER = 3;
+ @VisibleForTesting
+ static final int STABLE_ID_CONDITION_CONTAINER = 4;
+
+ private final List<Item> mItems;
+ private final DashboardCategory mCategory;
+ private final List<Condition> mConditions;
+ private final List<Suggestion> mSuggestions;
+ private final boolean mConditionExpanded;
+
+ private DashboardDataV2(Builder builder) {
+ mCategory = builder.mCategory;
+ mConditions = builder.mConditions;
+ mSuggestions = builder.mSuggestions;
+ mConditionExpanded = builder.mConditionExpanded;
+ mItems = new ArrayList<>();
+
+ buildItemsData();
+ }
+
+ public int getItemIdByPosition(int position) {
+ return mItems.get(position).id;
+ }
+
+ public int getItemTypeByPosition(int position) {
+ return mItems.get(position).type;
+ }
+
+ public Object getItemEntityByPosition(int position) {
+ return mItems.get(position).entity;
+ }
+
+ public List<Item> getItemList() {
+ return mItems;
+ }
+
+ public int size() {
+ return mItems.size();
+ }
+
+ public Object getItemEntityById(long id) {
+ for (final Item item : mItems) {
+ if (item.id == id) {
+ return item.entity;
+ }
+ }
+ return null;
+ }
+
+ public DashboardCategory getCategory() {
+ return mCategory;
+ }
+
+ public List<Condition> getConditions() {
+ return mConditions;
+ }
+
+ public List<Suggestion> getSuggestions() {
+ return mSuggestions;
+ }
+
+ public boolean hasSuggestion() {
+ return sizeOf(mSuggestions) > 0;
+ }
+
+ public boolean isConditionExpanded() {
+ return mConditionExpanded;
+ }
+
+ /**
+ * Find the position of the object in mItems list, using the equals method to compare
+ *
+ * @param entity the object that need to be found in list
+ * @return position of the object, return POSITION_NOT_FOUND if object isn't in the list
+ */
+ public int getPositionByEntity(Object entity) {
+ if (entity == null) return POSITION_NOT_FOUND;
+
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Object item = mItems.get(i).entity;
+ if (entity.equals(item)) {
+ return i;
+ }
+ }
+
+ return POSITION_NOT_FOUND;
+ }
+
+ /**
+ * Find the position of the Tile object.
+ * <p>
+ * First, try to find the exact identical instance of the tile object, if not found,
+ * then try to find a tile has the same title.
+ *
+ * @param tile tile that need to be found
+ * @return position of the object, return INDEX_NOT_FOUND if object isn't in the list
+ */
+ public int getPositionByTile(Tile tile) {
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Object entity = mItems.get(i).entity;
+ if (entity == tile) {
+ return i;
+ } else if (entity instanceof Tile && tile.title.equals(((Tile) entity).title)) {
+ return i;
+ }
+ }
+
+ return POSITION_NOT_FOUND;
+ }
+
+ /**
+ * Add item into list when {@paramref add} is true.
+ *
+ * @param item maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null
+ * @param type type of the item, and value is the layout id
+ * @param stableId The stable id for this item
+ * @param add flag about whether to add item into list
+ */
+ private void addToItemList(Object item, int type, int stableId, boolean add) {
+ if (add) {
+ mItems.add(new Item(item, type, stableId));
+ }
+ }
+
+ /**
+ * Build the mItems list using mConditions, mSuggestions, mCategories data
+ * and mIsShowingAll, mConditionExpanded flag.
+ */
+ private void buildItemsData() {
+ final List<Condition> conditions = getConditionsToShow(mConditions);
+ final boolean hasConditions = sizeOf(conditions) > 0;
+
+ final List<Suggestion> suggestions = getSuggestionsToShow(mSuggestions);
+ final boolean hasSuggestions = sizeOf(suggestions) > 0;
+
+ /* Suggestion container. This is the card view that contains the list of suggestions.
+ * This will be added whenever the suggestion list is not empty */
+ addToItemList(suggestions, R.layout.suggestion_container,
+ STABLE_ID_SUGGESTION_CONTAINER, hasSuggestions);
+
+ /* Divider between suggestion and conditions if both are present. */
+ addToItemList(suggestions, R.layout.horizontal_divider,
+ STABLE_ID_SUGGESTION_CONDITION_DIVIDER, hasSuggestions && hasConditions);
+
+ /* Condition header. This will be present when there is condition and it is collapsed */
+ addToItemList(new ConditionHeaderData(conditions),
+ R.layout.suggestion_condition_header,
+ STABLE_ID_CONDITION_HEADER, hasConditions && !mConditionExpanded);
+
+ /* Condition container. This is the card view that contains the list of conditions.
+ * This will be added whenever the condition list is not empty and expanded */
+ addToItemList(conditions, R.layout.condition_container,
+ STABLE_ID_CONDITION_CONTAINER, hasConditions && mConditionExpanded);
+
+ /* Condition footer. This will be present when there is condition and it is expanded */
+ addToItemList(null /* item */, R.layout.suggestion_condition_footer,
+ STABLE_ID_CONDITION_FOOTER, hasConditions && mConditionExpanded);
+
+ if (mCategory != null) {
+ final List<Tile> tiles = mCategory.getTiles();
+ for (int i = 0; i < tiles.size(); i++) {
+ final Tile tile = tiles.get(i);
+ addToItemList(tile, R.layout.dashboard_tile, Objects.hash(tile.title),
+ true /* add */);
+ }
+ }
+ }
+
+ private static int sizeOf(List<?> list) {
+ return list == null ? 0 : list.size();
+ }
+
+ private List<Condition> getConditionsToShow(List<Condition> conditions) {
+ if (conditions == null) {
+ return null;
+ }
+ List<Condition> result = new ArrayList<>();
+ final int size = conditions == null ? 0 : conditions.size();
+ for (int i = 0; i < size; i++) {
+ final Condition condition = conditions.get(i);
+ if (condition.shouldShow()) {
+ result.add(condition);
+ }
+ }
+ return result;
+ }
+
+ private List<Suggestion> getSuggestionsToShow(List<Suggestion> suggestions) {
+ if (suggestions == null) {
+ return null;
+ }
+ if (suggestions.size() <= MAX_SUGGESTION_COUNT) {
+ return suggestions;
+ }
+ return suggestions.subList(0, MAX_SUGGESTION_COUNT);
+ }
+
+ /**
+ * Builder used to build the ItemsData
+ */
+ public static class Builder {
+ private DashboardCategory mCategory;
+ private List<Condition> mConditions;
+ private List<Suggestion> mSuggestions;
+ private boolean mConditionExpanded;
+
+ public Builder() {
+ }
+
+ public Builder(DashboardDataV2 dashboardData) {
+ mCategory = dashboardData.mCategory;
+ mConditions = dashboardData.mConditions;
+ mSuggestions = dashboardData.mSuggestions;
+ mConditionExpanded = dashboardData.mConditionExpanded;
+ }
+
+ public Builder setCategory(DashboardCategory category) {
+ this.mCategory = category;
+ return this;
+ }
+
+ public Builder setConditions(List<Condition> conditions) {
+ this.mConditions = conditions;
+ return this;
+ }
+
+ public Builder setSuggestions(List<Suggestion> suggestions) {
+ this.mSuggestions = suggestions;
+ return this;
+ }
+
+ public Builder setConditionExpanded(boolean expanded) {
+ this.mConditionExpanded = expanded;
+ return this;
+ }
+
+ public DashboardDataV2 build() {
+ return new DashboardDataV2(this);
+ }
+ }
+
+ /**
+ * A DiffCallback to calculate the difference between old and new Item
+ * List in DashboardDataV2
+ */
+ public static class ItemsDataDiffCallback extends DiffUtil.Callback {
+ final private List<Item> mOldItems;
+ final private List<Item> mNewItems;
+
+ public ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems) {
+ mOldItems = oldItems;
+ mNewItems = newItems;
+ }
+
+ @Override
+ public int getOldListSize() {
+ return mOldItems.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return mNewItems.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ return mOldItems.get(oldItemPosition).id == mNewItems.get(newItemPosition).id;
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ return mOldItems.get(oldItemPosition).equals(mNewItems.get(newItemPosition));
+ }
+
+ }
+
+ /**
+ * An item contains the data needed in the DashboardDataV2.
+ */
+ static class Item {
+ // valid types in field type
+ private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile;
+ private static final int TYPE_SUGGESTION_CONTAINER =
+ R.layout.suggestion_container;
+ private static final int TYPE_CONDITION_CONTAINER =
+ R.layout.condition_container;
+ private static final int TYPE_CONDITION_HEADER =
+ R.layout.suggestion_condition_header;
+ private static final int TYPE_CONDITION_FOOTER =
+ R.layout.suggestion_condition_footer;
+ private static final int TYPE_SUGGESTION_CONDITION_DIVIDER = R.layout.horizontal_divider;
+
+ @IntDef({TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_CONTAINER, TYPE_CONDITION_CONTAINER,
+ TYPE_CONDITION_HEADER, TYPE_CONDITION_FOOTER, TYPE_SUGGESTION_CONDITION_DIVIDER})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ItemTypes {
+ }
+
+ /**
+ * The main data object in item, usually is a {@link Tile}, {@link Condition}
+ * object. This object can also be null when the
+ * item is an divider line. Please refer to {@link #buildItemsData()} for
+ * detail usage of the Item.
+ */
+ public final Object entity;
+
+ /**
+ * The type of item, value inside is the layout id(e.g. R.layout.dashboard_tile)
+ */
+ @ItemTypes
+ public final int type;
+
+ /**
+ * Id of this item, used in the {@link ItemsDataDiffCallback} to identify the same item.
+ */
+ public final int id;
+
+ public Item(Object entity, @ItemTypes int type, int id) {
+ this.entity = entity;
+ this.type = type;
+ this.id = id;
+ }
+
+ /**
+ * Override it to make comparision in the {@link ItemsDataDiffCallback}
+ *
+ * @param obj object to compared with
+ * @return true if the same object or has equal value.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (!(obj instanceof Item)) {
+ return false;
+ }
+
+ final Item targetItem = (Item) obj;
+ if (type != targetItem.type || id != targetItem.id) {
+ return false;
+ }
+
+ switch (type) {
+ case TYPE_DASHBOARD_TILE:
+ final Tile localTile = (Tile) entity;
+ final Tile targetTile = (Tile) targetItem.entity;
+
+ // Only check title and summary for dashboard tile
+ return TextUtils.equals(localTile.title, targetTile.title)
+ && TextUtils.equals(localTile.summary, targetTile.summary);
+ case TYPE_SUGGESTION_CONTAINER:
+ case TYPE_CONDITION_CONTAINER:
+ // If entity is suggestion and contains remote view, force refresh
+ final List entities = (List) entity;
+ if (!entities.isEmpty()) {
+ Object firstEntity = entities.get(0);
+ if (firstEntity instanceof Tile
+ && ((Tile) firstEntity).remoteViews != null) {
+ return false;
+ }
+ }
+ // Otherwise Fall through to default
+ default:
+ return entity == null ? targetItem.entity == null
+ : entity.equals(targetItem.entity);
+ }
+ }
+ }
+
+ /**
+ * This class contains the data needed to build the suggestion/condition header. The data can
+ * also be used to check the diff in DiffUtil.Callback
+ */
+ public static class ConditionHeaderData {
+ public final List<Icon> conditionIcons;
+ public final CharSequence title;
+ public final int conditionCount;
+
+ public ConditionHeaderData(List<Condition> conditions) {
+ conditionCount = sizeOf(conditions);
+ title = conditionCount > 0 ? conditions.get(0).getTitle() : null;
+ conditionIcons = new ArrayList<>();
+ for (int i = 0; conditions != null && i < conditions.size(); i++) {
+ final Condition condition = conditions.get(i);
+ conditionIcons.add(condition.getIcon());
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/dashboard/DashboardFeatureProvider.java b/src/com/android/settings/dashboard/DashboardFeatureProvider.java
index 3ca146b..c493e672 100644
--- a/src/com/android/settings/dashboard/DashboardFeatureProvider.java
+++ b/src/com/android/settings/dashboard/DashboardFeatureProvider.java
@@ -88,4 +88,9 @@
*/
void openTileIntent(Activity activity, Tile tile);
+ /**
+ * Whether or not we should use the v2 of suggestions UI.
+ */
+ boolean useSuggestionUiV2();
+
}
diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
index 048f6ed..a06fee9 100644
--- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
+++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
@@ -33,12 +33,14 @@
import android.support.v7.preference.Preference;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.Pair;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
+import com.android.settings.core.FeatureFlags;
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.drawer.CategoryManager;
@@ -213,6 +215,11 @@
launchIntentOrSelectProfile(activity, tile, intent, MetricsEvent.DASHBOARD_SUMMARY);
}
+ @Override
+ public boolean useSuggestionUiV2() {
+ return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.SUGGESTION_UI_V2);
+ }
+
private void bindSummary(Preference preference, Tile tile) {
if (tile.summary != null) {
preference.setSummary(tile.summary);
diff --git a/src/com/android/settings/dashboard/DashboardSummary.java b/src/com/android/settings/dashboard/DashboardSummary.java
index 61c202e..15b407b 100644
--- a/src/com/android/settings/dashboard/DashboardSummary.java
+++ b/src/com/android/settings/dashboard/DashboardSummary.java
@@ -65,6 +65,7 @@
private FocusRecyclerView mDashboard;
private DashboardAdapter mAdapter;
+ private DashboardAdapterV2 mAdapterV2;
private SummaryLoader mSummaryLoader;
private ConditionManager mConditionManager;
private LinearLayoutManager mLayoutManager;
@@ -175,8 +176,10 @@
super.onSaveInstanceState(outState);
if (mLayoutManager == null) return;
outState.putInt(EXTRA_SCROLL_POSITION, mLayoutManager.findFirstVisibleItemPosition());
- if (mAdapter != null) {
- mAdapter.onSaveInstanceState(outState);
+ if (!mDashboardFeatureProvider.useSuggestionUiV2()) {
+ if (mAdapter != null) {
+ mAdapter.onSaveInstanceState(outState);
+ }
}
}
@@ -194,11 +197,18 @@
mDashboard.setLayoutManager(mLayoutManager);
mDashboard.setHasFixedSize(true);
mDashboard.setListener(this);
- mAdapter = new DashboardAdapter(getContext(), bundle, mConditionManager.getConditions(),
- mSuggestionControllerMixin, this /* SuggestionDismissController.Callback */);
- mDashboard.setAdapter(mAdapter);
mDashboard.setItemAnimator(new DashboardItemAnimator());
- mSummaryLoader.setSummaryConsumer(mAdapter);
+ if (mDashboardFeatureProvider.useSuggestionUiV2()) {
+ mAdapterV2 = new DashboardAdapterV2(getContext(), bundle,
+ mConditionManager.getConditions(), mSuggestionControllerMixin, getLifecycle());
+ mDashboard.setAdapter(mAdapterV2);
+ mSummaryLoader.setSummaryConsumer(mAdapterV2);
+ } else {
+ mAdapter = new DashboardAdapter(getContext(), bundle, mConditionManager.getConditions(),
+ mSuggestionControllerMixin, this /* SuggestionDismissController.Callback */);
+ mDashboard.setAdapter(mAdapter);
+ mSummaryLoader.setSummaryConsumer(mAdapter);
+ }
ActionBarShadowController.attachToRecyclerView(
getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard);
rebuildUI();
@@ -237,7 +247,11 @@
if (mOnConditionsChangedCalled) {
final boolean scrollToTop =
mLayoutManager.findFirstCompletelyVisibleItemPosition() <= 1;
- mAdapter.setConditions(mConditionManager.getConditions());
+ if (mDashboardFeatureProvider.useSuggestionUiV2()) {
+ mAdapterV2.setConditions(mConditionManager.getConditions());
+ } else {
+ mAdapter.setConditions(mConditionManager.getConditions());
+ }
if (scrollToTop) {
mDashboard.scrollToPosition(0);
}
@@ -248,7 +262,11 @@
@Override
public Suggestion getSuggestionAt(int position) {
- return mAdapter.getSuggestion(position);
+ if (mDashboardFeatureProvider.useSuggestionUiV2()) {
+ return mAdapterV2.getSuggestion(position);
+ } else {
+ return mAdapter.getSuggestion(position);
+ }
}
@Override
@@ -259,11 +277,20 @@
@Override
public void onSuggestionReady(List<Suggestion> suggestions) {
mStagingSuggestions = suggestions;
- mAdapter.setSuggestions(suggestions);
- if (mStagingCategory != null) {
- Log.d(TAG, "Category has loaded, setting category from suggestionReady");
- mHandler.removeCallbacksAndMessages(null);
- mAdapter.setCategory(mStagingCategory);
+ if (mDashboardFeatureProvider.useSuggestionUiV2()) {
+ mAdapterV2.setSuggestions(suggestions);
+ if (mStagingCategory != null) {
+ Log.d(TAG, "Category has loaded, setting category from suggestionReady");
+ mHandler.removeCallbacksAndMessages(null);
+ mAdapterV2.setCategory(mStagingCategory);
+ }
+ } else {
+ mAdapter.setSuggestions(suggestions);
+ if (mStagingCategory != null) {
+ Log.d(TAG, "Category has loaded, setting category from suggestionReady");
+ mHandler.removeCallbacksAndMessages(null);
+ mAdapter.setCategory(mStagingCategory);
+ }
}
}
@@ -276,14 +303,26 @@
if (mSuggestionControllerMixin.isSuggestionLoaded()) {
Log.d(TAG, "Suggestion has loaded, setting suggestion/category");
ThreadUtils.postOnMainThread(() -> {
- if (mStagingSuggestions != null) {
- mAdapter.setSuggestions(mStagingSuggestions);
+ if (mDashboardFeatureProvider.useSuggestionUiV2()) {
+ if (mStagingSuggestions != null) {
+ mAdapterV2.setSuggestions(mStagingSuggestions);
+ }
+ mAdapterV2.setCategory(mStagingCategory);
+ } else {
+ if (mStagingSuggestions != null) {
+ mAdapter.setSuggestions(mStagingSuggestions);
+ }
+ mAdapter.setCategory(mStagingCategory);
}
- mAdapter.setCategory(mStagingCategory);
});
} else {
Log.d(TAG, "Suggestion NOT loaded, delaying setCategory by " + MAX_WAIT_MILLIS + "ms");
- mHandler.postDelayed(() -> mAdapter.setCategory(mStagingCategory), MAX_WAIT_MILLIS);
+ if (mDashboardFeatureProvider.useSuggestionUiV2()) {
+ mHandler.postDelayed(()
+ -> mAdapterV2.setCategory(mStagingCategory), MAX_WAIT_MILLIS);
+ } else {
+ mHandler.postDelayed(() -> mAdapter.setCategory(mStagingCategory), MAX_WAIT_MILLIS);
+ }
}
}
}
diff --git a/src/com/android/settings/dashboard/conditional/ConditionAdapterV2.java b/src/com/android/settings/dashboard/conditional/ConditionAdapterV2.java
new file mode 100644
index 0000000..3f3e5c9
--- /dev/null
+++ b/src/com/android/settings/dashboard/conditional/ConditionAdapterV2.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2018 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.dashboard.conditional;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.MetricsFeatureProvider;
+import com.android.settings.dashboard.DashboardAdapterV2.DashboardItemHolder;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.WirelessUtils;
+
+import java.util.List;
+import java.util.Objects;
+
+public class ConditionAdapterV2 extends RecyclerView.Adapter<DashboardItemHolder> {
+ public static final String TAG = "ConditionAdapter";
+
+ private final Context mContext;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+ private List<Condition> mConditions;
+ private boolean mExpanded;
+
+ private View.OnClickListener mConditionClickListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ //TODO: get rid of setTag/getTag
+ Condition condition = (Condition) v.getTag();
+ mMetricsFeatureProvider.action(mContext,
+ MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
+ condition.getMetricsConstant());
+ condition.onPrimaryClick();
+ }
+ };
+
+ @VisibleForTesting
+ ItemTouchHelper.SimpleCallback mSwipeCallback = new ItemTouchHelper.SimpleCallback(0,
+ ItemTouchHelper.START | ItemTouchHelper.END) {
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
+ RecyclerView.ViewHolder target) {
+ return true;
+ }
+
+ @Override
+ public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+ return viewHolder.getItemViewType() == R.layout.condition_tile
+ ? super.getSwipeDirs(recyclerView, viewHolder) : 0;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
+ Object item = getItem(viewHolder.getItemId());
+ // item can become null when running monkey
+ if (item != null) {
+ ((Condition) item).silence();
+ }
+ }
+ };
+
+ public ConditionAdapterV2(Context context, List<Condition> conditions, boolean expanded) {
+ mContext = context;
+ mConditions = conditions;
+ mExpanded = expanded;
+ mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
+
+ setHasStableIds(true);
+ }
+
+ public Object getItem(long itemId) {
+ for (Condition condition : mConditions) {
+ if (Objects.hash(condition.getTitle()) == itemId) {
+ return condition;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate(
+ viewType, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(DashboardItemHolder holder, int position) {
+ bindViews(mConditions.get(position), holder,
+ position == mConditions.size() - 1, mConditionClickListener);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return Objects.hash(mConditions.get(position).getTitle());
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return R.layout.condition_tile;
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mExpanded) {
+ return mConditions.size();
+ }
+ return 0;
+ }
+
+ public void addDismissHandling(final RecyclerView recyclerView) {
+ final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mSwipeCallback);
+ itemTouchHelper.attachToRecyclerView(recyclerView);
+ }
+
+ private void bindViews(final Condition condition,
+ DashboardItemHolder view, boolean isLastItem,
+ View.OnClickListener onClickListener) {
+ if (condition instanceof AirplaneModeCondition) {
+ Log.d(TAG, "Airplane mode condition has been bound with "
+ + "isActive=" + condition.isActive() + ". Airplane mode is currently " +
+ WirelessUtils.isAirplaneModeOn(condition.mManager.getContext()));
+ }
+ View card = view.itemView.findViewById(R.id.content);
+ card.setTag(condition);
+ card.setOnClickListener(onClickListener);
+ view.icon.setImageIcon(condition.getIcon());
+ view.title.setText(condition.getTitle());
+
+ CharSequence[] actions = condition.getActions();
+ final boolean hasButtons = actions.length > 0;
+ setViewVisibility(view.itemView, R.id.buttonBar, hasButtons);
+
+ view.summary.setText(condition.getSummary());
+ for (int i = 0; i < 2; i++) {
+ Button button = (Button) view.itemView.findViewById(i == 0
+ ? R.id.first_action : R.id.second_action);
+ if (actions.length > i) {
+ button.setVisibility(View.VISIBLE);
+ button.setText(actions[i]);
+ final int index = i;
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Context context = v.getContext();
+ FeatureFactory.getFactory(context).getMetricsFeatureProvider()
+ .action(context, MetricsEvent.ACTION_SETTINGS_CONDITION_BUTTON,
+ condition.getMetricsConstant());
+ condition.onActionClick(index);
+ }
+ });
+ } else {
+ button.setVisibility(View.GONE);
+ }
+ }
+ setViewVisibility(view.itemView, R.id.divider, !isLastItem);
+ }
+
+ private void setViewVisibility(View containerView, int viewId, boolean visible) {
+ View view = containerView.findViewById(viewId);
+ if (view != null) {
+ view.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+}
diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionAdapterV2.java b/src/com/android/settings/dashboard/suggestions/SuggestionAdapterV2.java
new file mode 100644
index 0000000..89c731f
--- /dev/null
+++ b/src/com/android/settings/dashboard/suggestions/SuggestionAdapterV2.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2018 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.dashboard.suggestions;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.service.settings.suggestions.Suggestion;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.MetricsFeatureProvider;
+import com.android.settings.dashboard.DashboardAdapterV2.DashboardItemHolder;
+import com.android.settings.dashboard.DashboardAdapterV2.IconCache;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class SuggestionAdapterV2 extends RecyclerView.Adapter<DashboardItemHolder> implements
+ LifecycleObserver, OnSaveInstanceState {
+ public static final String TAG = "SuggestionAdapterV2";
+
+ private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged";
+ private static final String STATE_SUGGESTION_LIST = "suggestion_list";
+
+ private final Context mContext;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+ private final IconCache mCache;
+ private final ArrayList<String> mSuggestionsShownLogged;
+ private final SuggestionControllerMixin mSuggestionControllerMixin;
+ private final Callback mCallback;
+ private final int mMultipleCardsMarginEnd;
+ private final int mWidthSingleCard;
+ private final int mWidthTwoCards;
+ private final int mWidthMultipleCards;
+
+ private List<Suggestion> mSuggestions;
+
+ public interface Callback {
+ /**
+ * Called when the close button of the suggestion card is clicked.
+ */
+ void onSuggestionClosed(Suggestion suggestion);
+ }
+
+ public SuggestionAdapterV2(Context context, SuggestionControllerMixin suggestionControllerMixin,
+ Bundle savedInstanceState, Callback callback, Lifecycle lifecycle) {
+ mContext = context;
+ mSuggestionControllerMixin = suggestionControllerMixin;
+ mCache = new IconCache(context);
+ final FeatureFactory factory = FeatureFactory.getFactory(context);
+ mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
+ mCallback = callback;
+ if (savedInstanceState != null) {
+ mSuggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
+ mSuggestionsShownLogged = savedInstanceState.getStringArrayList(
+ STATE_SUGGESTIONS_SHOWN_LOGGED);
+ } else {
+ mSuggestionsShownLogged = new ArrayList<>();
+ }
+
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+
+ final Resources res = mContext.getResources();
+ mMultipleCardsMarginEnd = res.getDimensionPixelOffset(R.dimen.suggestion_card_margin_end);
+ mWidthSingleCard = res.getDimensionPixelOffset(R.dimen.suggestion_card_width_one_card);
+ mWidthTwoCards = res.getDimensionPixelOffset(R.dimen.suggestion_card_width_two_cards);
+ mWidthMultipleCards =
+ res.getDimensionPixelOffset(R.dimen.suggestion_card_width_multiple_cards);
+
+ setHasStableIds(true);
+ }
+
+ @Override
+ public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate(
+ viewType, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(DashboardItemHolder holder, int position) {
+ final Suggestion suggestion = mSuggestions.get(position);
+ final String id = suggestion.getId();
+ final int suggestionCount = mSuggestions.size();
+ if (!mSuggestionsShownLogged.contains(id)) {
+ mMetricsFeatureProvider.action(
+ mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, id);
+ mSuggestionsShownLogged.add(id);
+ }
+ setCardWidthAndMargin(holder, suggestionCount);
+ holder.icon.setImageDrawable(mCache.getIcon(suggestion.getIcon()));
+ holder.title.setText(suggestion.getTitle());
+ holder.title.setSingleLine(suggestionCount == 1);
+
+ if (suggestionCount == 1) {
+ final CharSequence summary = suggestion.getSummary();
+ if (!TextUtils.isEmpty(summary)) {
+ holder.summary.setText(summary);
+ holder.summary.setVisibility(View.VISIBLE);
+ } else {
+ holder.summary.setVisibility(View.GONE);
+ }
+ } else {
+ // Do not show summary if there are more than 1 suggestions
+ holder.summary.setVisibility(View.GONE);
+ holder.title.setMaxLines(3);
+ }
+
+ final ImageView closeButton = holder.itemView.findViewById(R.id.close_button);
+ if (closeButton != null) {
+ if (mCallback != null) {
+ closeButton.setOnClickListener(v -> {
+ mCallback.onSuggestionClosed(suggestion);
+ });
+ } else {
+ closeButton.setOnClickListener(null);
+ }
+ }
+
+ View clickHandler = holder.itemView;
+ // If a view with @android:id/primary is defined, use that as the click handler
+ // instead.
+ final View primaryAction = holder.itemView.findViewById(android.R.id.primary);
+ if (primaryAction != null) {
+ clickHandler = primaryAction;
+ }
+ clickHandler.setOnClickListener(v -> {
+ mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_SUGGESTION, id);
+ try {
+ suggestion.getPendingIntent().send();
+ mSuggestionControllerMixin.launchSuggestion(suggestion);
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Failed to start suggestion " + suggestion.getTitle());
+ }
+ });
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return Objects.hash(mSuggestions.get(position).getId());
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ final Suggestion suggestion = getSuggestion(position);
+ if ((suggestion.getFlags() & Suggestion.FLAG_HAS_BUTTON) != 0) {
+ return R.layout.suggestion_tile_with_button_v2;
+ } else {
+ return R.layout.suggestion_tile_v2;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSuggestions.size();
+ }
+
+ public Suggestion getSuggestion(int position) {
+ final long itemId = getItemId(position);
+ if (mSuggestions == null) {
+ return null;
+ }
+ for (Suggestion suggestion : mSuggestions) {
+ if (Objects.hash(suggestion.getId()) == itemId) {
+ return suggestion;
+ }
+ }
+ return null;
+ }
+
+ public void removeSuggestion(Suggestion suggestion) {
+ final int position = mSuggestions.indexOf(suggestion);
+ mSuggestions.remove(suggestion);
+ notifyItemRemoved(position);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mSuggestions != null) {
+ outState.putParcelableArrayList(STATE_SUGGESTION_LIST,
+ new ArrayList<>(mSuggestions));
+ }
+ outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged);
+ }
+
+ public void setSuggestions(List<Suggestion> suggestions) {
+ mSuggestions = suggestions;
+ }
+
+ public List<Suggestion> getSuggestions() {
+ return mSuggestions;
+ }
+
+ private void setCardWidthAndMargin(DashboardItemHolder holder, int suggestionCount) {
+ final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ suggestionCount == 1
+ ? mWidthSingleCard : suggestionCount == 2 ? mWidthTwoCards : mWidthMultipleCards,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ params.setMarginEnd(suggestionCount == 1 ? 0 : mMultipleCardsMarginEnd);
+ holder.itemView.setLayoutParams(params);
+ }
+}
diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionDismissController.java b/src/com/android/settings/dashboard/suggestions/SuggestionDismissController.java
index de0c129..db2d0bb 100644
--- a/src/com/android/settings/dashboard/suggestions/SuggestionDismissController.java
+++ b/src/com/android/settings/dashboard/suggestions/SuggestionDismissController.java
@@ -24,6 +24,10 @@
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
+/**
+ * Deprecated as a close button is provided to dismiss the suggestion.
+ */
+@Deprecated
public class SuggestionDismissController extends ItemTouchHelper.SimpleCallback {
public interface Callback {
diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardAdapterV2Test.java b/tests/robotests/src/com/android/settings/dashboard/DashboardAdapterV2Test.java
new file mode 100644
index 0000000..801a8e4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/dashboard/DashboardAdapterV2Test.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2018 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.dashboard;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Icon;
+import android.service.settings.suggestions.Suggestion;
+import android.support.v7.widget.RecyclerView;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.TestConfig;
+import com.android.settings.dashboard.conditional.Condition;
+import com.android.settings.dashboard.suggestions.SuggestionAdapterV2;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.shadow.SettingsShadowResources;
+import com.android.settingslib.drawer.DashboardCategory;
+import com.android.settingslib.drawer.Tile;
+
+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 java.util.ArrayList;
+import java.util.List;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH,
+ sdk = TestConfig.SDK_VERSION,
+ shadows = {
+ SettingsShadowResources.class,
+ SettingsShadowResources.SettingsShadowTheme.class,
+ })
+public class DashboardAdapterV2Test {
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private SettingsActivity mContext;
+ @Mock
+ private View mView;
+ @Mock
+ private Condition mCondition;
+ @Mock
+ private Resources mResources;
+ private FakeFeatureFactory mFactory;
+ private DashboardAdapterV2 mDashboardAdapter;
+ private List<Condition> mConditionList;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mFactory = FakeFeatureFactory.setupForTest();
+ when(mFactory.dashboardFeatureProvider.shouldTintIcon()).thenReturn(true);
+
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getQuantityString(any(int.class), any(int.class), any()))
+ .thenReturn("");
+
+ mConditionList = new ArrayList<>();
+ mConditionList.add(mCondition);
+ when(mCondition.shouldShow()).thenReturn(true);
+ mDashboardAdapter = new DashboardAdapterV2(mContext, null /* savedInstanceState */,
+ mConditionList, null /* suggestionControllerMixin */, null /* lifecycle */);
+ when(mView.getTag()).thenReturn(mCondition);
+ }
+
+ @Test
+ public void testSuggestionDismissed_notOnlySuggestion_updateSuggestionOnly() {
+ final DashboardAdapterV2 adapter =
+ spy(new DashboardAdapterV2(mContext, null /* savedInstanceState */,
+ null /* conditions */, null /* suggestionControllerMixin */, null /* lifecycle */));
+ final List<Suggestion> suggestions = makeSuggestionsV2("pkg1", "pkg2", "pkg3");
+ adapter.setSuggestions(suggestions);
+
+ final RecyclerView data = mock(RecyclerView.class);
+ when(data.getResources()).thenReturn(mResources);
+ when(data.getContext()).thenReturn(mContext);
+ when(mResources.getDisplayMetrics()).thenReturn(mock(DisplayMetrics.class));
+ final View itemView = mock(View.class);
+ when(itemView.findViewById(R.id.suggestion_list)).thenReturn(data);
+ when(itemView.findViewById(android.R.id.summary)).thenReturn(mock(TextView.class));
+ final DashboardAdapterV2.SuggestionContainerHolder holder =
+ new DashboardAdapterV2.SuggestionContainerHolder(itemView);
+
+ adapter.onBindSuggestion(holder, 0);
+
+ final DashboardDataV2 dashboardData = adapter.mDashboardData;
+ reset(adapter); // clear interactions tracking
+
+ final Suggestion suggestionToRemove = suggestions.get(1);
+ adapter.onSuggestionClosed(suggestionToRemove);
+
+ assertThat(adapter.mDashboardData).isEqualTo(dashboardData);
+ assertThat(suggestions.size()).isEqualTo(2);
+ assertThat(suggestions.contains(suggestionToRemove)).isFalse();
+ verify(adapter, never()).notifyDashboardDataChanged(any());
+ }
+
+ @Test
+ public void testSuggestionDismissed_moreThanTwoSuggestions_shouldNotCrash() {
+ final RecyclerView data = new RecyclerView(RuntimeEnvironment.application);
+ final View itemView = mock(View.class);
+ when(itemView.findViewById(R.id.suggestion_list)).thenReturn(data);
+ when(itemView.findViewById(android.R.id.summary)).thenReturn(mock(TextView.class));
+ final DashboardAdapterV2.SuggestionContainerHolder holder =
+ new DashboardAdapterV2.SuggestionContainerHolder(itemView);
+ final List<Suggestion> suggestions = makeSuggestionsV2("pkg1", "pkg2", "pkg3", "pkg4");
+ final DashboardAdapterV2 adapter = spy(new DashboardAdapterV2(mContext,
+ null /*savedInstance */, null /* conditions */, null /* suggestionControllerMixin */,
+ null /* lifecycle */));
+ adapter.setSuggestions(suggestions);
+ adapter.onBindSuggestion(holder, 0);
+
+ adapter.onSuggestionClosed(suggestions.get(1));
+
+ // verify operations that access the lists will not cause ConcurrentModificationException
+ assertThat(holder.data.getAdapter().getItemCount()).isEqualTo(3);
+ adapter.setSuggestions(suggestions);
+ // should not crash
+ }
+
+ @Test
+ public void testSuggestionDismissed_onlySuggestion_updateDashboardData() {
+ DashboardAdapterV2 adapter =
+ spy(new DashboardAdapterV2(mContext, null /* savedInstanceState */,
+ null /* conditions */, null /* suggestionControllerMixin */, null /* lifecycle */));
+ final List<Suggestion> suggestions = makeSuggestionsV2("pkg1");
+ adapter.setSuggestions(suggestions);
+ final DashboardDataV2 dashboardData = adapter.mDashboardData;
+ reset(adapter); // clear interactions tracking
+
+ adapter.onSuggestionClosed(suggestions.get(0));
+
+ assertThat(adapter.mDashboardData).isNotEqualTo(dashboardData);
+ verify(adapter).notifyDashboardDataChanged(any());
+ }
+
+ @Test
+ public void testSetCategories_iconTinted() {
+ TypedArray mockTypedArray = mock(TypedArray.class);
+ doReturn(mockTypedArray).when(mContext).obtainStyledAttributes(any(int[].class));
+ doReturn(0x89000000).when(mockTypedArray).getColor(anyInt(), anyInt());
+
+ final DashboardCategory category = new DashboardCategory();
+ final Icon mockIcon = mock(Icon.class);
+ final Tile tile = new Tile();
+ tile.isIconTintable = true;
+ tile.icon = mockIcon;
+ category.addTile(tile);
+
+ mDashboardAdapter.setCategory(category);
+
+ verify(mockIcon).setTint(eq(0x89000000));
+ }
+
+ @Test
+ public void testBindSuggestion_shouldSetSuggestionAdapterAndNoCrash() {
+ mDashboardAdapter = new DashboardAdapterV2(mContext, null /* savedInstanceState */,
+ null /* conditions */, null /* suggestionControllerMixin */, null /* lifecycle */);
+ final List<Suggestion> suggestions = makeSuggestionsV2("pkg1");
+
+ mDashboardAdapter.setSuggestions(suggestions);
+
+ final RecyclerView data = mock(RecyclerView.class);
+ when(data.getResources()).thenReturn(mResources);
+ when(data.getContext()).thenReturn(mContext);
+ when(mResources.getDisplayMetrics()).thenReturn(mock(DisplayMetrics.class));
+ final View itemView = mock(View.class);
+ when(itemView.findViewById(R.id.suggestion_list)).thenReturn(data);
+ when(itemView.findViewById(android.R.id.summary)).thenReturn(mock(TextView.class));
+ final DashboardAdapterV2.SuggestionContainerHolder holder =
+ new DashboardAdapterV2.SuggestionContainerHolder(itemView);
+
+ mDashboardAdapter.onBindSuggestion(holder, 0);
+
+ verify(data).setAdapter(any(SuggestionAdapterV2.class));
+ // should not crash
+ }
+
+ @Test
+ public void testBindSuggestion_shouldSetSummary() {
+ mDashboardAdapter = new DashboardAdapterV2(mContext, null /* savedInstanceState */,
+ null /* conditions */, null /* suggestionControllerMixin */, null /* lifecycle */);
+ final List<Suggestion> suggestions = makeSuggestionsV2("pkg1");
+
+ mDashboardAdapter.setSuggestions(suggestions);
+
+ final RecyclerView data = mock(RecyclerView.class);
+ when(data.getResources()).thenReturn(mResources);
+ when(data.getContext()).thenReturn(mContext);
+ when(mResources.getDisplayMetrics()).thenReturn(mock(DisplayMetrics.class));
+ final View itemView = mock(View.class);
+ when(itemView.findViewById(R.id.suggestion_list)).thenReturn(data);
+ final TextView summary = mock(TextView.class);
+ when(itemView.findViewById(android.R.id.summary)).thenReturn(summary);
+ final DashboardAdapterV2.SuggestionContainerHolder holder =
+ new DashboardAdapterV2.SuggestionContainerHolder(itemView);
+
+ mDashboardAdapter.onBindSuggestion(holder, 0);
+
+ verify(summary).setText("1");
+
+ suggestions.addAll(makeSuggestionsV2("pkg2", "pkg3", "pkg4"));
+ mDashboardAdapter.setSuggestions(suggestions);
+
+ mDashboardAdapter.onBindSuggestion(holder, 0);
+
+ verify(summary).setText("4");
+ }
+
+ private List<Suggestion> makeSuggestionsV2(String... pkgNames) {
+ final List<Suggestion> suggestions = new ArrayList<>();
+ for (String pkgName : pkgNames) {
+ final Suggestion suggestion = new Suggestion.Builder(pkgName)
+ .setPendingIntent(mock(PendingIntent.class))
+ .build();
+ suggestions.add(suggestion);
+ }
+ return suggestions;
+ }
+
+ private void setupSuggestions(List<Suggestion> suggestions) {
+ final Context context = RuntimeEnvironment.application;
+ mDashboardAdapter.setSuggestions(suggestions);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/dashboard/conditional/ConditionAdapterV2Test.java b/tests/robotests/src/com/android/settings/dashboard/conditional/ConditionAdapterV2Test.java
new file mode 100644
index 0000000..5e0ecec
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/dashboard/conditional/ConditionAdapterV2Test.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 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.dashboard.conditional;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.settings.R;
+import com.android.settings.TestConfig;
+import com.android.settings.dashboard.DashboardAdapterV2;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class ConditionAdapterV2Test {
+ @Mock
+ private Condition mCondition1;
+ @Mock
+ private Condition mCondition2;
+
+ private Context mContext;
+ private ConditionAdapterV2 mConditionAdapter;
+ private List<Condition> mOneCondition;
+ private List<Condition> mTwoConditions;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ final CharSequence[] actions = new CharSequence[2];
+ when(mCondition1.getActions()).thenReturn(actions);
+ when(mCondition1.shouldShow()).thenReturn(true);
+ mOneCondition = new ArrayList<>();
+ mOneCondition.add(mCondition1);
+ mTwoConditions = new ArrayList<>();
+ mTwoConditions.add(mCondition1);
+ mTwoConditions.add(mCondition2);
+ }
+
+ @Test
+ public void getItemCount_notExpanded_shouldReturn0() {
+ mConditionAdapter = new ConditionAdapterV2(mContext, mOneCondition, false);
+ assertThat(mConditionAdapter.getItemCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void getItemCount_expanded_shouldReturnListSize() {
+ mConditionAdapter = new ConditionAdapterV2(mContext, mOneCondition, true);
+ assertThat(mConditionAdapter.getItemCount()).isEqualTo(1);
+
+ mConditionAdapter = new ConditionAdapterV2(mContext, mTwoConditions, true);
+ assertThat(mConditionAdapter.getItemCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void getItemViewType_shouldReturnConditionTile() {
+ mConditionAdapter = new ConditionAdapterV2(mContext, mTwoConditions, true);
+ assertThat(mConditionAdapter.getItemViewType(0)).isEqualTo(R.layout.condition_tile);
+ }
+
+ @Test
+ public void onBindViewHolder_shouldSetListener() {
+ final View view = LayoutInflater.from(mContext).inflate(
+ R.layout.condition_tile, new LinearLayout(mContext), true);
+ final DashboardAdapterV2.DashboardItemHolder viewHolder =
+ new DashboardAdapterV2.DashboardItemHolder(view);
+ mConditionAdapter = new ConditionAdapterV2(mContext, mOneCondition, true);
+
+ mConditionAdapter.onBindViewHolder(viewHolder, 0);
+ final View card = view.findViewById(R.id.content);
+ assertThat(card.hasOnClickListeners()).isTrue();
+ }
+
+ @Test
+ public void viewClick_shouldInvokeConditionPrimaryClick() {
+ final View view = LayoutInflater.from(mContext).inflate(
+ R.layout.condition_tile, new LinearLayout(mContext), true);
+ final DashboardAdapterV2.DashboardItemHolder viewHolder =
+ new DashboardAdapterV2.DashboardItemHolder(view);
+ mConditionAdapter = new ConditionAdapterV2(mContext, mOneCondition, true);
+
+ mConditionAdapter.onBindViewHolder(viewHolder, 0);
+ final View card = view.findViewById(R.id.content);
+ card.performClick();
+ verify(mCondition1).onPrimaryClick();
+ }
+
+ @Test
+ public void onSwiped_nullCondition_shouldNotCrash() {
+ final RecyclerView recyclerView = new RecyclerView(mContext);
+ final View view = LayoutInflater.from(mContext).inflate(
+ R.layout.condition_tile, new LinearLayout(mContext), true);
+ final DashboardAdapterV2.DashboardItemHolder viewHolder =
+ new DashboardAdapterV2.DashboardItemHolder(view);
+ mConditionAdapter = new ConditionAdapterV2(mContext, mOneCondition, true);
+ mConditionAdapter.addDismissHandling(recyclerView);
+
+ // do not bind viewholder to simulate the null condition scenario
+ mConditionAdapter.mSwipeCallback.onSwiped(viewHolder, 0);
+ // no crash
+ }
+
+}
diff --git a/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionAdapterTest.java b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionAdapterTest.java
index 26940d6..2ecab8d 100644
--- a/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionAdapterTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionAdapterTest.java
@@ -36,7 +36,6 @@
import com.android.settings.dashboard.DashboardAdapter;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
-import com.android.settingslib.drawer.Tile;
import org.junit.Before;
import org.junit.Test;
@@ -62,10 +61,8 @@
private Context mContext;
private SuggestionAdapter mSuggestionAdapter;
private DashboardAdapter.DashboardItemHolder mSuggestionHolder;
- private List<Tile> mOneSuggestion;
- private List<Tile> mTwoSuggestions;
- private List<Suggestion> mOneSuggestionV2;
- private List<Suggestion> mTwoSuggestionsV2;
+ private List<Suggestion> mOneSuggestion;
+ private List<Suggestion> mTwoSuggestions;
@Before
public void setUp() {
@@ -73,45 +70,34 @@
mContext = RuntimeEnvironment.application;
mFeatureFactory = FakeFeatureFactory.setupForTest();
- final Tile suggestion1 = new Tile();
- final Tile suggestion2 = new Tile();
- final Suggestion suggestion1V2 = new Suggestion.Builder("id1")
+ final Suggestion suggestion1 = new Suggestion.Builder("id1")
.setTitle("Test suggestion 1")
.build();
- final Suggestion suggestion2V2 = new Suggestion.Builder("id2")
+ final Suggestion suggestion2 = new Suggestion.Builder("id2")
.setTitle("Test suggestion 2")
.build();
- suggestion1.title = "Test Suggestion 1";
- suggestion1.icon = mock(Icon.class);
- suggestion2.title = "Test Suggestion 2";
- suggestion2.icon = mock(Icon.class);
mOneSuggestion = new ArrayList<>();
mOneSuggestion.add(suggestion1);
mTwoSuggestions = new ArrayList<>();
mTwoSuggestions.add(suggestion1);
mTwoSuggestions.add(suggestion2);
- mOneSuggestionV2 = new ArrayList<>();
- mOneSuggestionV2.add(suggestion1V2);
- mTwoSuggestionsV2 = new ArrayList<>();
- mTwoSuggestionsV2.add(suggestion1V2);
- mTwoSuggestionsV2.add(suggestion2V2);
}
@Test
public void getItemCount_shouldReturnListSize() {
mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin,
- mOneSuggestionV2, new ArrayList<>());
+ mOneSuggestion, new ArrayList<>());
assertThat(mSuggestionAdapter.getItemCount()).isEqualTo(1);
mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin,
- mTwoSuggestionsV2, new ArrayList<>());
+ mTwoSuggestions, new ArrayList<>());
assertThat(mSuggestionAdapter.getItemCount()).isEqualTo(2);
}
@Test
public void getItemViewType_shouldReturnSuggestionTile() {
mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin,
- mOneSuggestionV2, new ArrayList<>());
+ mOneSuggestion, new ArrayList<>());
assertThat(mSuggestionAdapter.getItemViewType(0))
.isEqualTo(R.layout.suggestion_tile);
}
@@ -137,7 +123,7 @@
R.layout.suggestion_tile, new LinearLayout(mContext), true));
mSuggestionHolder = new DashboardAdapter.DashboardItemHolder(view);
mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin,
- mOneSuggestionV2, new ArrayList<>());
+ mOneSuggestion, new ArrayList<>());
// Bind twice
mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0);
@@ -146,13 +132,13 @@
// Log once
verify(mFeatureFactory.metricsFeatureProvider).action(
mContext, MetricsProto.MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION,
- mOneSuggestionV2.get(0).getId());
+ mOneSuggestion.get(0).getId());
}
@Test
public void onBindViewHolder_itemViewShouldHandleClick()
throws PendingIntent.CanceledException {
- final List<Suggestion> suggestions = makeSuggestionsV2("pkg1");
+ final List<Suggestion> suggestions = makeSuggestions("pkg1");
setupSuggestions(mActivity, suggestions);
mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0);
@@ -164,21 +150,21 @@
@Test
public void getSuggestions_shouldReturnSuggestionWhenMatch() {
- final List<Suggestion> suggestionsV2 = makeSuggestionsV2("pkg1");
- setupSuggestions(mActivity, suggestionsV2);
+ final List<Suggestion> suggestions = makeSuggestions("pkg1");
+ setupSuggestions(mActivity, suggestions);
assertThat(mSuggestionAdapter.getSuggestion(0)).isNotNull();
}
- private void setupSuggestions(Context context, List<Suggestion> suggestionsV2) {
+ private void setupSuggestions(Context context, List<Suggestion> suggestions) {
mSuggestionAdapter = new SuggestionAdapter(context, mSuggestionControllerMixin,
- suggestionsV2, new ArrayList<>());
+ suggestions, new ArrayList<>());
mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder(
new FrameLayout(RuntimeEnvironment.application),
mSuggestionAdapter.getItemViewType(0));
}
- private List<Suggestion> makeSuggestionsV2(String... pkgNames) {
+ private List<Suggestion> makeSuggestions(String... pkgNames) {
final List<Suggestion> suggestions = new ArrayList<>();
for (String pkgName : pkgNames) {
final Suggestion suggestion = new Suggestion.Builder(pkgName)
diff --git a/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionAdapterV2Test.java b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionAdapterV2Test.java
new file mode 100644
index 0000000..2297f07
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionAdapterV2Test.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2018 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.dashboard.suggestions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.service.settings.suggestions.Suggestion;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.TestConfig;
+import com.android.settings.dashboard.DashboardAdapterV2;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+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 java.util.ArrayList;
+import java.util.List;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SuggestionAdapterV2Test {
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private SettingsActivity mActivity;
+ @Mock
+ private SuggestionControllerMixin mSuggestionControllerMixin;
+ private FakeFeatureFactory mFeatureFactory;
+ private Context mContext;
+ private SuggestionAdapterV2 mSuggestionAdapter;
+ private DashboardAdapterV2.DashboardItemHolder mSuggestionHolder;
+ private List<Suggestion> mOneSuggestion;
+ private List<Suggestion> mTwoSuggestions;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+
+ final Suggestion suggestion1 = new Suggestion.Builder("id1")
+ .setTitle("Test suggestion 1")
+ .build();
+ final Suggestion suggestion2 = new Suggestion.Builder("id2")
+ .setTitle("Test suggestion 2")
+ .build();
+ mOneSuggestion = new ArrayList<>();
+ mOneSuggestion.add(suggestion1);
+ mTwoSuggestions = new ArrayList<>();
+ mTwoSuggestions.add(suggestion1);
+ mTwoSuggestions.add(suggestion2);
+ }
+
+ @Test
+ public void getItemCount_shouldReturnListSize() {
+ mSuggestionAdapter = new SuggestionAdapterV2(mContext, mSuggestionControllerMixin,
+ null /* savedInstanceState */, null /* callback */, null /* lifecycle */);
+ mSuggestionAdapter.setSuggestions(mOneSuggestion);
+ assertThat(mSuggestionAdapter.getItemCount()).isEqualTo(1);
+
+ mSuggestionAdapter.setSuggestions(mTwoSuggestions);
+ assertThat(mSuggestionAdapter.getItemCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void getItemViewType_shouldReturnSuggestionTile() {
+ mSuggestionAdapter = new SuggestionAdapterV2(mContext, mSuggestionControllerMixin,
+ null /* savedInstanceState */, null /* callback */, null /* lifecycle */);
+ mSuggestionAdapter.setSuggestions(mOneSuggestion);
+ assertThat(mSuggestionAdapter.getItemViewType(0))
+ .isEqualTo(R.layout.suggestion_tile_v2);
+ }
+
+ @Test
+ public void getItemType_hasButton_shouldReturnSuggestionWithButton() {
+ final List<Suggestion> suggestions = new ArrayList<>();
+ suggestions.add(new Suggestion.Builder("id")
+ .setFlags(Suggestion.FLAG_HAS_BUTTON)
+ .setTitle("123")
+ .setSummary("456")
+ .build());
+ mSuggestionAdapter = new SuggestionAdapterV2(mContext, mSuggestionControllerMixin,
+ null /* savedInstanceState */, null /* callback */, null /* lifecycle */);
+ mSuggestionAdapter.setSuggestions(suggestions);
+
+ assertThat(mSuggestionAdapter.getItemViewType(0))
+ .isEqualTo(R.layout.suggestion_tile_with_button_v2);
+ }
+
+ @Test
+ public void onBindViewHolder_shouldLog() {
+ final View view = spy(LayoutInflater.from(mContext).inflate(
+ R.layout.suggestion_tile, new LinearLayout(mContext), true));
+ mSuggestionHolder = new DashboardAdapterV2.DashboardItemHolder(view);
+ mSuggestionAdapter = new SuggestionAdapterV2(mContext, mSuggestionControllerMixin,
+ null /* savedInstanceState */, null /* callback */, null /* lifecycle */);
+ mSuggestionAdapter.setSuggestions(mOneSuggestion);
+
+ // Bind twice
+ mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0);
+ mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0);
+
+ // Log once
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ mContext, MetricsProto.MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION,
+ mOneSuggestion.get(0).getId());
+ }
+
+ @Test
+ public void onBindViewHolder_itemViewShouldHandleClick()
+ throws PendingIntent.CanceledException {
+ final List<Suggestion> suggestions = makeSuggestions("pkg1");
+ setupSuggestions(mActivity, suggestions);
+
+ mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0);
+ mSuggestionHolder.itemView.performClick();
+
+ verify(mSuggestionControllerMixin).launchSuggestion(suggestions.get(0));
+ verify(suggestions.get(0).getPendingIntent()).send();
+ }
+
+ @Test
+ public void onBindViewHolder_hasButton_buttonShouldHandleClick()
+ throws PendingIntent.CanceledException {
+ final List<Suggestion> suggestions = new ArrayList<>();
+ final PendingIntent pendingIntent = mock(PendingIntent.class);
+ suggestions.add(new Suggestion.Builder("id")
+ .setFlags(Suggestion.FLAG_HAS_BUTTON)
+ .setTitle("123")
+ .setSummary("456")
+ .setPendingIntent(pendingIntent)
+ .build());
+ mSuggestionAdapter = new SuggestionAdapterV2(mContext, mSuggestionControllerMixin,
+ null /* savedInstanceState */, null /* callback */, null /* lifecycle */);
+ mSuggestionAdapter.setSuggestions(suggestions);
+ mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder(
+ new FrameLayout(RuntimeEnvironment.application),
+ mSuggestionAdapter.getItemViewType(0));
+
+ mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0);
+ mSuggestionHolder.itemView.findViewById(android.R.id.primary).performClick();
+
+ verify(mSuggestionControllerMixin).launchSuggestion(suggestions.get(0));
+ verify(pendingIntent).send();
+ }
+
+ @Test
+ public void getSuggestions_shouldReturnSuggestionWhenMatch() {
+ final List<Suggestion> suggestions = makeSuggestions("pkg1");
+ setupSuggestions(mActivity, suggestions);
+
+ assertThat(mSuggestionAdapter.getSuggestion(0)).isNotNull();
+ }
+
+ @Test
+ public void onBindViewHolder_closeButtonShouldHandleClick()
+ throws PendingIntent.CanceledException {
+ final List<Suggestion> suggestions = makeSuggestions("pkg1");
+ final SuggestionAdapterV2.Callback callback = mock(SuggestionAdapterV2.Callback.class);
+ mSuggestionAdapter = new SuggestionAdapterV2(mActivity, mSuggestionControllerMixin,
+ null /* savedInstanceState */, callback, null /* lifecycle */);
+ mSuggestionAdapter.setSuggestions(suggestions);
+ mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder(
+ new FrameLayout(RuntimeEnvironment.application),
+ mSuggestionAdapter.getItemViewType(0));
+
+ mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0);
+ mSuggestionHolder.itemView.findViewById(R.id.close_button).performClick();
+
+ verify(callback).onSuggestionClosed(suggestions.get(0));
+ }
+
+ private void setupSuggestions(Context context, List<Suggestion> suggestions) {
+ mSuggestionAdapter = new SuggestionAdapterV2(context, mSuggestionControllerMixin,
+ null /* savedInstanceState */, null /* callback */, null /* lifecycle */);
+ mSuggestionAdapter.setSuggestions(suggestions);
+ mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder(
+ new FrameLayout(RuntimeEnvironment.application),
+ mSuggestionAdapter.getItemViewType(0));
+ }
+
+ private List<Suggestion> makeSuggestions(String... pkgNames) {
+ final List<Suggestion> suggestions = new ArrayList<>();
+ for (String pkgName : pkgNames) {
+ final Suggestion suggestion = new Suggestion.Builder(pkgName)
+ .setPendingIntent(mock(PendingIntent.class))
+ .build();
+ suggestions.add(suggestion);
+ }
+ return suggestions;
+ }
+}