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.pm.PackageManager; 21 import android.graphics.drawable.Drawable; 22 import android.os.Bundle; 23 import android.service.settings.suggestions.Suggestion; 24 import android.support.annotation.VisibleForTesting; 25 import android.support.v7.util.DiffUtil; 26 import android.support.v7.widget.LinearLayoutManager; 27 import android.support.v7.widget.RecyclerView; 28 import android.text.TextUtils; 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.LinearLayout; 35 import android.widget.TextView; 36 37 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 38 import com.android.settings.R; 39 import com.android.settings.R.id; 40 import com.android.settings.dashboard.DashboardData.ConditionHeaderData; 41 import com.android.settings.dashboard.conditional.Condition; 42 import com.android.settings.dashboard.conditional.ConditionAdapter; 43 import com.android.settings.dashboard.suggestions.SuggestionAdapter; 44 import com.android.settings.overlay.FeatureFactory; 45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 46 import com.android.settingslib.core.lifecycle.Lifecycle; 47 import com.android.settingslib.core.lifecycle.LifecycleObserver; 48 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; 49 import com.android.settingslib.drawer.DashboardCategory; 50 import com.android.settingslib.drawer.Tile; 51 import com.android.settingslib.drawer.TileUtils; 52 import com.android.settingslib.suggestions.SuggestionControllerMixin; 53 import com.android.settingslib.utils.IconCache; 54 55 import java.util.List; 56 57 public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> 58 implements SummaryLoader.SummaryConsumer, SuggestionAdapter.Callback, LifecycleObserver, 59 OnSaveInstanceState { 60 public static final String TAG = "DashboardAdapter"; 61 private static final String STATE_CATEGORY_LIST = "category_list"; 62 63 @VisibleForTesting 64 static final String STATE_CONDITION_EXPANDED = "condition_expanded"; 65 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 SuggestionAdapter mSuggestionAdapter; 73 74 @VisibleForTesting 75 DashboardData mDashboardData; 76 77 private View.OnClickListener mTileClickListener = new View.OnClickListener() { 78 @Override 79 public void onClick(View v) { 80 //TODO: get rid of setTag/getTag 81 mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag()); 82 } 83 }; 84 85 public DashboardAdapter(Context context, Bundle savedInstanceState, 86 List<Condition> conditions, SuggestionControllerMixin suggestionControllerMixin, 87 Lifecycle lifecycle) { 88 89 DashboardCategory category = null; 90 boolean conditionExpanded = false; 91 92 mContext = context; 93 final FeatureFactory factory = FeatureFactory.getFactory(context); 94 mMetricsFeatureProvider = factory.getMetricsFeatureProvider(); 95 mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context); 96 mCache = new IconCache(context); 97 mSuggestionAdapter = new SuggestionAdapter(mContext, suggestionControllerMixin, 98 savedInstanceState, this /* callback */, lifecycle); 99 100 setHasStableIds(true); 101 102 if (savedInstanceState != null) { 103 category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST); 104 conditionExpanded = savedInstanceState.getBoolean( 105 STATE_CONDITION_EXPANDED, conditionExpanded); 106 } 107 108 if (lifecycle != null) { 109 lifecycle.addObserver(this); 110 } 111 112 mDashboardData = new DashboardData.Builder() 113 .setConditions(conditions) 114 .setSuggestions(mSuggestionAdapter.getSuggestions()) 115 .setCategory(category) 116 .setConditionExpanded(conditionExpanded) 117 .build(); 118 } 119 120 public void setSuggestions(List<Suggestion> data) { 121 final DashboardData prevData = mDashboardData; 122 mDashboardData = new DashboardData.Builder(prevData) 123 .setSuggestions(data) 124 .build(); 125 notifyDashboardDataChanged(prevData); 126 } 127 128 public void setCategory(DashboardCategory category) { 129 final DashboardData prevData = mDashboardData; 130 Log.d(TAG, "adapter setCategory called"); 131 mDashboardData = new DashboardData.Builder(prevData) 132 .setCategory(category) 133 .build(); 134 notifyDashboardDataChanged(prevData); 135 } 136 137 public void setConditions(List<Condition> conditions) { 138 final DashboardData prevData = mDashboardData; 139 Log.d(TAG, "adapter setConditions called"); 140 mDashboardData = new DashboardData.Builder(prevData) 141 .setConditions(conditions) 142 .build(); 143 notifyDashboardDataChanged(prevData); 144 } 145 146 @Override 147 public void onSuggestionClosed(Suggestion suggestion) { 148 final List<Suggestion> list = mDashboardData.getSuggestions(); 149 if (list == null || list.size() == 0) { 150 return; 151 } 152 if (list.size() == 1) { 153 // The only suggestion is dismissed, and the the empty suggestion container will 154 // remain as the dashboard item. Need to refresh the dashboard list. 155 setSuggestions(null); 156 } else { 157 list.remove(suggestion); 158 setSuggestions(list); 159 } 160 } 161 162 @Override 163 public void notifySummaryChanged(Tile tile) { 164 final int position = mDashboardData.getPositionByTile(tile); 165 if (position != DashboardData.POSITION_NOT_FOUND) { 166 // Since usually tile in parameter and tile in mCategories are same instance, 167 // which is hard to be detected by DiffUtil, so we notifyItemChanged directly. 168 notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position)); 169 } 170 } 171 172 @Override 173 public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { 174 final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); 175 if (viewType == R.layout.condition_header) { 176 return new ConditionHeaderHolder(view); 177 } 178 if (viewType == R.layout.condition_container) { 179 return new ConditionContainerHolder(view); 180 } 181 if (viewType == R.layout.suggestion_container) { 182 return new SuggestionContainerHolder(view); 183 } 184 return new DashboardItemHolder(view); 185 } 186 187 @Override 188 public void onBindViewHolder(DashboardItemHolder holder, int position) { 189 final int type = mDashboardData.getItemTypeByPosition(position); 190 switch (type) { 191 case R.layout.dashboard_tile: 192 final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position); 193 onBindTile(holder, tile); 194 holder.itemView.setTag(tile); 195 holder.itemView.setOnClickListener(mTileClickListener); 196 break; 197 case R.layout.suggestion_container: 198 onBindSuggestion((SuggestionContainerHolder) holder, position); 199 break; 200 case R.layout.condition_container: 201 onBindCondition((ConditionContainerHolder) holder, position); 202 break; 203 case R.layout.condition_header: 204 onBindConditionHeader((ConditionHeaderHolder) holder, 205 (ConditionHeaderData) mDashboardData.getItemEntityByPosition(position)); 206 break; 207 case R.layout.condition_footer: 208 holder.itemView.setOnClickListener(v -> { 209 mMetricsFeatureProvider.action(mContext, 210 MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false); 211 DashboardData prevData = mDashboardData; 212 mDashboardData = new DashboardData.Builder(prevData). 213 setConditionExpanded(false).build(); 214 notifyDashboardDataChanged(prevData); 215 scrollToTopOfConditions(); 216 }); 217 break; 218 } 219 } 220 221 @Override 222 public long getItemId(int position) { 223 return mDashboardData.getItemIdByPosition(position); 224 } 225 226 @Override 227 public int getItemViewType(int position) { 228 return mDashboardData.getItemTypeByPosition(position); 229 } 230 231 @Override 232 public int getItemCount() { 233 return mDashboardData.size(); 234 } 235 236 @Override 237 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 238 super.onAttachedToRecyclerView(recyclerView); 239 // save the view so that we can scroll it when expanding/collapsing the suggestion and 240 // conditions. 241 mRecyclerView = recyclerView; 242 } 243 244 public Object getItem(long itemId) { 245 return mDashboardData.getItemEntityById(itemId); 246 } 247 248 public Suggestion getSuggestion(int position) { 249 return mSuggestionAdapter.getSuggestion(position); 250 } 251 252 @VisibleForTesting 253 void notifyDashboardDataChanged(DashboardData prevData) { 254 if (mFirstFrameDrawn && prevData != null) { 255 final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData 256 .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList())); 257 diffResult.dispatchUpdatesTo(this); 258 } else { 259 mFirstFrameDrawn = true; 260 notifyDataSetChanged(); 261 } 262 } 263 264 @VisibleForTesting 265 void onBindConditionHeader(final ConditionHeaderHolder holder, ConditionHeaderData data) { 266 holder.icon.setImageDrawable(data.conditionIcons.get(0)); 267 if (data.conditionCount == 1) { 268 holder.title.setText(data.title); 269 holder.summary.setText(null); 270 holder.icons.setVisibility(View.INVISIBLE); 271 } else { 272 holder.title.setText(null); 273 holder.summary.setText( 274 mContext.getString(R.string.condition_summary, data.conditionCount)); 275 updateConditionIcons(data.conditionIcons, holder.icons); 276 holder.icons.setVisibility(View.VISIBLE); 277 } 278 279 holder.itemView.setOnClickListener(v -> { 280 mMetricsFeatureProvider.action(mContext, 281 MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true); 282 final DashboardData prevData = mDashboardData; 283 mDashboardData = new DashboardData.Builder(prevData) 284 .setConditionExpanded(true).build(); 285 notifyDashboardDataChanged(prevData); 286 scrollToTopOfConditions(); 287 }); 288 } 289 290 @VisibleForTesting 291 void onBindCondition(final ConditionContainerHolder holder, int position) { 292 final ConditionAdapter adapter = new ConditionAdapter(mContext, 293 (List<Condition>) mDashboardData.getItemEntityByPosition(position), 294 mDashboardData.isConditionExpanded()); 295 adapter.addDismissHandling(holder.data); 296 holder.data.setAdapter(adapter); 297 holder.data.setLayoutManager(new LinearLayoutManager(mContext)); 298 } 299 300 @VisibleForTesting 301 void onBindSuggestion(final SuggestionContainerHolder holder, int position) { 302 // If there is suggestions to show, it will be at position 0 as we don't show the suggestion 303 // header anymore. 304 final List<Suggestion> suggestions = 305 (List<Suggestion>) mDashboardData.getItemEntityByPosition(position); 306 if (suggestions != null && suggestions.size() > 0) { 307 mSuggestionAdapter.setSuggestions(suggestions); 308 holder.data.setAdapter(mSuggestionAdapter); 309 } 310 final LinearLayoutManager layoutManager = new LinearLayoutManager(mContext); 311 layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); 312 holder.data.setLayoutManager(layoutManager); 313 } 314 315 @VisibleForTesting 316 void onBindTile(DashboardItemHolder holder, Tile tile) { 317 Drawable icon = mCache.getIcon(tile.icon); 318 if (!TextUtils.equals(tile.icon.getResPackage(), mContext.getPackageName()) 319 && !(icon instanceof RoundedHomepageIcon)) { 320 icon = new RoundedHomepageIcon(mContext, icon); 321 try { 322 if (tile.metaData != null) { 323 final int colorRes = tile.metaData.getInt( 324 TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, 0 /* default */); 325 if (colorRes != 0) { 326 final int bgColor = mContext.getPackageManager() 327 .getResourcesForApplication(tile.icon.getResPackage()) 328 .getColor(colorRes, null /* theme */); 329 ((RoundedHomepageIcon) icon).setBackgroundColor(bgColor); 330 } 331 } 332 } catch (PackageManager.NameNotFoundException e) { 333 Log.e(TAG, "Failed to set background color for " + tile.intent.getPackage()); 334 } 335 mCache.updateIcon(tile.icon, icon); 336 } 337 holder.icon.setImageDrawable(icon); 338 holder.title.setText(tile.title); 339 if (!TextUtils.isEmpty(tile.summary)) { 340 holder.summary.setText(tile.summary); 341 holder.summary.setVisibility(View.VISIBLE); 342 } else { 343 holder.summary.setVisibility(View.GONE); 344 } 345 } 346 347 @Override 348 public void onSaveInstanceState(Bundle outState) { 349 final DashboardCategory category = mDashboardData.getCategory(); 350 if (category != null) { 351 outState.putParcelable(STATE_CATEGORY_LIST, category); 352 } 353 outState.putBoolean(STATE_CONDITION_EXPANDED, mDashboardData.isConditionExpanded()); 354 } 355 356 private void updateConditionIcons(List<Drawable> icons, ViewGroup parent) { 357 if (icons == null || icons.size() < 2) { 358 parent.setVisibility(View.INVISIBLE); 359 return; 360 } 361 final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 362 parent.removeAllViews(); 363 for (int i = 1, size = icons.size(); i < size; i++) { 364 ImageView icon = (ImageView) inflater.inflate( 365 R.layout.condition_header_icon, parent, false); 366 icon.setImageDrawable(icons.get(i)); 367 parent.addView(icon); 368 } 369 parent.setVisibility(View.VISIBLE); 370 } 371 372 private void scrollToTopOfConditions() { 373 mRecyclerView.scrollToPosition(mDashboardData.hasSuggestion() ? 1 : 0); 374 } 375 376 public static class DashboardItemHolder extends RecyclerView.ViewHolder { 377 public final ImageView icon; 378 public final TextView title; 379 public final TextView summary; 380 381 public DashboardItemHolder(View itemView) { 382 super(itemView); 383 icon = itemView.findViewById(android.R.id.icon); 384 title = itemView.findViewById(android.R.id.title); 385 summary = itemView.findViewById(android.R.id.summary); 386 } 387 } 388 389 public static class ConditionHeaderHolder extends DashboardItemHolder { 390 public final LinearLayout icons; 391 public final ImageView expandIndicator; 392 393 public ConditionHeaderHolder(View itemView) { 394 super(itemView); 395 icons = itemView.findViewById(id.additional_icons); 396 expandIndicator = itemView.findViewById(id.expand_indicator); 397 } 398 } 399 400 public static class ConditionContainerHolder extends DashboardItemHolder { 401 public final RecyclerView data; 402 403 public ConditionContainerHolder(View itemView) { 404 super(itemView); 405 data = itemView.findViewById(id.data); 406 } 407 } 408 409 public static class SuggestionContainerHolder extends DashboardItemHolder { 410 public final RecyclerView data; 411 412 public SuggestionContainerHolder(View itemView) { 413 super(itemView); 414 data = itemView.findViewById(id.suggestion_list); 415 } 416 } 417 418 } 419