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 static com.google.common.truth.Truth.assertThat; 19 import static org.mockito.ArgumentMatchers.any; 20 import static org.mockito.ArgumentMatchers.anyInt; 21 import static org.mockito.Mockito.doReturn; 22 import static org.mockito.Mockito.mock; 23 import static org.mockito.Mockito.never; 24 import static org.mockito.Mockito.spy; 25 import static org.mockito.Mockito.verify; 26 import static org.mockito.Mockito.when; 27 28 import android.app.PendingIntent; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.content.res.TypedArray; 32 import android.graphics.drawable.Drawable; 33 import android.graphics.drawable.Icon; 34 import android.service.settings.suggestions.Suggestion; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.WindowManager; 38 import android.widget.FrameLayout; 39 import android.widget.LinearLayout; 40 41 import com.android.internal.logging.nano.MetricsProto; 42 import com.android.settings.R; 43 import com.android.settings.SettingsActivity; 44 import com.android.settings.dashboard.DashboardAdapter; 45 import com.android.settings.testutils.FakeFeatureFactory; 46 import com.android.settings.testutils.SettingsRobolectricTestRunner; 47 import com.android.settings.testutils.shadow.ShadowCardView; 48 import com.android.settingslib.suggestions.SuggestionControllerMixin; 49 import com.android.settingslib.utils.IconCache; 50 51 import org.junit.Before; 52 import org.junit.Test; 53 import org.junit.runner.RunWith; 54 import org.mockito.Answers; 55 import org.mockito.Mock; 56 import org.mockito.MockitoAnnotations; 57 import org.robolectric.RuntimeEnvironment; 58 import org.robolectric.annotation.Config; 59 import org.robolectric.util.ReflectionHelpers; 60 61 import java.util.ArrayList; 62 import java.util.List; 63 64 @RunWith(SettingsRobolectricTestRunner.class) 65 @Config(shadows = ShadowCardView.class) 66 public class SuggestionAdapterTest { 67 68 @Mock(answer = Answers.RETURNS_DEEP_STUBS) 69 private SettingsActivity mActivity; 70 @Mock 71 private SuggestionControllerMixin mSuggestionControllerMixin; 72 @Mock 73 private Resources mResources; 74 @Mock 75 private WindowManager mWindowManager; 76 77 private FakeFeatureFactory mFeatureFactory; 78 private Context mContext; 79 private SuggestionAdapter mSuggestionAdapter; 80 private DashboardAdapter.DashboardItemHolder mSuggestionHolder; 81 private List<Suggestion> mOneSuggestion; 82 private List<Suggestion> mTwoSuggestions; 83 private SuggestionAdapter.CardConfig mConfig; 84 85 @Before 86 public void setUp() { 87 MockitoAnnotations.initMocks(this); 88 mContext = spy(RuntimeEnvironment.application); 89 when(mActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mWindowManager); 90 when(mActivity.getResources()).thenReturn(mResources); 91 when(mResources.getDimensionPixelOffset(R.dimen.suggestion_card_inner_margin)) 92 .thenReturn(10); 93 when(mResources.getDimensionPixelOffset(R.dimen.suggestion_card_outer_margin)) 94 .thenReturn(20); 95 mConfig = spy(SuggestionAdapter.CardConfig.get(mActivity)); 96 97 mFeatureFactory = FakeFeatureFactory.setupForTest(); 98 99 final Suggestion suggestion1 = new Suggestion.Builder("id1") 100 .setTitle("Test suggestion 1") 101 .build(); 102 final Suggestion suggestion2 = new Suggestion.Builder("id2") 103 .setTitle("Test suggestion 2") 104 .build(); 105 mOneSuggestion = new ArrayList<>(); 106 mOneSuggestion.add(suggestion1); 107 mTwoSuggestions = new ArrayList<>(); 108 mTwoSuggestions.add(suggestion1); 109 mTwoSuggestions.add(suggestion2); 110 } 111 112 @Test 113 public void getItemCount_shouldReturnListSize() { 114 mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin, 115 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 116 mSuggestionAdapter.setSuggestions(mOneSuggestion); 117 assertThat(mSuggestionAdapter.getItemCount()).isEqualTo(1); 118 119 mSuggestionAdapter.setSuggestions(mTwoSuggestions); 120 assertThat(mSuggestionAdapter.getItemCount()).isEqualTo(2); 121 } 122 123 @Test 124 public void getItemViewType_shouldReturnSuggestionTile() { 125 mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin, 126 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 127 mSuggestionAdapter.setSuggestions(mOneSuggestion); 128 assertThat(mSuggestionAdapter.getItemViewType(0)) 129 .isEqualTo(R.layout.suggestion_tile); 130 } 131 132 @Test 133 public void getItemType_hasButton_shouldReturnSuggestionWithButton() { 134 final List<Suggestion> suggestions = new ArrayList<>(); 135 suggestions.add(new Suggestion.Builder("id") 136 .setFlags(Suggestion.FLAG_HAS_BUTTON) 137 .setTitle("123") 138 .setSummary("456") 139 .build()); 140 mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin, 141 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 142 mSuggestionAdapter.setSuggestions(suggestions); 143 144 assertThat(mSuggestionAdapter.getItemViewType(0)) 145 .isEqualTo(R.layout.suggestion_tile_with_button); 146 } 147 148 @Test 149 public void onBindViewHolder_shouldLog() { 150 final View view = spy(LayoutInflater.from(mContext).inflate( 151 R.layout.suggestion_tile, new LinearLayout(mContext), true)); 152 mSuggestionHolder = new DashboardAdapter.DashboardItemHolder(view); 153 mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin, 154 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 155 mSuggestionAdapter.setSuggestions(mOneSuggestion); 156 doReturn("sans").when(mContext).getString(anyInt()); 157 158 // Bind twice 159 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 160 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 161 162 // Log once 163 verify(mFeatureFactory.metricsFeatureProvider).action( 164 mContext, MetricsProto.MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, 165 mOneSuggestion.get(0).getId()); 166 } 167 168 @Test 169 public void onBindViewHolder_itemViewShouldHandleClick() 170 throws PendingIntent.CanceledException { 171 final List<Suggestion> suggestions = makeSuggestions("pkg1"); 172 setupSuggestions(mActivity, suggestions); 173 174 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 175 mSuggestionHolder.itemView.performClick(); 176 177 verify(mSuggestionControllerMixin).launchSuggestion(suggestions.get(0)); 178 verify(suggestions.get(0).getPendingIntent()).send(); 179 } 180 181 @Test 182 public void onBindViewHolder_hasButton_buttonShouldHandleClick() 183 throws PendingIntent.CanceledException { 184 final List<Suggestion> suggestions = new ArrayList<>(); 185 final PendingIntent pendingIntent = mock(PendingIntent.class); 186 suggestions.add(new Suggestion.Builder("id") 187 .setFlags(Suggestion.FLAG_HAS_BUTTON) 188 .setTitle("123") 189 .setSummary("456") 190 .setPendingIntent(pendingIntent) 191 .build()); 192 mSuggestionAdapter = new SuggestionAdapter(mContext, mSuggestionControllerMixin, 193 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 194 mSuggestionAdapter.setSuggestions(suggestions); 195 mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder( 196 new FrameLayout(RuntimeEnvironment.application), 197 mSuggestionAdapter.getItemViewType(0)); 198 doReturn("sans").when(mContext).getString(anyInt()); 199 200 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 201 mSuggestionHolder.itemView.findViewById(android.R.id.primary).performClick(); 202 203 verify(mSuggestionControllerMixin).launchSuggestion(suggestions.get(0)); 204 verify(pendingIntent).send(); 205 } 206 207 @Test 208 public void getSuggestions_shouldReturnSuggestionWhenMatch() { 209 final List<Suggestion> suggestions = makeSuggestions("pkg1"); 210 setupSuggestions(mActivity, suggestions); 211 212 assertThat(mSuggestionAdapter.getSuggestion(0)).isNotNull(); 213 } 214 215 @Test 216 public void onBindViewHolder_closeButtonShouldHandleClick() 217 throws PendingIntent.CanceledException { 218 final List<Suggestion> suggestions = makeSuggestions("pkg1"); 219 final SuggestionAdapter.Callback callback = mock(SuggestionAdapter.Callback.class); 220 mSuggestionAdapter = new SuggestionAdapter(mActivity, mSuggestionControllerMixin, 221 null /* savedInstanceState */, callback, null /* lifecycle */); 222 mSuggestionAdapter.setSuggestions(suggestions); 223 mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder( 224 new FrameLayout(RuntimeEnvironment.application), 225 mSuggestionAdapter.getItemViewType(0)); 226 227 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 228 mSuggestionHolder.itemView.findViewById(R.id.close_button).performClick(); 229 230 final Suggestion suggestion = suggestions.get(0); 231 verify(mFeatureFactory.suggestionsFeatureProvider).dismissSuggestion( 232 mActivity, mSuggestionControllerMixin, suggestion); 233 verify(callback).onSuggestionClosed(suggestion); 234 } 235 236 @Test 237 public void onBindViewHolder_iconNotTintable_shouldNotTintIcon() 238 throws PendingIntent.CanceledException { 239 final Icon icon = mock(Icon.class); 240 final Suggestion suggestion = new Suggestion.Builder("pkg1") 241 .setPendingIntent(mock(PendingIntent.class)) 242 .setIcon(icon) 243 .build(); 244 final List<Suggestion> suggestions = new ArrayList<>(); 245 suggestions.add(suggestion); 246 mSuggestionAdapter = new SuggestionAdapter(mActivity, mSuggestionControllerMixin, 247 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 248 mSuggestionAdapter.setSuggestions(suggestions); 249 mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder( 250 new FrameLayout(RuntimeEnvironment.application), 251 mSuggestionAdapter.getItemViewType(0)); 252 IconCache cache = mock(IconCache.class); 253 final Drawable drawable = mock(Drawable.class); 254 when(cache.getIcon(icon)).thenReturn(drawable); 255 ReflectionHelpers.setField(mSuggestionAdapter, "mCache", cache); 256 257 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 258 259 verify(drawable, never()).setTint(anyInt()); 260 } 261 262 @Test 263 public void onBindViewHolder_iconTintable_shouldTintIcon() 264 throws PendingIntent.CanceledException { 265 final Icon icon = mock(Icon.class); 266 final int FLAG_ICON_TINTABLE = 1 << 1; 267 final Suggestion suggestion = new Suggestion.Builder("pkg1") 268 .setPendingIntent(mock(PendingIntent.class)) 269 .setIcon(icon) 270 .setFlags(FLAG_ICON_TINTABLE) 271 .build(); 272 final List<Suggestion> suggestions = new ArrayList<>(); 273 suggestions.add(suggestion); 274 mSuggestionAdapter = new SuggestionAdapter(mActivity, mSuggestionControllerMixin, 275 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 276 mSuggestionAdapter.setSuggestions(suggestions); 277 mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder( 278 new FrameLayout(RuntimeEnvironment.application), 279 mSuggestionAdapter.getItemViewType(0)); 280 IconCache cache = mock(IconCache.class); 281 final Drawable drawable = mock(Drawable.class); 282 when(cache.getIcon(icon)).thenReturn(drawable); 283 ReflectionHelpers.setField(mSuggestionAdapter, "mCache", cache); 284 TypedArray typedArray = mock(TypedArray.class); 285 final int colorAccent = 1234; 286 when(mActivity.obtainStyledAttributes(any())).thenReturn(typedArray); 287 when(typedArray.getColor(anyInt(), anyInt())).thenReturn(colorAccent); 288 289 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 290 291 verify(drawable).setTint(colorAccent); 292 } 293 294 @Test 295 public void onBindViewHolder_closeButtonShouldHaveContentDescription() 296 throws PendingIntent.CanceledException { 297 final List<Suggestion> suggestions = makeSuggestions("pkg1"); 298 setupSuggestions(mActivity, suggestions); 299 300 mSuggestionAdapter.onBindViewHolder(mSuggestionHolder, 0); 301 302 assertThat( 303 mSuggestionHolder.itemView.findViewById(R.id.close_button).getContentDescription()) 304 .isNotNull(); 305 } 306 307 @Test 308 public void setCardLayout_twoCards_shouldSetCardWidthToHalfScreenMinusPadding() { 309 final List<Suggestion> suggestions = makeSuggestions("pkg1"); 310 setupSuggestions(mContext, suggestions); 311 doReturn(200).when(mConfig).getScreenWidth(); 312 313 mConfig.setCardLayout(mSuggestionHolder, 0); 314 315 /* 316 * card width = (screen width - left margin - inner margin - right margin) / 2 317 * = (200 - 20 - 10 - 20) / 2 318 * = 75 319 */ 320 assertThat(mSuggestionHolder.itemView.getLayoutParams().width).isEqualTo(75); 321 } 322 323 private void setupSuggestions(Context context, List<Suggestion> suggestions) { 324 mSuggestionAdapter = new SuggestionAdapter(context, mSuggestionControllerMixin, 325 null /* savedInstanceState */, null /* callback */, null /* lifecycle */); 326 mSuggestionAdapter.setSuggestions(suggestions); 327 mSuggestionHolder = mSuggestionAdapter.onCreateViewHolder( 328 new FrameLayout(RuntimeEnvironment.application), 329 mSuggestionAdapter.getItemViewType(0)); 330 } 331 332 private List<Suggestion> makeSuggestions(String... pkgNames) { 333 final List<Suggestion> suggestions = new ArrayList<>(); 334 for (String pkgName : pkgNames) { 335 final Suggestion suggestion = new Suggestion.Builder(pkgName) 336 .setPendingIntent(mock(PendingIntent.class)) 337 .build(); 338 suggestions.add(suggestion); 339 } 340 return suggestions; 341 } 342 } 343