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:
@@ -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";
|
||||
}
|
||||
|
442
src/com/android/settings/dashboard/DashboardAdapterV2.java
Normal file
442
src/com/android/settings/dashboard/DashboardAdapterV2.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
446
src/com/android/settings/dashboard/DashboardDataV2.java
Normal file
446
src/com/android/settings/dashboard/DashboardDataV2.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,8 +176,10 @@ public class DashboardSummary extends InstrumentedFragment
|
||||
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 @@ public class DashboardSummary extends InstrumentedFragment
|
||||
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 @@ public class DashboardSummary extends InstrumentedFragment
|
||||
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 @@ public class DashboardSummary extends InstrumentedFragment
|
||||
|
||||
@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 @@ public class DashboardSummary extends InstrumentedFragment
|
||||
@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 @@ public class DashboardSummary extends InstrumentedFragment
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user