1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 package com.android.settings.dashboard; 17 18 import android.app.Activity; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Color; 23 import android.graphics.drawable.Drawable; 24 import android.graphics.drawable.Icon; 25 import android.os.Bundle; 26 import android.support.annotation.VisibleForTesting; 27 import android.support.v7.util.DiffUtil; 28 import android.support.v7.widget.LinearLayoutManager; 29 import android.support.v7.widget.RecyclerView; 30 import android.text.TextUtils; 31 import android.util.ArrayMap; 32 import android.util.Log; 33 import android.util.Pair; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.ImageView; 38 import android.widget.LinearLayout; 39 import android.widget.TextView; 40 41 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 42 import com.android.settings.R; 43 import com.android.settings.R.id; 44 import com.android.settings.core.instrumentation.MetricsFeatureProvider; 45 import com.android.settings.dashboard.DashboardData.SuggestionConditionHeaderData; 46 import com.android.settings.dashboard.conditional.Condition; 47 import com.android.settings.dashboard.conditional.ConditionAdapter; 48 import com.android.settings.dashboard.suggestions.SuggestionAdapter; 49 import com.android.settings.dashboard.suggestions.SuggestionDismissController; 50 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; 51 import com.android.settings.dashboard.suggestions.SuggestionLogHelper; 52 import com.android.settings.overlay.FeatureFactory; 53 import com.android.settingslib.Utils; 54 import com.android.settingslib.drawer.DashboardCategory; 55 import com.android.settingslib.drawer.Tile; 56 import com.android.settingslib.suggestions.SuggestionParser; 57 58 import java.util.ArrayList; 59 import java.util.List; 60 61 public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> 62 implements SummaryLoader.SummaryConsumer { 63 public static final String TAG = "DashboardAdapter"; 64 private static final String STATE_SUGGESTION_LIST = "suggestion_list"; 65 private static final String STATE_CATEGORY_LIST = "category_list"; 66 private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged"; 67 68 @VisibleForTesting 69 static final String STATE_SUGGESTION_CONDITION_MODE = "suggestion_condition_mode"; 70 @VisibleForTesting 71 static final int SUGGESTION_CONDITION_HEADER_POSITION = 0; 72 @VisibleForTesting 73 static final int MAX_SUGGESTION_TO_SHOW = 5; 74 75 private final IconCache mCache; 76 private final Context mContext; 77 private final MetricsFeatureProvider mMetricsFeatureProvider; 78 private final DashboardFeatureProvider mDashboardFeatureProvider; 79 private final SuggestionFeatureProvider mSuggestionFeatureProvider; 80 private final ArrayList<String> mSuggestionsShownLogged; 81 private boolean mFirstFrameDrawn; 82 private RecyclerView mRecyclerView; 83 private SuggestionParser mSuggestionParser; 84 private SuggestionAdapter mSuggestionAdapter; 85 private SuggestionDismissController mSuggestionDismissHandler; 86 private SuggestionDismissController.Callback mCallback; 87 88 @VisibleForTesting 89 DashboardData mDashboardData; 90 91 private View.OnClickListener mTileClickListener = new View.OnClickListener() { 92 @Override 93 public void onClick(View v) { 94 //TODO: get rid of setTag/getTag 95 mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag()); 96 } 97 }; 98 99 private View.OnClickListener mConditionClickListener = new View.OnClickListener() { 100 101 @Override 102 public void onClick(View v) { 103 Condition condition = (Condition) v.getTag(); 104 //TODO: get rid of setTag/getTag 105 mMetricsFeatureProvider.action(mContext, 106 MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK, 107 condition.getMetricsConstant()); 108 condition.onPrimaryClick(); 109 } 110 }; 111 112 public DashboardAdapter(Context context, Bundle savedInstanceState, 113 List<Condition> conditions, SuggestionParser suggestionParser, 114 SuggestionDismissController.Callback callback) { 115 List<Tile> suggestions = null; 116 DashboardCategory category = null; 117 int suggestionConditionMode = DashboardData.HEADER_MODE_DEFAULT; 118 119 mContext = context; 120 final FeatureFactory factory = FeatureFactory.getFactory(context); 121 mMetricsFeatureProvider = factory.getMetricsFeatureProvider(); 122 mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context); 123 mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context); 124 mCache = new IconCache(context); 125 mSuggestionParser = suggestionParser; 126 mCallback = callback; 127 128 setHasStableIds(true); 129 130 if (savedInstanceState != null) { 131 suggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST); 132 category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST); 133 suggestionConditionMode = savedInstanceState.getInt( 134 STATE_SUGGESTION_CONDITION_MODE, suggestionConditionMode); 135 mSuggestionsShownLogged = savedInstanceState.getStringArrayList( 136 STATE_SUGGESTIONS_SHOWN_LOGGED); 137 } else { 138 mSuggestionsShownLogged = new ArrayList<>(); 139 } 140 141 mDashboardData = new DashboardData.Builder() 142 .setConditions(conditions) 143 .setSuggestions(suggestions) 144 .setCategory(category) 145 .setSuggestionConditionMode(suggestionConditionMode) 146 .build(); 147 } 148 149 public List<Tile> getSuggestions() { 150 return mDashboardData.getSuggestions(); 151 } 152 153 public void setCategoriesAndSuggestions(DashboardCategory category, 154 List<Tile> suggestions) { 155 tintIcons(category, suggestions); 156 157 final DashboardData prevData = mDashboardData; 158 mDashboardData = new DashboardData.Builder(prevData) 159 .setSuggestions(suggestions.subList(0, 160 Math.min(suggestions.size(), MAX_SUGGESTION_TO_SHOW))) 161 .setCategory(category) 162 .build(); 163 notifyDashboardDataChanged(prevData); 164 List<Tile> shownSuggestions = null; 165 final int mode = mDashboardData.getSuggestionConditionMode(); 166 if (mode == DashboardData.HEADER_MODE_DEFAULT) { 167 shownSuggestions = suggestions.subList(0, 168 Math.min(suggestions.size(), DashboardData.DEFAULT_SUGGESTION_COUNT)); 169 } else if (mode != DashboardData.HEADER_MODE_COLLAPSED) { 170 shownSuggestions = suggestions; 171 } 172 if (shownSuggestions != null) { 173 for (Tile suggestion : shownSuggestions) { 174 final String identifier = mSuggestionFeatureProvider.getSuggestionIdentifier( 175 mContext, suggestion); 176 mMetricsFeatureProvider.action( 177 mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, identifier, 178 getSuggestionTaggedData()); 179 mSuggestionsShownLogged.add(identifier); 180 } 181 } 182 } 183 184 public void setCategory(DashboardCategory category) { 185 tintIcons(category, null); 186 final DashboardData prevData = mDashboardData; 187 Log.d(TAG, "adapter setCategory called"); 188 mDashboardData = new DashboardData.Builder(prevData) 189 .setCategory(category) 190 .build(); 191 notifyDashboardDataChanged(prevData); 192 } 193 194 public void setConditions(List<Condition> conditions) { 195 final DashboardData prevData = mDashboardData; 196 Log.d(TAG, "adapter setConditions called"); 197 mDashboardData = new DashboardData.Builder(prevData) 198 .setConditions(conditions) 199 .build(); 200 notifyDashboardDataChanged(prevData); 201 } 202 203 public void onSuggestionDismissed(Tile suggestion) { 204 final List<Tile> suggestions = mDashboardData.getSuggestions(); 205 if (suggestions == null || suggestions.isEmpty()) { 206 return; 207 } 208 if (suggestions.size() == 1) { 209 // The only suggestion is dismissed, and the the empty suggestion container will 210 // remain as the dashboard item. Need to refresh the dashboard list. 211 final DashboardData prevData = mDashboardData; 212 mDashboardData = new DashboardData.Builder(prevData) 213 .setSuggestions(null) 214 .build(); 215 notifyDashboardDataChanged(prevData); 216 } else { 217 mSuggestionAdapter.removeSuggestion(suggestion); 218 } 219 } 220 221 @Override 222 public void notifySummaryChanged(Tile tile) { 223 final int position = mDashboardData.getPositionByTile(tile); 224 if (position != DashboardData.POSITION_NOT_FOUND) { 225 // Since usually tile in parameter and tile in mCategories are same instance, 226 // which is hard to be detected by DiffUtil, so we notifyItemChanged directly. 227 notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position)); 228 } 229 } 230 231 @Override 232 public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { 233 final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); 234 if (viewType == R.layout.suggestion_condition_header) { 235 return new SuggestionAndConditionHeaderHolder(view); 236 } 237 if (viewType == R.layout.suggestion_condition_container) { 238 return new SuggestionAndConditionContainerHolder(view); 239 } 240 return new DashboardItemHolder(view); 241 } 242 243 @Override 244 public void onBindViewHolder(DashboardItemHolder holder, int position) { 245 final int type = mDashboardData.getItemTypeByPosition(position); 246 switch (type) { 247 case R.layout.dashboard_tile: 248 final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position); 249 onBindTile(holder, tile); 250 holder.itemView.setTag(tile); 251 holder.itemView.setOnClickListener(mTileClickListener); 252 break; 253 case R.layout.suggestion_condition_container: 254 onBindConditionAndSuggestion( 255 (SuggestionAndConditionContainerHolder) holder, position); 256 break; 257 case R.layout.suggestion_condition_header: 258 onBindSuggestionConditionHeader((SuggestionAndConditionHeaderHolder) holder, 259 (SuggestionConditionHeaderData) 260 mDashboardData.getItemEntityByPosition(position)); 261 break; 262 case R.layout.suggestion_condition_footer: 263 holder.itemView.setOnClickListener(v -> { 264 mMetricsFeatureProvider.action(mContext, 265 MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false); 266 DashboardData prevData = mDashboardData; 267 mDashboardData = new DashboardData.Builder(prevData).setSuggestionConditionMode( 268 DashboardData.HEADER_MODE_COLLAPSED).build(); 269 notifyDashboardDataChanged(prevData); 270 mRecyclerView.scrollToPosition(SUGGESTION_CONDITION_HEADER_POSITION); 271 }); 272 break; 273 } 274 } 275 276 @Override 277 public long getItemId(int position) { 278 return mDashboardData.getItemIdByPosition(position); 279 } 280 281 @Override 282 public int getItemViewType(int position) { 283 return mDashboardData.getItemTypeByPosition(position); 284 } 285 286 @Override 287 public int getItemCount() { 288 return mDashboardData.size(); 289 } 290 291 @Override 292 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 293 super.onAttachedToRecyclerView(recyclerView); 294 // save the view so that we can scroll it when expanding/collapsing the suggestion and 295 // conditions. 296 mRecyclerView = recyclerView; 297 } 298 299 public void onPause() { 300 if (mDashboardData.getSuggestions() == null) { 301 return; 302 } 303 for (Tile suggestion : mDashboardData.getSuggestions()) { 304 String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier( 305 mContext, suggestion); 306 if (mSuggestionsShownLogged.contains(suggestionId)) { 307 mMetricsFeatureProvider.action( 308 mContext, MetricsEvent.ACTION_HIDE_SETTINGS_SUGGESTION, suggestionId, 309 getSuggestionTaggedData()); 310 } 311 } 312 mSuggestionsShownLogged.clear(); 313 } 314 315 public Object getItem(long itemId) { 316 return mDashboardData.getItemEntityById(itemId); 317 } 318 319 public Tile getSuggestion(int position) { 320 return mSuggestionAdapter.getSuggestion(position); 321 } 322 323 @VisibleForTesting 324 void notifyDashboardDataChanged(DashboardData prevData) { 325 if (mFirstFrameDrawn && prevData != null) { 326 final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData 327 .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList())); 328 diffResult.dispatchUpdatesTo(this); 329 } else { 330 mFirstFrameDrawn = true; 331 notifyDataSetChanged(); 332 } 333 } 334 335 private void logSuggestions() { 336 final List<Tile> suggestions = mDashboardData.getSuggestions(); 337 if (suggestions == null) { 338 return; 339 } 340 for (Tile suggestion : suggestions) { 341 final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier( 342 mContext, suggestion); 343 if (!mSuggestionsShownLogged.contains(suggestionId)) { 344 mMetricsFeatureProvider.action( 345 mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, suggestionId, 346 getSuggestionTaggedData()); 347 mSuggestionsShownLogged.add(suggestionId); 348 } 349 } 350 } 351 352 @VisibleForTesting 353 void onBindSuggestionConditionHeader(final SuggestionAndConditionHeaderHolder holder, 354 SuggestionConditionHeaderData data) { 355 final int curMode = mDashboardData.getSuggestionConditionMode(); 356 final int nextMode = data.hiddenSuggestionCount > 0 && data.conditionCount > 0 357 && curMode != DashboardData.HEADER_MODE_SUGGESTION_EXPANDED 358 ? DashboardData.HEADER_MODE_SUGGESTION_EXPANDED 359 : DashboardData.HEADER_MODE_FULLY_EXPANDED; 360 final boolean moreSuggestions = data.hiddenSuggestionCount > 0; 361 final boolean hasConditions = data.conditionCount > 0; 362 if (data.conditionCount > 0) { 363 holder.icon.setImageIcon(data.conditionIcons.get(0)); 364 holder.icon.setVisibility(View.VISIBLE); 365 if (data.conditionCount == 1) { 366 holder.title.setText(data.title); 367 holder.title.setTextColor(Utils.getColorAccent(mContext)); 368 holder.icons.setVisibility(View.INVISIBLE); 369 } else { 370 holder.title.setText(null); 371 updateConditionIcons(data.conditionIcons, holder.icons); 372 holder.icons.setVisibility(View.VISIBLE); 373 } 374 } else { 375 holder.icon.setVisibility(View.INVISIBLE); 376 holder.icons.setVisibility(View.INVISIBLE); 377 } 378 379 if (data.hiddenSuggestionCount > 0) { 380 holder.summary.setTextColor(Color.BLACK); 381 if (curMode == DashboardData.HEADER_MODE_COLLAPSED) { 382 if (data.conditionCount > 0) { 383 holder.summary.setText(mContext.getResources().getQuantityString( 384 R.plurals.suggestions_collapsed_summary, 385 data.hiddenSuggestionCount, data.hiddenSuggestionCount)); 386 } else { 387 holder.title.setText(mContext.getResources().getQuantityString( 388 R.plurals.suggestions_collapsed_title, 389 data.hiddenSuggestionCount, data.hiddenSuggestionCount)); 390 holder.title.setTextColor(Color.BLACK); 391 holder.summary.setText(null); 392 } 393 } else if (curMode == DashboardData.HEADER_MODE_DEFAULT) { 394 if (data.conditionCount > 0) { 395 holder.summary.setText(mContext.getString( 396 R.string.suggestions_summary, data.hiddenSuggestionCount)); 397 } else { 398 holder.title.setText(mContext.getString( 399 R.string.suggestions_more_title, data.hiddenSuggestionCount)); 400 holder.title.setTextColor(Color.BLACK); 401 holder.summary.setText(null); 402 } 403 } 404 } else if (data.conditionCount > 1) { 405 holder.summary.setTextColor(Utils.getColorAccent(mContext)); 406 holder.summary.setText( 407 mContext.getString(R.string.condition_summary, data.conditionCount)); 408 } else { 409 holder.summary.setText(null); 410 } 411 412 final Resources res = mContext.getResources(); 413 final int padding = res.getDimensionPixelOffset( 414 curMode == DashboardData.HEADER_MODE_COLLAPSED 415 ? R.dimen.suggestion_condition_header_padding_collapsed 416 : R.dimen.suggestion_condition_header_padding_expanded); 417 holder.itemView.setPadding(0, padding, 0, padding); 418 419 holder.itemView.setOnClickListener(v -> { 420 if (moreSuggestions) { 421 logSuggestions(); 422 } else if (hasConditions) { 423 mMetricsFeatureProvider.action(mContext, 424 MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true); 425 } 426 DashboardData prevData = mDashboardData; 427 final boolean wasCollapsed = curMode == DashboardData.HEADER_MODE_COLLAPSED; 428 mDashboardData = new DashboardData.Builder(prevData) 429 .setSuggestionConditionMode(nextMode).build(); 430 notifyDashboardDataChanged(prevData); 431 if (wasCollapsed) { 432 mRecyclerView.scrollToPosition(SUGGESTION_CONDITION_HEADER_POSITION); 433 } 434 }); 435 } 436 437 @VisibleForTesting 438 void onBindConditionAndSuggestion(final SuggestionAndConditionContainerHolder holder, 439 int position) { 440 // If there is suggestions to show, it will be at position 0 as we don't show the suggestion 441 // header anymore. 442 final List<Tile> suggestions = mDashboardData.getSuggestions(); 443 if (position == SUGGESTION_CONDITION_HEADER_POSITION 444 && suggestions != null && suggestions.size() > 0) { 445 mSuggestionAdapter = new SuggestionAdapter(mContext, (List<Tile>) 446 mDashboardData.getItemEntityByPosition(position), mSuggestionsShownLogged); 447 mSuggestionDismissHandler = new SuggestionDismissController(mContext, 448 holder.data, mSuggestionParser, mCallback); 449 holder.data.setAdapter(mSuggestionAdapter); 450 } else { 451 ConditionAdapter adapter = new ConditionAdapter(mContext, 452 (List<Condition>) mDashboardData.getItemEntityByPosition(position), 453 mDashboardData.getSuggestionConditionMode()); 454 adapter.addDismissHandling(holder.data); 455 holder.data.setAdapter(adapter); 456 } 457 holder.data.setLayoutManager(new LinearLayoutManager(mContext)); 458 } 459 460 private void onBindTile(DashboardItemHolder holder, Tile tile) { 461 if (tile.remoteViews != null) { 462 final ViewGroup itemView = (ViewGroup) holder.itemView; 463 itemView.removeAllViews(); 464 itemView.addView(tile.remoteViews.apply(itemView.getContext(), itemView)); 465 } else { 466 holder.icon.setImageDrawable(mCache.getIcon(tile.icon)); 467 holder.title.setText(tile.title); 468 if (!TextUtils.isEmpty(tile.summary)) { 469 holder.summary.setText(tile.summary); 470 holder.summary.setVisibility(View.VISIBLE); 471 } else { 472 holder.summary.setVisibility(View.GONE); 473 } 474 } 475 } 476 477 private void tintIcons(DashboardCategory category, List<Tile> suggestions) { 478 if (!mDashboardFeatureProvider.shouldTintIcon()) { 479 return; 480 } 481 // TODO: Better place for tinting? 482 final TypedArray a = mContext.obtainStyledAttributes(new int[]{ 483 android.R.attr.colorControlNormal}); 484 final int tintColor = a.getColor(0, mContext.getColor(R.color.fallback_tintColor)); 485 a.recycle(); 486 if (category != null) { 487 for (Tile tile : category.tiles) { 488 if (tile.isIconTintable) { 489 // If this drawable is tintable, tint it to match the color. 490 tile.icon.setTint(tintColor); 491 } 492 } 493 } 494 if (suggestions != null) { 495 for (Tile suggestion : suggestions) { 496 if (suggestion.isIconTintable) { 497 suggestion.icon.setTint(tintColor); 498 } 499 } 500 } 501 } 502 503 void onSaveInstanceState(Bundle outState) { 504 final List<Tile> suggestions = mDashboardData.getSuggestions(); 505 final DashboardCategory category = mDashboardData.getCategory(); 506 if (suggestions != null) { 507 outState.putParcelableArrayList(STATE_SUGGESTION_LIST, new ArrayList<>(suggestions)); 508 } 509 if (category != null) { 510 outState.putParcelable(STATE_CATEGORY_LIST, category); 511 } 512 outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged); 513 outState.putInt(STATE_SUGGESTION_CONDITION_MODE, 514 mDashboardData.getSuggestionConditionMode()); 515 } 516 517 private void updateConditionIcons(List<Icon> icons, ViewGroup parent) { 518 if (icons == null || icons.size() < 2) { 519 parent.setVisibility(View.INVISIBLE); 520 return; 521 } 522 final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 523 parent.removeAllViews(); 524 for (int i = 1, size = icons.size(); i < size; i++) { 525 ImageView icon = (ImageView) inflater.inflate( 526 R.layout.condition_header_icon, parent, false); 527 icon.setImageIcon(icons.get(i)); 528 parent.addView(icon); 529 } 530 parent.setVisibility(View.VISIBLE); 531 } 532 533 private Pair<Integer, Object>[] getSuggestionTaggedData() { 534 return SuggestionLogHelper.getSuggestionTaggedData( 535 mSuggestionFeatureProvider.isSmartSuggestionEnabled(mContext)); 536 } 537 538 public static class IconCache { 539 private final Context mContext; 540 private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>(); 541 542 public IconCache(Context context) { 543 mContext = context; 544 } 545 546 public Drawable getIcon(Icon icon) { 547 Drawable drawable = mMap.get(icon); 548 if (drawable == null) { 549 drawable = icon.loadDrawable(mContext); 550 mMap.put(icon, drawable); 551 } 552 return drawable; 553 } 554 } 555 556 public static class DashboardItemHolder extends RecyclerView.ViewHolder { 557 public final ImageView icon; 558 public final TextView title; 559 public final TextView summary; 560 561 public DashboardItemHolder(View itemView) { 562 super(itemView); 563 icon = itemView.findViewById(android.R.id.icon); 564 title = itemView.findViewById(android.R.id.title); 565 summary = itemView.findViewById(android.R.id.summary); 566 } 567 } 568 569 public static class SuggestionAndConditionHeaderHolder extends DashboardItemHolder { 570 public final LinearLayout icons; 571 public final ImageView expandIndicator; 572 573 public SuggestionAndConditionHeaderHolder(View itemView) { 574 super(itemView); 575 icons = itemView.findViewById(id.additional_icons); 576 expandIndicator = itemView.findViewById(id.expand_indicator); 577 } 578 } 579 580 public static class SuggestionAndConditionContainerHolder extends DashboardItemHolder { 581 public final RecyclerView data; 582 583 public SuggestionAndConditionContainerHolder(View itemView) { 584 super(itemView); 585 data = itemView.findViewById(id.data); 586 } 587 } 588 589 } 590