2 * Copyright (C) 2018 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.android.settings.dashboard;
18 import android.app.Activity;
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.drawable.Drawable;
22 import android.graphics.drawable.Icon;
23 import android.os.Bundle;
24 import android.service.settings.suggestions.Suggestion;
25 import android.support.annotation.VisibleForTesting;
26 import android.support.v7.util.DiffUtil;
27 import android.support.v7.widget.LinearLayoutManager;
28 import android.support.v7.widget.RecyclerView;
29 import android.text.TextUtils;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.widget.ImageView;
36 import android.widget.LinearLayout;
37 import android.widget.TextView;
39 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
40 import com.android.settings.R;
41 import com.android.settings.R.id;
42 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
43 import com.android.settings.dashboard.DashboardDataV2.ConditionHeaderData;
44 import com.android.settings.dashboard.conditional.Condition;
45 import com.android.settings.dashboard.conditional.ConditionAdapterV2;
46 import com.android.settings.dashboard.suggestions.SuggestionAdapterV2;
47 import com.android.settings.dashboard.suggestions.SuggestionControllerMixin;
48 import com.android.settings.overlay.FeatureFactory;
49 import com.android.settingslib.core.lifecycle.Lifecycle;
50 import com.android.settingslib.core.lifecycle.LifecycleObserver;
51 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
52 import com.android.settingslib.drawer.DashboardCategory;
53 import com.android.settingslib.drawer.Tile;
55 import java.util.List;
57 public class DashboardAdapterV2 extends RecyclerView.Adapter<DashboardAdapterV2.DashboardItemHolder>
58 implements SummaryLoader.SummaryConsumer, SuggestionAdapterV2.Callback, LifecycleObserver,
60 public static final String TAG = "DashboardAdapterV2";
61 private static final String STATE_CATEGORY_LIST = "category_list";
64 static final String STATE_CONDITION_EXPANDED = "condition_expanded";
66 private final IconCache mCache;
67 private final Context mContext;
68 private final MetricsFeatureProvider mMetricsFeatureProvider;
69 private final DashboardFeatureProvider mDashboardFeatureProvider;
70 private boolean mFirstFrameDrawn;
71 private RecyclerView mRecyclerView;
72 private SuggestionAdapterV2 mSuggestionAdapter;
75 DashboardDataV2 mDashboardData;
77 private View.OnClickListener mTileClickListener = new View.OnClickListener() {
79 public void onClick(View v) {
80 //TODO: get rid of setTag/getTag
81 mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag());
85 public DashboardAdapterV2(Context context, Bundle savedInstanceState,
86 List<Condition> conditions, SuggestionControllerMixin suggestionControllerMixin,
87 Lifecycle lifecycle) {
89 DashboardCategory category = null;
90 boolean conditionExpanded = false;
93 final FeatureFactory factory = FeatureFactory.getFactory(context);
94 mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
95 mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
96 mCache = new IconCache(context);
97 mSuggestionAdapter = new SuggestionAdapterV2(mContext, suggestionControllerMixin,
98 savedInstanceState, this /* callback */, lifecycle);
100 setHasStableIds(true);
102 if (savedInstanceState != null) {
103 category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST);
104 conditionExpanded = savedInstanceState.getBoolean(
105 STATE_CONDITION_EXPANDED, conditionExpanded);
108 if (lifecycle != null) {
109 lifecycle.addObserver(this);
112 mDashboardData = new DashboardDataV2.Builder()
113 .setConditions(conditions)
114 .setSuggestions(mSuggestionAdapter.getSuggestions())
115 .setCategory(category)
116 .setConditionExpanded(conditionExpanded)
120 public void setSuggestions(List<Suggestion> data) {
121 final DashboardDataV2 prevData = mDashboardData;
122 mDashboardData = new DashboardDataV2.Builder(prevData)
123 .setSuggestions(data)
125 notifyDashboardDataChanged(prevData);
128 public void setCategory(DashboardCategory category) {
130 final DashboardDataV2 prevData = mDashboardData;
131 Log.d(TAG, "adapter setCategory called");
132 mDashboardData = new DashboardDataV2.Builder(prevData)
133 .setCategory(category)
135 notifyDashboardDataChanged(prevData);
138 public void setConditions(List<Condition> conditions) {
139 final DashboardDataV2 prevData = mDashboardData;
140 Log.d(TAG, "adapter setConditions called");
141 mDashboardData = new DashboardDataV2.Builder(prevData)
142 .setConditions(conditions)
144 notifyDashboardDataChanged(prevData);
148 public void onSuggestionClosed(Suggestion suggestion) {
149 final List<Suggestion> list = mDashboardData.getSuggestions();
150 if (list == null || list.size() == 0) {
153 if (list.size() == 1) {
154 // The only suggestion is dismissed, and the the empty suggestion container will
155 // remain as the dashboard item. Need to refresh the dashboard list.
156 final DashboardDataV2 prevData = mDashboardData;
157 mDashboardData = new DashboardDataV2.Builder(prevData)
158 .setSuggestions(null)
160 notifyDashboardDataChanged(prevData);
162 mSuggestionAdapter.removeSuggestion(suggestion);
167 public void notifySummaryChanged(Tile tile) {
168 final int position = mDashboardData.getPositionByTile(tile);
169 if (position != DashboardDataV2.POSITION_NOT_FOUND) {
170 // Since usually tile in parameter and tile in mCategories are same instance,
171 // which is hard to be detected by DiffUtil, so we notifyItemChanged directly.
172 notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position));
177 public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
178 final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
179 if (viewType == R.layout.suggestion_condition_header) {
180 return new ConditionHeaderHolder(view);
182 if (viewType == R.layout.condition_container) {
183 return new ConditionContainerHolder(view);
185 if (viewType == R.layout.suggestion_container) {
186 return new SuggestionContainerHolder(view);
188 return new DashboardItemHolder(view);
192 public void onBindViewHolder(DashboardItemHolder holder, int position) {
193 final int type = mDashboardData.getItemTypeByPosition(position);
195 case R.layout.dashboard_tile:
196 final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position);
197 onBindTile((DashboardItemHolder) holder, tile);
198 holder.itemView.setTag(tile);
199 holder.itemView.setOnClickListener(mTileClickListener);
201 case R.layout.suggestion_container:
202 onBindSuggestion((SuggestionContainerHolder) holder, position);
204 case R.layout.condition_container:
205 onBindCondition((ConditionContainerHolder) holder, position);
207 case R.layout.suggestion_condition_header:
208 onBindConditionHeader((ConditionHeaderHolder) holder,
209 (ConditionHeaderData) mDashboardData.getItemEntityByPosition(position));
211 case R.layout.suggestion_condition_footer:
212 holder.itemView.setOnClickListener(v -> {
213 mMetricsFeatureProvider.action(mContext,
214 MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false);
215 DashboardDataV2 prevData = mDashboardData;
216 mDashboardData = new DashboardDataV2.Builder(prevData).
217 setConditionExpanded(false).build();
218 notifyDashboardDataChanged(prevData);
219 scrollToTopOfConditions();
226 public long getItemId(int position) {
227 return mDashboardData.getItemIdByPosition(position);
231 public int getItemViewType(int position) {
232 return mDashboardData.getItemTypeByPosition(position);
236 public int getItemCount() {
237 return mDashboardData.size();
241 public void onAttachedToRecyclerView(RecyclerView recyclerView) {
242 super.onAttachedToRecyclerView(recyclerView);
243 // save the view so that we can scroll it when expanding/collapsing the suggestion and
245 mRecyclerView = recyclerView;
248 public Object getItem(long itemId) {
249 return mDashboardData.getItemEntityById(itemId);
252 public Suggestion getSuggestion(int position) {
253 return mSuggestionAdapter.getSuggestion(position);
257 void notifyDashboardDataChanged(DashboardDataV2 prevData) {
258 if (mFirstFrameDrawn && prevData != null) {
259 final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardDataV2
260 .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList()));
261 diffResult.dispatchUpdatesTo(this);
263 mFirstFrameDrawn = true;
264 notifyDataSetChanged();
269 void onBindConditionHeader(final ConditionHeaderHolder holder, ConditionHeaderData data) {
270 holder.icon.setImageIcon(data.conditionIcons.get(0));
271 if (data.conditionCount == 1) {
272 holder.title.setText(data.title);
273 holder.summary.setText(null);
274 holder.icons.setVisibility(View.INVISIBLE);
276 holder.title.setText(null);
277 holder.summary.setText(
278 mContext.getString(R.string.condition_summary, data.conditionCount));
279 updateConditionIcons(data.conditionIcons, holder.icons);
280 holder.icons.setVisibility(View.VISIBLE);
283 holder.itemView.setOnClickListener(v -> {
284 mMetricsFeatureProvider.action(mContext,
285 MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true);
286 final DashboardDataV2 prevData = mDashboardData;
287 mDashboardData = new DashboardDataV2.Builder(prevData)
288 .setConditionExpanded(true).build();
289 notifyDashboardDataChanged(prevData);
290 scrollToTopOfConditions();
295 void onBindCondition(final ConditionContainerHolder holder, int position) {
296 final ConditionAdapterV2 adapter = new ConditionAdapterV2(mContext,
297 (List<Condition>) mDashboardData.getItemEntityByPosition(position),
298 mDashboardData.isConditionExpanded());
299 adapter.addDismissHandling(holder.data);
300 holder.data.setAdapter(adapter);
301 holder.data.setLayoutManager(new LinearLayoutManager(mContext));
305 void onBindSuggestion(final SuggestionContainerHolder holder, int position) {
306 // If there is suggestions to show, it will be at position 0 as we don't show the suggestion
308 final List<Suggestion> suggestions =
309 (List<Suggestion>) mDashboardData.getItemEntityByPosition(position);
310 final int suggestionCount = suggestions.size();
311 if (suggestions != null && suggestionCount > 0) {
312 holder.summary.setText(""+suggestionCount);
313 mSuggestionAdapter.setSuggestions(suggestions);
314 holder.data.setAdapter(mSuggestionAdapter);
316 final LinearLayoutManager layoutManager = new LinearLayoutManager(mContext);
317 layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
318 holder.data.setLayoutManager(layoutManager);
321 private void onBindTile(DashboardItemHolder holder, Tile tile) {
322 holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
323 holder.title.setText(tile.title);
324 if (!TextUtils.isEmpty(tile.summary)) {
325 holder.summary.setText(tile.summary);
326 holder.summary.setVisibility(View.VISIBLE);
328 holder.summary.setVisibility(View.GONE);
332 private void tintIcons(DashboardCategory category) {
333 if (!mDashboardFeatureProvider.shouldTintIcon()) {
336 // TODO: Better place for tinting?
337 final TypedArray a = mContext.obtainStyledAttributes(new int[]{
338 android.R.attr.colorControlNormal});
339 final int tintColor = a.getColor(0, mContext.getColor(R.color.fallback_tintColor));
341 if (category != null) {
342 for (Tile tile : category.getTiles()) {
343 if (tile.isIconTintable) {
344 // If this drawable is tintable, tint it to match the color.
345 tile.icon.setTint(tintColor);
352 public void onSaveInstanceState(Bundle outState) {
353 final DashboardCategory category = mDashboardData.getCategory();
354 if (category != null) {
355 outState.putParcelable(STATE_CATEGORY_LIST, category);
357 outState.putBoolean(STATE_CONDITION_EXPANDED, mDashboardData.isConditionExpanded());
360 private void updateConditionIcons(List<Icon> icons, ViewGroup parent) {
361 if (icons == null || icons.size() < 2) {
362 parent.setVisibility(View.INVISIBLE);
365 final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
366 parent.removeAllViews();
367 for (int i = 1, size = icons.size(); i < size; i++) {
368 ImageView icon = (ImageView) inflater.inflate(
369 R.layout.condition_header_icon, parent, false);
370 icon.setImageIcon(icons.get(i));
371 parent.addView(icon);
373 parent.setVisibility(View.VISIBLE);
376 private void scrollToTopOfConditions() {
377 mRecyclerView.scrollToPosition(mDashboardData.hasSuggestion() ? 1 : 0);
380 public static class IconCache {
381 private final Context mContext;
382 private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>();
384 public IconCache(Context context) {
388 public Drawable getIcon(Icon icon) {
392 Drawable drawable = mMap.get(icon);
393 if (drawable == null) {
394 drawable = icon.loadDrawable(mContext);
395 mMap.put(icon, drawable);
401 public static class DashboardItemHolder extends RecyclerView.ViewHolder {
402 public final ImageView icon;
403 public final TextView title;
404 public final TextView summary;
406 public DashboardItemHolder(View itemView) {
408 icon = itemView.findViewById(android.R.id.icon);
409 title = itemView.findViewById(android.R.id.title);
410 summary = itemView.findViewById(android.R.id.summary);
414 public static class ConditionHeaderHolder extends DashboardItemHolder {
415 public final LinearLayout icons;
416 public final ImageView expandIndicator;
418 public ConditionHeaderHolder(View itemView) {
420 icons = itemView.findViewById(id.additional_icons);
421 expandIndicator = itemView.findViewById(id.expand_indicator);
425 public static class ConditionContainerHolder extends DashboardItemHolder {
426 public final RecyclerView data;
428 public ConditionContainerHolder(View itemView) {
430 data = itemView.findViewById(id.data);
434 public static class SuggestionContainerHolder extends DashboardItemHolder {
435 public final RecyclerView data;
437 public SuggestionContainerHolder(View itemView) {
439 data = itemView.findViewById(id.suggestion_list);