1 /* 2 * Copyright (C) 2017 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.suggestions; 17 18 import android.app.PendingIntent; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Typeface; 22 import android.graphics.drawable.Drawable; 23 import android.graphics.drawable.Icon; 24 import android.os.Bundle; 25 import android.service.settings.suggestions.Suggestion; 26 import android.support.annotation.VisibleForTesting; 27 import android.support.v7.widget.RecyclerView; 28 import android.text.TextUtils; 29 import android.util.DisplayMetrics; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.WindowManager; 35 import android.widget.ImageView; 36 import android.widget.LinearLayout; 37 38 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 39 import com.android.settings.R; 40 import com.android.settings.dashboard.DashboardAdapter.DashboardItemHolder; 41 import com.android.settings.overlay.FeatureFactory; 42 import com.android.settingslib.Utils; 43 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 44 import com.android.settingslib.core.lifecycle.Lifecycle; 45 import com.android.settingslib.core.lifecycle.LifecycleObserver; 46 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; 47 import com.android.settingslib.suggestions.SuggestionControllerMixin; 48 import com.android.settingslib.utils.IconCache; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.Objects; 53 54 public class SuggestionAdapter extends RecyclerView.Adapter<DashboardItemHolder> implements 55 LifecycleObserver, OnSaveInstanceState { 56 public static final String TAG = "SuggestionAdapter"; 57 58 private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged"; 59 private static final String STATE_SUGGESTION_LIST = "suggestion_list"; 60 61 private final Context mContext; 62 private final MetricsFeatureProvider mMetricsFeatureProvider; 63 private final IconCache mCache; 64 private final ArrayList<String> mSuggestionsShownLogged; 65 private final SuggestionFeatureProvider mSuggestionFeatureProvider; 66 private final SuggestionControllerMixin mSuggestionControllerMixin; 67 private final Callback mCallback; 68 private final CardConfig mConfig; 69 70 private List<Suggestion> mSuggestions; 71 72 public interface Callback { 73 /** 74 * Called when the close button of the suggestion card is clicked. 75 */ 76 void onSuggestionClosed(Suggestion suggestion); 77 } 78 79 public SuggestionAdapter(Context context, SuggestionControllerMixin suggestionControllerMixin, 80 Bundle savedInstanceState, Callback callback, Lifecycle lifecycle) { 81 mContext = context; 82 mSuggestionControllerMixin = suggestionControllerMixin; 83 mCache = new IconCache(context); 84 final FeatureFactory factory = FeatureFactory.getFactory(context); 85 mMetricsFeatureProvider = factory.getMetricsFeatureProvider(); 86 mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context); 87 mCallback = callback; 88 if (savedInstanceState != null) { 89 mSuggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST); 90 mSuggestionsShownLogged = savedInstanceState.getStringArrayList( 91 STATE_SUGGESTIONS_SHOWN_LOGGED); 92 } else { 93 mSuggestionsShownLogged = new ArrayList<>(); 94 } 95 96 if (lifecycle != null) { 97 lifecycle.addObserver(this); 98 } 99 mConfig = CardConfig.get(context); 100 101 setHasStableIds(true); 102 } 103 104 @Override 105 public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { 106 return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate( 107 viewType, parent, false)); 108 } 109 110 @Override 111 public void onBindViewHolder(DashboardItemHolder holder, int position) { 112 final Suggestion suggestion = mSuggestions.get(position); 113 final String id = suggestion.getId(); 114 final int suggestionCount = mSuggestions.size(); 115 if (!mSuggestionsShownLogged.contains(id)) { 116 mMetricsFeatureProvider.action( 117 mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, id); 118 mSuggestionsShownLogged.add(id); 119 } 120 final Icon icon = suggestion.getIcon(); 121 final Drawable drawable = mCache.getIcon(icon); 122 if (drawable != null && (suggestion.getFlags() & Suggestion.FLAG_ICON_TINTABLE) != 0) { 123 drawable.setTint(Utils.getColorAccent(mContext)); 124 } 125 holder.icon.setImageDrawable(drawable); 126 holder.title.setText(suggestion.getTitle()); 127 holder.title.setTypeface(Typeface.create( 128 mContext.getString(com.android.internal.R.string.config_headlineFontFamily), 129 Typeface.NORMAL)); 130 131 if (suggestionCount == 1) { 132 final CharSequence summary = suggestion.getSummary(); 133 if (!TextUtils.isEmpty(summary)) { 134 holder.summary.setText(summary); 135 holder.summary.setVisibility(View.VISIBLE); 136 } else { 137 holder.summary.setVisibility(View.GONE); 138 } 139 } else { 140 mConfig.setCardLayout(holder, position); 141 } 142 143 final View closeButton = holder.itemView.findViewById(R.id.close_button); 144 if (closeButton != null) { 145 closeButton.setOnClickListener(v -> { 146 mSuggestionFeatureProvider.dismissSuggestion( 147 mContext, mSuggestionControllerMixin, suggestion); 148 if (mCallback != null) { 149 mCallback.onSuggestionClosed(suggestion); 150 } 151 }); 152 } 153 154 View clickHandler = holder.itemView; 155 // If a view with @android:id/primary is defined, use that as the click handler 156 // instead. 157 final View primaryAction = holder.itemView.findViewById(android.R.id.primary); 158 if (primaryAction != null) { 159 clickHandler = primaryAction; 160 } 161 clickHandler.setOnClickListener(v -> { 162 mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_SUGGESTION, id); 163 try { 164 suggestion.getPendingIntent().send(); 165 mSuggestionControllerMixin.launchSuggestion(suggestion); 166 } catch (PendingIntent.CanceledException e) { 167 Log.w(TAG, "Failed to start suggestion " + suggestion.getTitle()); 168 } 169 }); 170 } 171 172 @Override 173 public long getItemId(int position) { 174 return Objects.hash(mSuggestions.get(position).getId()); 175 } 176 177 @Override 178 public int getItemViewType(int position) { 179 final Suggestion suggestion = getSuggestion(position); 180 if ((suggestion.getFlags() & Suggestion.FLAG_HAS_BUTTON) != 0) { 181 return R.layout.suggestion_tile_with_button; 182 } 183 if (getItemCount() == 1) { 184 return R.layout.suggestion_tile; 185 } 186 return R.layout.suggestion_tile_two_cards; 187 } 188 189 @Override 190 public int getItemCount() { 191 return mSuggestions.size(); 192 } 193 194 public Suggestion getSuggestion(int position) { 195 final long itemId = getItemId(position); 196 if (mSuggestions == null) { 197 return null; 198 } 199 for (Suggestion suggestion : mSuggestions) { 200 if (Objects.hash(suggestion.getId()) == itemId) { 201 return suggestion; 202 } 203 } 204 return null; 205 } 206 207 public void removeSuggestion(Suggestion suggestion) { 208 final int position = mSuggestions.indexOf(suggestion); 209 mSuggestions.remove(suggestion); 210 notifyItemRemoved(position); 211 } 212 213 @Override 214 public void onSaveInstanceState(Bundle outState) { 215 if (mSuggestions != null) { 216 outState.putParcelableArrayList(STATE_SUGGESTION_LIST, 217 new ArrayList<>(mSuggestions)); 218 } 219 outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged); 220 } 221 222 public void setSuggestions(List<Suggestion> suggestions) { 223 mSuggestions = suggestions; 224 } 225 226 public List<Suggestion> getSuggestions() { 227 return mSuggestions; 228 } 229 230 @VisibleForTesting 231 static class CardConfig { 232 // Card start/end margin 233 private final int mMarginInner; 234 private final int mMarginOuter; 235 private final WindowManager mWindowManager; 236 237 private static CardConfig sConfig; 238 239 private CardConfig(Context context) { 240 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 241 final Resources res = context.getResources(); 242 mMarginInner = 243 res.getDimensionPixelOffset(R.dimen.suggestion_card_inner_margin); 244 mMarginOuter = 245 res.getDimensionPixelOffset(R.dimen.suggestion_card_outer_margin); 246 } 247 248 public static CardConfig get(Context context) { 249 if (sConfig == null) { 250 sConfig = new CardConfig(context); 251 } 252 return sConfig; 253 } 254 255 @VisibleForTesting 256 void setCardLayout(DashboardItemHolder holder, int position) { 257 final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( 258 getWidthForTwoCrads(), LinearLayout.LayoutParams.WRAP_CONTENT); 259 params.setMarginStart(position == 0 ? mMarginOuter : mMarginInner); 260 params.setMarginEnd(position != 0 ? mMarginOuter : 0); 261 holder.itemView.setLayoutParams(params); 262 } 263 264 private int getWidthForTwoCrads() { 265 return (getScreenWidth() - mMarginInner - mMarginOuter * 2) / 2; 266 } 267 268 @VisibleForTesting 269 int getScreenWidth() { 270 final DisplayMetrics metrics = new DisplayMetrics(); 271 mWindowManager.getDefaultDisplay().getMetrics(metrics); 272 return metrics.widthPixels; 273 } 274 } 275 276 } 277