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.TypedArray; 21 import android.graphics.drawable.Drawable; 22 import android.graphics.drawable.Icon; 23 import android.os.Bundle; 24 import android.support.annotation.VisibleForTesting; 25 import android.support.v7.util.DiffUtil; 26 import android.support.v7.widget.RecyclerView; 27 import android.text.TextUtils; 28 import android.util.ArrayMap; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.ImageView; 34 import android.widget.TextView; 35 36 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 37 import com.android.settings.R; 38 import com.android.settings.SettingsActivity; 39 import com.android.settings.core.instrumentation.MetricsFeatureProvider; 40 import com.android.settings.dashboard.conditional.Condition; 41 import com.android.settings.dashboard.conditional.ConditionAdapterUtils; 42 import com.android.settings.dashboard.suggestions.SuggestionDismissController; 43 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; 44 import com.android.settings.overlay.FeatureFactory; 45 import com.android.settingslib.drawer.DashboardCategory; 46 import com.android.settingslib.drawer.Tile; 47 48 import java.util.ArrayList; 49 import java.util.List; 50 51 public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> 52 implements SummaryLoader.SummaryConsumer, SuggestionDismissController.Callback { 53 public static final String TAG = "DashboardAdapter"; 54 private static final String STATE_SUGGESTION_LIST = "suggestion_list"; 55 private static final String STATE_CATEGORY_LIST = "category_list"; 56 private static final String STATE_SUGGESTION_MODE = "suggestion_mode"; 57 private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged"; 58 59 private final IconCache mCache; 60 private final Context mContext; 61 private final MetricsFeatureProvider mMetricsFeatureProvider; 62 private final DashboardFeatureProvider mDashboardFeatureProvider; 63 private final SuggestionFeatureProvider mSuggestionFeatureProvider; 64 private final ArrayList<String> mSuggestionsShownLogged; 65 private boolean mFirstFrameDrawn; 66 67 @VisibleForTesting 68 DashboardData mDashboardData; 69 70 private View.OnClickListener mTileClickListener = new View.OnClickListener() { 71 @Override 72 public void onClick(View v) { 73 //TODO: get rid of setTag/getTag 74 mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag()); 75 } 76 }; 77 78 private View.OnClickListener mConditionClickListener = new View.OnClickListener() { 79 80 @Override 81 public void onClick(View v) { 82 Condition expandedCondition = mDashboardData.getExpandedCondition(); 83 84 //TODO: get rid of setTag/getTag 85 if (v.getTag() == expandedCondition) { 86 mMetricsFeatureProvider.action(mContext, 87 MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK, 88 expandedCondition.getMetricsConstant()); 89 expandedCondition.onPrimaryClick(); 90 } else { 91 expandedCondition = (Condition) v.getTag(); 92 mMetricsFeatureProvider.action(mContext, 93 MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, 94 expandedCondition.getMetricsConstant()); 95 96 updateExpandedCondition(expandedCondition); 97 } 98 } 99 }; 100 DashboardAdapter(Context context, Bundle savedInstanceState, List<Condition> conditions)101 public DashboardAdapter(Context context, Bundle savedInstanceState, 102 List<Condition> conditions) { 103 List<Tile> suggestions = null; 104 List<DashboardCategory> categories = null; 105 int suggestionMode = DashboardData.SUGGESTION_MODE_DEFAULT; 106 107 mContext = context; 108 final FeatureFactory factory = FeatureFactory.getFactory(context); 109 mMetricsFeatureProvider = factory.getMetricsFeatureProvider(); 110 mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context); 111 mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context); 112 mCache = new IconCache(context); 113 114 setHasStableIds(true); 115 116 if (savedInstanceState != null) { 117 suggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST); 118 categories = savedInstanceState.getParcelableArrayList(STATE_CATEGORY_LIST); 119 suggestionMode = savedInstanceState.getInt( 120 STATE_SUGGESTION_MODE, DashboardData.SUGGESTION_MODE_DEFAULT); 121 mSuggestionsShownLogged = savedInstanceState.getStringArrayList( 122 STATE_SUGGESTIONS_SHOWN_LOGGED); 123 } else { 124 mSuggestionsShownLogged = new ArrayList<>(); 125 } 126 127 mDashboardData = new DashboardData.Builder() 128 .setConditions(conditions) 129 .setSuggestions(suggestions) 130 .setCategories(categories) 131 .setSuggestionMode(suggestionMode) 132 .build(); 133 } 134 getSuggestions()135 public List<Tile> getSuggestions() { 136 return mDashboardData.getSuggestions(); 137 } 138 setCategoriesAndSuggestions(List<DashboardCategory> categories, List<Tile> suggestions)139 public void setCategoriesAndSuggestions(List<DashboardCategory> categories, 140 List<Tile> suggestions) { 141 // TODO: Better place for tinting? 142 final TypedArray a = mContext.obtainStyledAttributes(new int[]{ 143 android.R.attr.colorControlNormal}); 144 int tintColor = a.getColor(0, mContext.getColor(android.R.color.white)); 145 a.recycle(); 146 for (int i = 0; i < categories.size(); i++) { 147 for (int j = 0; j < categories.get(i).tiles.size(); j++) { 148 final Tile tile = categories.get(i).tiles.get(j); 149 150 if (!mContext.getPackageName().equals( 151 tile.intent.getComponent().getPackageName())) { 152 // If this drawable is coming from outside Settings, tint it to match the 153 // color. 154 tile.icon.setTint(tintColor); 155 } 156 } 157 } 158 159 final DashboardData prevData = mDashboardData; 160 mDashboardData = new DashboardData.Builder(prevData) 161 .setSuggestions(suggestions) 162 .setCategories(categories) 163 .build(); 164 notifyDashboardDataChanged(prevData); 165 List<Tile> shownSuggestions = null; 166 switch (mDashboardData.getSuggestionMode()) { 167 case DashboardData.SUGGESTION_MODE_DEFAULT: 168 shownSuggestions = suggestions.subList(0, 169 Math.min(suggestions.size(), DashboardData.DEFAULT_SUGGESTION_COUNT)); 170 break; 171 case DashboardData.SUGGESTION_MODE_EXPANDED: 172 shownSuggestions = suggestions; 173 break; 174 } 175 if (shownSuggestions != null) { 176 for (Tile suggestion : shownSuggestions) { 177 final String identifier = mSuggestionFeatureProvider.getSuggestionIdentifier( 178 mContext, suggestion); 179 mMetricsFeatureProvider.action( 180 mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, identifier); 181 mSuggestionsShownLogged.add(identifier); 182 } 183 } 184 } 185 setCategory(List<DashboardCategory> category)186 public void setCategory(List<DashboardCategory> category) { 187 final DashboardData prevData = mDashboardData; 188 Log.d(TAG, "adapter setCategory called"); 189 mDashboardData = new DashboardData.Builder(prevData) 190 .setCategories(category) 191 .build(); 192 notifyDashboardDataChanged(prevData); 193 } 194 setConditions(List<Condition> conditions)195 public void setConditions(List<Condition> conditions) { 196 final DashboardData prevData = mDashboardData; 197 Log.d(TAG, "adapter setConditions called"); 198 mDashboardData = new DashboardData.Builder(prevData) 199 .setConditions(conditions) 200 .setExpandedCondition(null) 201 .build(); 202 notifyDashboardDataChanged(prevData); 203 } 204 205 @Override notifySummaryChanged(Tile tile)206 public void notifySummaryChanged(Tile tile) { 207 final int position = mDashboardData.getPositionByTile(tile); 208 if (position != DashboardData.POSITION_NOT_FOUND) { 209 // Since usually tile in parameter and tile in mCategories are same instance, 210 // which is hard to be detected by DiffUtil, so we notifyItemChanged directly. 211 notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position)); 212 } 213 } 214 215 @Override onCreateViewHolder(ViewGroup parent, int viewType)216 public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { 217 return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate( 218 viewType, parent, false)); 219 } 220 221 @Override onBindViewHolder(DashboardItemHolder holder, int position)222 public void onBindViewHolder(DashboardItemHolder holder, int position) { 223 final int type = mDashboardData.getItemTypeByPosition(position); 224 switch (type) { 225 case R.layout.dashboard_category: 226 onBindCategory(holder, 227 (DashboardCategory) mDashboardData.getItemEntityByPosition(position)); 228 break; 229 case R.layout.dashboard_tile: 230 final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position); 231 onBindTile(holder, tile); 232 holder.itemView.setTag(tile); 233 holder.itemView.setOnClickListener(mTileClickListener); 234 break; 235 case R.layout.suggestion_header: 236 onBindSuggestionHeader(holder, (DashboardData.SuggestionHeaderData) 237 mDashboardData.getItemEntityByPosition(position)); 238 break; 239 case R.layout.suggestion_tile: 240 final Tile suggestion = (Tile) mDashboardData.getItemEntityByPosition(position); 241 final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier( 242 mContext, suggestion); 243 // This is for cases when a suggestion is dismissed and the next one comes to view 244 if (!mSuggestionsShownLogged.contains(suggestionId)) { 245 mMetricsFeatureProvider.action( 246 mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, suggestionId); 247 mSuggestionsShownLogged.add(suggestionId); 248 } 249 onBindTile(holder, suggestion); 250 holder.itemView.setOnClickListener(v -> { 251 mMetricsFeatureProvider.action(mContext, 252 MetricsEvent.ACTION_SETTINGS_SUGGESTION, suggestionId); 253 ((SettingsActivity) mContext).startSuggestion(suggestion.intent); 254 }); 255 break; 256 case R.layout.condition_card: 257 final boolean isExpanded = mDashboardData.getItemEntityByPosition(position) 258 == mDashboardData.getExpandedCondition(); 259 ConditionAdapterUtils.bindViews( 260 (Condition) mDashboardData.getItemEntityByPosition(position), 261 holder, isExpanded, mConditionClickListener, v -> onExpandClick(v)); 262 break; 263 } 264 } 265 266 @Override getItemId(int position)267 public long getItemId(int position) { 268 return mDashboardData.getItemIdByPosition(position); 269 } 270 271 @Override getItemViewType(int position)272 public int getItemViewType(int position) { 273 return mDashboardData.getItemTypeByPosition(position); 274 } 275 276 @Override getItemCount()277 public int getItemCount() { 278 return mDashboardData.size(); 279 } 280 onPause()281 public void onPause() { 282 if (mDashboardData.getSuggestions() == null) { 283 return; 284 } 285 for (Tile suggestion : mDashboardData.getSuggestions()) { 286 String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier( 287 mContext, suggestion); 288 if (mSuggestionsShownLogged.contains(suggestionId)) { 289 mMetricsFeatureProvider.action( 290 mContext, MetricsEvent.ACTION_HIDE_SETTINGS_SUGGESTION, suggestionId); 291 } 292 } 293 mSuggestionsShownLogged.clear(); 294 } 295 onExpandClick(View v)296 public void onExpandClick(View v) { 297 Condition expandedCondition = mDashboardData.getExpandedCondition(); 298 if (v.getTag() == expandedCondition) { 299 mMetricsFeatureProvider.action(mContext, 300 MetricsEvent.ACTION_SETTINGS_CONDITION_COLLAPSE, 301 expandedCondition.getMetricsConstant()); 302 expandedCondition = null; 303 } else { 304 expandedCondition = (Condition) v.getTag(); 305 mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, 306 expandedCondition.getMetricsConstant()); 307 } 308 309 updateExpandedCondition(expandedCondition); 310 } 311 getItem(long itemId)312 public Object getItem(long itemId) { 313 return mDashboardData.getItemEntityById(itemId); 314 } 315 notifyDashboardDataChanged(DashboardData prevData)316 private void notifyDashboardDataChanged(DashboardData prevData) { 317 if (mFirstFrameDrawn && prevData != null) { 318 final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData 319 .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList())); 320 diffResult.dispatchUpdatesTo(this); 321 } else { 322 mFirstFrameDrawn = true; 323 notifyDataSetChanged(); 324 } 325 } 326 updateExpandedCondition(Condition condition)327 private void updateExpandedCondition(Condition condition) { 328 final DashboardData prevData = mDashboardData; 329 mDashboardData = new DashboardData.Builder(prevData) 330 .setExpandedCondition(condition) 331 .build(); 332 notifyDashboardDataChanged(prevData); 333 } 334 335 @Override getSuggestionForPosition(int position)336 public Tile getSuggestionForPosition(int position) { 337 return (Tile) mDashboardData.getItemEntityByPosition(position); 338 } 339 340 @Override onSuggestionDismissed(Tile suggestion)341 public void onSuggestionDismissed(Tile suggestion) { 342 final List<Tile> suggestions = mDashboardData.getSuggestions(); 343 if (suggestions == null) { 344 return; 345 } 346 suggestions.remove(suggestion); 347 348 final DashboardData prevData = mDashboardData; 349 mDashboardData = new DashboardData.Builder(prevData) 350 .setSuggestions(suggestions) 351 .build(); 352 notifyDashboardDataChanged(prevData); 353 } 354 355 @VisibleForTesting onBindSuggestionHeader(final DashboardItemHolder holder, DashboardData .SuggestionHeaderData data)356 void onBindSuggestionHeader(final DashboardItemHolder holder, DashboardData 357 .SuggestionHeaderData data) { 358 final boolean moreSuggestions = data.hasMoreSuggestions; 359 final int undisplayedSuggestionCount = data.undisplayedSuggestionCount; 360 361 holder.icon.setImageResource(moreSuggestions ? R.drawable.ic_expand_more 362 : R.drawable.ic_expand_less); 363 holder.title.setText(mContext.getString(R.string.suggestions_title, data.suggestionSize)); 364 String summaryContentDescription; 365 if (moreSuggestions) { 366 summaryContentDescription = mContext.getResources().getQuantityString( 367 R.plurals.settings_suggestion_header_summary_hidden_items, 368 undisplayedSuggestionCount, undisplayedSuggestionCount); 369 } else { 370 summaryContentDescription = mContext.getString(R.string.condition_expand_hide); 371 } 372 holder.summary.setContentDescription(summaryContentDescription); 373 374 if (undisplayedSuggestionCount == 0) { 375 holder.summary.setText(null); 376 } else { 377 holder.summary.setText( 378 mContext.getString(R.string.suggestions_summary, undisplayedSuggestionCount)); 379 } 380 holder.itemView.setOnClickListener(v -> { 381 final int suggestionMode; 382 if (moreSuggestions) { 383 suggestionMode = DashboardData.SUGGESTION_MODE_EXPANDED; 384 385 for (Tile suggestion : mDashboardData.getSuggestions()) { 386 final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier( 387 mContext, suggestion); 388 if (!mSuggestionsShownLogged.contains(suggestionId)) { 389 mMetricsFeatureProvider.action( 390 mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, 391 suggestionId); 392 mSuggestionsShownLogged.add(suggestionId); 393 } 394 } 395 } else { 396 suggestionMode = DashboardData.SUGGESTION_MODE_COLLAPSED; 397 } 398 399 DashboardData prevData = mDashboardData; 400 mDashboardData = new DashboardData.Builder(prevData) 401 .setSuggestionMode(suggestionMode) 402 .build(); 403 notifyDashboardDataChanged(prevData); 404 }); 405 } 406 onBindTile(DashboardItemHolder holder, Tile tile)407 private void onBindTile(DashboardItemHolder holder, Tile tile) { 408 holder.icon.setImageDrawable(mCache.getIcon(tile.icon)); 409 holder.title.setText(tile.title); 410 if (!TextUtils.isEmpty(tile.summary)) { 411 holder.summary.setText(tile.summary); 412 holder.summary.setVisibility(View.VISIBLE); 413 } else { 414 holder.summary.setVisibility(View.GONE); 415 } 416 } 417 onBindCategory(DashboardItemHolder holder, DashboardCategory category)418 private void onBindCategory(DashboardItemHolder holder, DashboardCategory category) { 419 holder.title.setText(category.title); 420 } 421 onSaveInstanceState(Bundle outState)422 void onSaveInstanceState(Bundle outState) { 423 final List<Tile> suggestions = mDashboardData.getSuggestions(); 424 final List<DashboardCategory> categories = mDashboardData.getCategories(); 425 if (suggestions != null) { 426 outState.putParcelableArrayList(STATE_SUGGESTION_LIST, new ArrayList<>(suggestions)); 427 } 428 if (categories != null) { 429 outState.putParcelableArrayList(STATE_CATEGORY_LIST, new ArrayList<>(categories)); 430 } 431 outState.putInt(STATE_SUGGESTION_MODE, mDashboardData.getSuggestionMode()); 432 outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged); 433 } 434 435 private static class IconCache { 436 private final Context mContext; 437 private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>(); 438 IconCache(Context context)439 public IconCache(Context context) { 440 mContext = context; 441 } 442 getIcon(Icon icon)443 public Drawable getIcon(Icon icon) { 444 Drawable drawable = mMap.get(icon); 445 if (drawable == null) { 446 drawable = icon.loadDrawable(mContext); 447 mMap.put(icon, drawable); 448 } 449 return drawable; 450 } 451 } 452 453 public static class DashboardItemHolder extends RecyclerView.ViewHolder { 454 public final ImageView icon; 455 public final TextView title; 456 public final TextView summary; 457 DashboardItemHolder(View itemView)458 public DashboardItemHolder(View itemView) { 459 super(itemView); 460 icon = itemView.findViewById(android.R.id.icon); 461 title = itemView.findViewById(android.R.id.title); 462 summary = itemView.findViewById(android.R.id.summary); 463 } 464 } 465 } 466