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
This commit is contained in:
Doris Ling
2018-01-10 17:26:48 -08:00
parent d6b0490dea
commit a213bf0839
22 changed files with 2333 additions and 46 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8400,6 +8400,9 @@
<!-- Summary for the condition section on the dashboard, representing number of conditions. [CHAR LIMIT=10] -->
<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>

View File

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

View File

@@ -27,4 +27,5 @@ public class FeatureFlags {
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";
}

View File

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

View File

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

View File

@@ -88,4 +88,9 @@ public interface DashboardFeatureProvider {
*/
void openTileIntent(Activity activity, Tile tile);
/**
* Whether or not we should use the v2 of suggestions UI.
*/
boolean useSuggestionUiV2();
}

View File

@@ -33,12 +33,14 @@ import android.support.annotation.VisibleForTesting;
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 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
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);

View File

@@ -65,6 +65,7 @@ public class DashboardSummary extends InstrumentedFragment
private FocusRecyclerView mDashboard;
private DashboardAdapter mAdapter;
private DashboardAdapterV2 mAdapterV2;
private SummaryLoader mSummaryLoader;
private ConditionManager mConditionManager;
private LinearLayoutManager mLayoutManager;
@@ -175,10 +176,12 @@ public class DashboardSummary extends InstrumentedFragment
super.onSaveInstanceState(outState);
if (mLayoutManager == null) return;
outState.putInt(EXTRA_SCROLL_POSITION, mLayoutManager.findFirstVisibleItemPosition());
if (!mDashboardFeatureProvider.useSuggestionUiV2()) {
if (mAdapter != null) {
mAdapter.onSaveInstanceState(outState);
}
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
@@ -194,11 +197,18 @@ public class DashboardSummary extends InstrumentedFragment
mDashboard.setLayoutManager(mLayoutManager);
mDashboard.setHasFixedSize(true);
mDashboard.setListener(this);
mDashboard.setItemAnimator(new DashboardItemAnimator());
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);
mDashboard.setItemAnimator(new DashboardItemAnimator());
mSummaryLoader.setSummaryConsumer(mAdapter);
}
ActionBarShadowController.attachToRecyclerView(
getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard);
rebuildUI();
@@ -237,7 +247,11 @@ public class DashboardSummary extends InstrumentedFragment
if (mOnConditionsChangedCalled) {
final boolean scrollToTop =
mLayoutManager.findFirstCompletelyVisibleItemPosition() <= 1;
if (mDashboardFeatureProvider.useSuggestionUiV2()) {
mAdapterV2.setConditions(mConditionManager.getConditions());
} else {
mAdapter.setConditions(mConditionManager.getConditions());
}
if (scrollToTop) {
mDashboard.scrollToPosition(0);
}
@@ -248,8 +262,12 @@ public class DashboardSummary extends InstrumentedFragment
@Override
public Suggestion getSuggestionAt(int position) {
if (mDashboardFeatureProvider.useSuggestionUiV2()) {
return mAdapterV2.getSuggestion(position);
} else {
return mAdapter.getSuggestion(position);
}
}
@Override
public void onSuggestionDismissed(Suggestion suggestion) {
@@ -259,6 +277,14 @@ public class DashboardSummary extends InstrumentedFragment
@Override
public void onSuggestionReady(List<Suggestion> suggestions) {
mStagingSuggestions = suggestions;
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");
@@ -266,6 +292,7 @@ public class DashboardSummary extends InstrumentedFragment
mAdapter.setCategory(mStagingCategory);
}
}
}
@WorkerThread
void updateCategory() {
@@ -276,14 +303,26 @@ public class DashboardSummary extends InstrumentedFragment
if (mSuggestionControllerMixin.isSuggestionLoaded()) {
Log.d(TAG, "Suggestion has loaded, setting suggestion/category");
ThreadUtils.postOnMainThread(() -> {
if (mDashboardFeatureProvider.useSuggestionUiV2()) {
if (mStagingSuggestions != null) {
mAdapterV2.setSuggestions(mStagingSuggestions);
}
mAdapterV2.setCategory(mStagingCategory);
} else {
if (mStagingSuggestions != null) {
mAdapter.setSuggestions(mStagingSuggestions);
}
mAdapter.setCategory(mStagingCategory);
}
});
} else {
Log.d(TAG, "Suggestion NOT loaded, delaying setCategory by " + MAX_WAIT_MILLIS + "ms");
if (mDashboardFeatureProvider.useSuggestionUiV2()) {
mHandler.postDelayed(()
-> mAdapterV2.setCategory(mStagingCategory), MAX_WAIT_MILLIS);
} else {
mHandler.postDelayed(() -> mAdapter.setCategory(mStagingCategory), MAX_WAIT_MILLIS);
}
}
}
}

View File

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

View File

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

View File

@@ -24,6 +24,10 @@ import android.support.v7.widget.helper.ItemTouchHelper;
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 {

View File

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

View File

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

View File

@@ -36,7 +36,6 @@ import com.android.settings.TestConfig;
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 @@ public class SuggestionAdapterTest {
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 @@ public class SuggestionAdapterTest {
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 @@ public class SuggestionAdapterTest {
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 @@ public class SuggestionAdapterTest {
// 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 @@ public class SuggestionAdapterTest {
@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)

View File

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