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
17package com.android.settings.applications;
18
19import static com.google.common.truth.Truth.assertThat;
20import static org.mockito.Matchers.any;
21import static org.mockito.Matchers.anyInt;
22import static org.mockito.Matchers.anyLong;
23import static org.mockito.Matchers.anyString;
24import static org.mockito.Matchers.argThat;
25import static org.mockito.Mockito.doNothing;
26import static org.mockito.Mockito.doReturn;
27import static org.mockito.Mockito.mock;
28import static org.mockito.Mockito.never;
29import static org.mockito.Mockito.spy;
30import static org.mockito.Mockito.times;
31import static org.mockito.Mockito.verify;
32import static org.mockito.Mockito.when;
33
34import android.app.Application;
35import android.app.usage.UsageStats;
36import android.app.usage.UsageStatsManager;
37import android.content.Context;
38import android.content.Intent;
39import android.content.pm.ApplicationInfo;
40import android.content.pm.PackageManager;
41import android.content.pm.ResolveInfo;
42import android.os.UserHandle;
43import android.os.UserManager;
44import android.support.v7.preference.Preference;
45import android.support.v7.preference.PreferenceCategory;
46import android.support.v7.preference.PreferenceScreen;
47import android.text.TextUtils;
48
49import com.android.settings.R;
50import com.android.settings.testutils.SettingsRobolectricTestRunner;
51import com.android.settingslib.applications.AppUtils;
52import com.android.settingslib.applications.ApplicationsState;
53import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
54
55import org.junit.Before;
56import org.junit.Test;
57import org.junit.runner.RunWith;
58import org.mockito.ArgumentCaptor;
59import org.mockito.ArgumentMatcher;
60import org.mockito.Mock;
61import org.mockito.MockitoAnnotations;
62import org.robolectric.RuntimeEnvironment;
63import org.robolectric.annotation.Config;
64import org.robolectric.util.ReflectionHelpers;
65
66import java.util.ArrayList;
67import java.util.List;
68
69@RunWith(SettingsRobolectricTestRunner.class)
70public class RecentAppsPreferenceControllerTest {
71
72    @Mock
73    private PreferenceScreen mScreen;
74    @Mock
75    private PreferenceCategory mCategory;
76    @Mock
77    private Preference mSeeAllPref;
78    @Mock
79    private PreferenceCategory mDivider;
80    @Mock
81    private UsageStatsManager mUsageStatsManager;
82    @Mock
83    private UserManager mUserManager;
84    @Mock
85    private ApplicationsState mAppState;
86    @Mock
87    private PackageManager mPackageManager;
88    @Mock
89    private ApplicationsState.AppEntry mAppEntry;
90    @Mock
91    private ApplicationInfo mApplicationInfo;
92
93    private Context mContext;
94    private RecentAppsPreferenceController mController;
95
96    @Before
97    public void setUp() {
98        MockitoAnnotations.initMocks(this);
99        mContext = spy(RuntimeEnvironment.application);
100        doReturn(mUsageStatsManager).when(mContext).getSystemService(Context.USAGE_STATS_SERVICE);
101        doReturn(mUserManager).when(mContext).getSystemService(Context.USER_SERVICE);
102        doReturn(mPackageManager).when(mContext).getPackageManager();
103
104        mController = new RecentAppsPreferenceController(mContext, mAppState, null);
105        when(mScreen.findPreference(anyString())).thenReturn(mCategory);
106
107        when(mScreen.findPreference(RecentAppsPreferenceController.KEY_SEE_ALL))
108                .thenReturn(mSeeAllPref);
109        when(mScreen.findPreference(RecentAppsPreferenceController.KEY_DIVIDER))
110                .thenReturn(mDivider);
111        when(mCategory.getContext()).thenReturn(mContext);
112    }
113
114    @Test
115    public void isAlwaysAvailable() {
116        assertThat(mController.isAvailable()).isTrue();
117    }
118
119    @Test
120    public void doNotIndexCategory() {
121        final List<String> nonIndexable = new ArrayList<>();
122
123        mController.updateNonIndexableKeys(nonIndexable);
124
125        assertThat(nonIndexable).containsAllOf(mController.getPreferenceKey(),
126                RecentAppsPreferenceController.KEY_DIVIDER);
127    }
128
129    @Test
130    public void onDisplayAndUpdateState_shouldRefreshUi() {
131        mController = spy(new RecentAppsPreferenceController(mContext, (Application) null, null));
132
133        doNothing().when(mController).refreshUi(mContext);
134
135        mController.displayPreference(mScreen);
136        mController.updateState(mCategory);
137
138        verify(mController, times(2)).refreshUi(mContext);
139    }
140
141    @Test
142    @Config(qualifiers = "mcc999")
143    public void display_shouldNotShowRecents_showAppInfoPreference() {
144        mController.displayPreference(mScreen);
145
146        verify(mCategory, never()).addPreference(any(Preference.class));
147        verify(mCategory).setTitle(null);
148        verify(mSeeAllPref).setTitle(R.string.applications_settings);
149        verify(mSeeAllPref).setIcon(null);
150        verify(mDivider).setVisible(false);
151    }
152
153    @Test
154    public void display_showRecents() {
155        final List<UsageStats> stats = new ArrayList<>();
156        final UsageStats stat1 = new UsageStats();
157        final UsageStats stat2 = new UsageStats();
158        final UsageStats stat3 = new UsageStats();
159        stat1.mLastTimeUsed = System.currentTimeMillis();
160        stat1.mPackageName = "pkg.class";
161        stats.add(stat1);
162
163        stat2.mLastTimeUsed = System.currentTimeMillis();
164        stat2.mPackageName = "com.android.settings";
165        stats.add(stat2);
166
167        stat3.mLastTimeUsed = System.currentTimeMillis();
168        stat3.mPackageName = "pkg.class2";
169        stats.add(stat3);
170
171        // stat1, stat2 are valid apps. stat3 is invalid.
172        when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
173            .thenReturn(mAppEntry);
174        when(mAppState.getEntry(stat2.mPackageName, UserHandle.myUserId()))
175            .thenReturn(mAppEntry);
176        when(mAppState.getEntry(stat3.mPackageName, UserHandle.myUserId()))
177            .thenReturn(null);
178        when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
179            .thenReturn(new ResolveInfo());
180        when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
181            .thenReturn(stats);
182        mAppEntry.info = mApplicationInfo;
183
184        mController.displayPreference(mScreen);
185
186        verify(mCategory).setTitle(R.string.recent_app_category_title);
187        // Only add stat1. stat2 is skipped because of the package name, stat3 skipped because
188        // it's invalid app.
189        verify(mCategory, times(1)).addPreference(any(Preference.class));
190
191        verify(mSeeAllPref).setSummary(null);
192        verify(mSeeAllPref).setIcon(R.drawable.ic_chevron_right_24dp);
193        verify(mDivider).setVisible(true);
194    }
195
196    @Test
197    public void display_showRecentsWithInstantApp() {
198        // Regular app.
199        final List<UsageStats> stats = new ArrayList<>();
200        final UsageStats stat1 = new UsageStats();
201        stat1.mLastTimeUsed = System.currentTimeMillis();
202        stat1.mPackageName = "com.foo.bar";
203        stats.add(stat1);
204
205        // Instant app.
206        final UsageStats stat2 = new UsageStats();
207        stat2.mLastTimeUsed = System.currentTimeMillis() + 200;
208        stat2.mPackageName = "com.foo.barinstant";
209        stats.add(stat2);
210
211        ApplicationsState.AppEntry stat1Entry = mock(ApplicationsState.AppEntry.class);
212        ApplicationsState.AppEntry stat2Entry = mock(ApplicationsState.AppEntry.class);
213        stat1Entry.info = mApplicationInfo;
214        stat2Entry.info = mApplicationInfo;
215
216        when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId())).thenReturn(stat1Entry);
217        when(mAppState.getEntry(stat2.mPackageName, UserHandle.myUserId())).thenReturn(stat2Entry);
218
219        // Only the regular app stat1 should have its intent resolve.
220        when(mPackageManager.resolveActivity(argThat(intentMatcher(stat1.mPackageName)), anyInt()))
221            .thenReturn(new ResolveInfo());
222
223        when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
224                .thenReturn(stats);
225
226        // Make sure stat2 is considered an instant app.
227        ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
228                (InstantAppDataProvider) (ApplicationInfo info) -> info == stat2Entry.info);
229
230        mController.displayPreference(mScreen);
231
232        ArgumentCaptor<Preference> prefCaptor = ArgumentCaptor.forClass(Preference.class);
233        verify(mCategory, times(2)).addPreference(prefCaptor.capture());
234        List<Preference> prefs = prefCaptor.getAllValues();
235        assertThat(prefs.get(1).getKey()).isEqualTo(stat1.mPackageName);
236        assertThat(prefs.get(0).getKey()).isEqualTo(stat2.mPackageName);
237    }
238
239    @Test
240    public void display_showRecentsWithNullAppEntryOrInfo() {
241        final List<UsageStats> stats = new ArrayList<>();
242        final UsageStats stat1 = new UsageStats();
243        final UsageStats stat2 = new UsageStats();
244        stat1.mLastTimeUsed = System.currentTimeMillis();
245        stat1.mPackageName = "pkg.class";
246        stats.add(stat1);
247
248        stat2.mLastTimeUsed = System.currentTimeMillis();
249        stat2.mPackageName = "pkg.class2";
250        stats.add(stat2);
251
252        // app1 has AppEntry with null info, app2 has null AppEntry.
253        mAppEntry.info = null;
254        when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
255                .thenReturn(mAppEntry);
256        when(mAppState.getEntry(stat2.mPackageName, UserHandle.myUserId()))
257                .thenReturn(null);
258
259        when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
260                .thenReturn(stats);
261
262        // We should not crash here.
263        mController.displayPreference(mScreen);
264    }
265
266    @Test
267    public void display_hasRecentButNoneDisplayable_showAppInfo() {
268        final List<UsageStats> stats = new ArrayList<>();
269        final UsageStats stat1 = new UsageStats();
270        final UsageStats stat2 = new UsageStats();
271        stat1.mLastTimeUsed = System.currentTimeMillis();
272        stat1.mPackageName = "com.android.phone";
273        stats.add(stat1);
274
275        stat2.mLastTimeUsed = System.currentTimeMillis();
276        stat2.mPackageName = "com.android.settings";
277        stats.add(stat2);
278
279        // stat1, stat2 are not displayable
280        when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
281            .thenReturn(mock(ApplicationsState.AppEntry.class));
282        when(mAppState.getEntry(stat2.mPackageName, UserHandle.myUserId()))
283            .thenReturn(mock(ApplicationsState.AppEntry.class));
284        when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
285            .thenReturn(new ResolveInfo());
286        when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
287            .thenReturn(stats);
288
289        mController.displayPreference(mScreen);
290
291        verify(mCategory, never()).addPreference(any(Preference.class));
292        verify(mCategory).setTitle(null);
293        verify(mSeeAllPref).setTitle(R.string.applications_settings);
294        verify(mSeeAllPref).setIcon(null);
295    }
296
297    @Test
298    public void display_showRecents_formatSummary() {
299        final UsageStats stat1 = new UsageStats();
300        stat1.mLastTimeUsed = System.currentTimeMillis();
301        stat1.mPackageName = "pkg.class";
302        final List<UsageStats> stats = new ArrayList<>();
303        stats.add(stat1);
304
305        when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
306            .thenReturn(mAppEntry);
307        when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
308            .thenReturn(new ResolveInfo());
309        when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
310            .thenReturn(stats);
311        mAppEntry.info = mApplicationInfo;
312
313        mController.displayPreference(mScreen);
314
315        verify(mCategory).addPreference(argThat(summaryMatches("0 minutes ago")));
316    }
317
318    private static ArgumentMatcher<Preference> summaryMatches(String expected) {
319        return preference -> TextUtils.equals(expected, preference.getSummary());
320    }
321
322    // Used for matching an intent with a specific package name.
323    private static ArgumentMatcher<Intent> intentMatcher(String packageName) {
324        return intent -> packageName.equals(intent.getPackage());
325    }
326}
327