1 /*
2  * Copyright (C) 2016 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.contacts.activities;
17 
18 import static com.android.contacts.tests.ContactsMatchers.DataCursor.hasMimeType;
19 import static com.android.contacts.tests.ContactsMatchers.hasRowMatching;
20 import static com.android.contacts.tests.ContactsMatchers.hasValueForColumn;
21 
22 import static org.hamcrest.MatcherAssert.assertThat;
23 import static org.hamcrest.Matchers.allOf;
24 import static org.junit.Assert.assertTrue;
25 import static org.mockito.Matchers.anyInt;
26 import static org.mockito.Mockito.doReturn;
27 import static org.mockito.Mockito.spy;
28 
29 import android.annotation.TargetApi;
30 import android.app.Activity;
31 import android.app.Instrumentation;
32 import android.content.BroadcastReceiver;
33 import android.content.ContentProviderClient;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.pm.ActivityInfo;
38 import android.database.Cursor;
39 import android.os.Build;
40 import android.provider.ContactsContract;
41 import android.provider.ContactsContract.CommonDataKinds.Phone;
42 import android.provider.ContactsContract.Data;
43 import android.support.test.uiautomator.By;
44 import android.support.test.uiautomator.UiDevice;
45 import android.support.test.uiautomator.Until;
46 import android.telephony.TelephonyManager;
47 import android.test.mock.MockContentResolver;
48 
49 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
50 import androidx.test.InstrumentationRegistry;
51 import androidx.test.filters.LargeTest;
52 import androidx.test.filters.SdkSuppress;
53 import androidx.test.runner.AndroidJUnit4;
54 
55 import com.android.contacts.SimImportService;
56 import com.android.contacts.database.SimContactDao;
57 import com.android.contacts.database.SimContactDaoImpl;
58 import com.android.contacts.model.SimCard;
59 import com.android.contacts.model.SimContact;
60 import com.android.contacts.model.account.AccountWithDataSet;
61 import com.android.contacts.test.mocks.ForwardingContentProvider;
62 import com.android.contacts.test.mocks.MockContentProvider;
63 import com.android.contacts.tests.AccountsTestHelper;
64 import com.android.contacts.tests.ContactsMatchers;
65 import com.android.contacts.tests.FakeSimContactDao;
66 import com.android.contacts.tests.StringableCursor;
67 
68 import com.google.common.base.Function;
69 import com.google.common.base.Functions;
70 import com.google.common.util.concurrent.ListenableFuture;
71 import com.google.common.util.concurrent.SettableFuture;
72 
73 import org.junit.After;
74 import org.junit.AfterClass;
75 import org.junit.Before;
76 import org.junit.Test;
77 import org.junit.runner.RunWith;
78 
79 import java.util.Collections;
80 import java.util.concurrent.TimeUnit;
81 
82 /**
83  * UI Tests for {@link SimImportActivity}
84  *
85  * These should probably be converted to espresso tests because espresso does a better job of
86  * waiting for the app to be idle once espresso library is added
87  */
88 //@Suppress
89 @LargeTest
90 @RunWith(AndroidJUnit4.class)
91 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
92 @TargetApi(Build.VERSION_CODES.M)
93 public class SimImportActivityTest {
94 
95     public static final int TIMEOUT = 100000;
96     private Context mContext;
97     private UiDevice mDevice;
98     private Instrumentation mInstrumentation;
99     private FakeSimContactDao mDao;
100     private AccountsTestHelper mAccountHelper;
101     private Activity mActivity;
102 
103     @Before
setUp()104     public void setUp() throws Exception {
105         mContext = InstrumentationRegistry.getTargetContext();
106         mInstrumentation = InstrumentationRegistry.getInstrumentation();
107         mDao = new FakeSimContactDao();
108         SimContactDao.setFactoryForTest(Functions.<SimContactDao>constant(mDao));
109         mDevice = UiDevice.getInstance(mInstrumentation);
110 
111         // Add some test accounts so that account picker is exercised
112         mAccountHelper = new AccountsTestHelper();
113         mAccountHelper.addTestAccount(mAccountHelper.generateAccountName("SimImportActivity1_"));
114         mAccountHelper.addTestAccount(mAccountHelper.generateAccountName("SimImportActivity2_"));
115         mAccountHelper.addTestAccount(mAccountHelper.generateAccountName("SimImportActivity3_"));
116     }
117 
118     @After
tearDown()119     public void tearDown() throws Exception {
120         SimContactDao.setFactoryForTest(SimContactDao.DEFAULT_FACTORY);
121         mAccountHelper.cleanup();
122         if (mActivity != null) {
123             mActivity.finish();
124             mInstrumentation.waitForIdleSync();
125         }
126     }
127 
128     @AfterClass
tearDownClass()129     public static void tearDownClass() {
130         AccountsTestHelper.removeAccountsWithPrefix(
131                 InstrumentationRegistry.getTargetContext(), "SimImportActivity");
132     }
133 
134     @Test
shouldDisplaySimContacts()135     public void shouldDisplaySimContacts() {
136         mDao.addSim(someSimCard(),
137                         new SimContact(1, "Sim One", "5550101"),
138                         new SimContact(2, "Sim Two", null),
139                         new SimContact(3, null, "5550103")
140                 );
141         mActivity = mInstrumentation.startActivitySync(new Intent(mContext, SimImportActivity.class)
142                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
143 
144         mDevice.waitForIdle();
145 
146         assertTrue(mDevice.wait(Until.hasObject(By.text("Sim One")), TIMEOUT));
147 
148         assertTrue(mDevice.hasObject(By.text("Sim One")));
149         assertTrue(mDevice.hasObject(By.text("Sim Two")));
150         assertTrue(mDevice.hasObject(By.text("5550103")));
151     }
152 
153     @Test
shouldHaveEmptyState()154     public void shouldHaveEmptyState() {
155         mDao.addSim(someSimCard());
156 
157         mInstrumentation.startActivitySync(new Intent(mContext, SimImportActivity.class)
158                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
159 
160         mDevice.waitForIdle();
161 
162         assertTrue(mDevice.wait(Until.hasObject(By.textStartsWith("No contacts")), TIMEOUT));
163     }
164 
165     @Test
smokeRotateInEmptyState()166     public void smokeRotateInEmptyState() {
167         mDao.addSim(someSimCard());
168 
169         mActivity = mInstrumentation.startActivitySync(
170                 new Intent(mContext, SimImportActivity.class)
171                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
172 
173         assertTrue(mDevice.wait(Until.hasObject(By.textStartsWith("No contacts")), TIMEOUT));
174 
175         mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
176 
177         mDevice.waitForIdle();
178 
179         assertTrue(mDevice.wait(Until.hasObject(By.textStartsWith("No contacts")), TIMEOUT));
180     }
181 
182     @Test
smokeRotateInNonEmptyState()183     public void smokeRotateInNonEmptyState() throws Exception {
184         mDao.addSim(someSimCard(), new SimContact(1, "Name One", "5550101"),
185                 new SimContact(2, "Name Two", "5550102"));
186 
187         mActivity = mInstrumentation.startActivitySync(
188                 new Intent(mContext, SimImportActivity.class)
189                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
190 
191         assertTrue(mDevice.wait(Until.hasObject(By.textStartsWith("Name One")), TIMEOUT));
192 
193         mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
194 
195         mDevice.waitForIdle();
196 
197         assertTrue(mDevice.wait(Until.hasObject(By.textStartsWith("Name One")), TIMEOUT));
198     }
199 
200     /**
201      * Tests a complete import flow
202      *
203      * <p>Test case outline:</p>
204      * <ul>
205      * <li>Load SIM contacts
206      * <li>Change to a specific target account
207      * <li>Deselect 3 specific SIM contacts
208      * <li>Rotate the screen to landscape
209      * <li>Rotate the screen back to portrait
210      * <li>Press the import button
211      * <li>Wait for import to complete
212      * <li>Query contacts in target account and verify that they match selected contacts
213      * <li>Start import activity again
214      * <li>Switch to target account
215      * <li>Verify that previously imported contacts are disabled and not checked
216      * </ul>
217      *
218      * <p>This mocks out the IccProvider and stubs the canReadSimContacts method to make it work on
219      * an emulator but otherwise uses real dependency.
220      * </p>
221      */
222     @Test
selectionsAreImportedAndDisabledOnSubsequentImports()223     public void selectionsAreImportedAndDisabledOnSubsequentImports() throws Exception {
224         final AccountWithDataSet targetAccount = mAccountHelper.addTestAccount(
225                 mAccountHelper.generateAccountName("SimImportActivity0_targetAccount_"));
226 
227         final MockContentProvider iccProvider = new MockContentProvider();
228         iccProvider.expect(MockContentProvider.Query.forAnyUri())
229                 .withDefaultProjection(new String[] {SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
230                         SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS })
231                 .anyNumberOfTimes()
232                 .returnRow(toCursorRow(new SimContact(1, "Import One", "5550101")))
233                 .returnRow(toCursorRow(new SimContact(2, "Skip Two", "5550102")))
234                 .returnRow(toCursorRow(new SimContact(3, "Import Three", "5550103")))
235                 .returnRow(toCursorRow(new SimContact(4, "Skip Four", "5550104")))
236                 .returnRow(toCursorRow(new SimContact(5, "Skip Five", "5550105")))
237                 .returnRow(toCursorRow(new SimContact(6, "Import Six", "5550106")));
238         final MockContentResolver mockResolver = new MockContentResolver();
239         mockResolver.addProvider("icc", iccProvider);
240         final ContentProviderClient contactsProviderClient = mContext.getContentResolver()
241                 .acquireContentProviderClient(ContactsContract.AUTHORITY);
242         mockResolver.addProvider(ContactsContract.AUTHORITY, new ForwardingContentProvider(
243                 contactsProviderClient));
244 
245         SimContactDao.setFactoryForTest(new Function<Context, SimContactDao>() {
246             @Override
247             public SimContactDao apply(Context input) {
248                 final SimContactDaoImpl spy = spy(new SimContactDaoImpl(
249                         mContext, mockResolver,
250                         (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE)));
251                 final SimCard sim = someSimCard();
252                 doReturn(true).when(spy).canReadSimContacts();
253                 doReturn(Collections.singletonList(sim)).when(spy).getSimCards();
254                 doReturn(sim).when(spy).getSimBySubscriptionId(anyInt());
255                 return spy;
256             }
257         });
258 
259         mActivity = mInstrumentation.startActivitySync(
260                 new Intent(mContext, SimImportActivity.class)
261                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
262 
263         assertTrue(mDevice.wait(Until.hasObject(By.desc("Show more")), TIMEOUT));
264 
265         mDevice.findObject(By.desc("Show more")).clickAndWait(Until.newWindow(), TIMEOUT);
266         mDevice.findObject(By.textContains("_targetAccount_")).click();
267 
268         assertTrue(mDevice.wait(Until.hasObject(By.text("Skip Two")), TIMEOUT));
269 
270         mDevice.findObject(By.text("Skip Two")).click();
271         mDevice.findObject(By.text("Skip Four")).click();
272         mDevice.findObject(By.text("Skip Five")).click();
273         mDevice.waitForIdle();
274 
275         assertTrue(mDevice.hasObject(By.text("Skip Two").checked(false)));
276         assertTrue(mDevice.hasObject(By.text("Skip Five").checked(false)));
277 
278         mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
279         mDevice.wait(Until.hasObject(By.text("Import One")), TIMEOUT);
280         mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
281         mDevice.wait(Until.hasObject(By.text("Import One")), TIMEOUT);
282 
283         ListenableFuture<?> nextImportFuture = nextImportCompleteBroadcast();
284 
285         mDevice.findObject(By.text("IMPORT").clickable(true)).click();
286 
287         // Block until import completes
288         nextImportFuture.get(TIMEOUT, TimeUnit.MILLISECONDS);
289 
290         final Cursor cursor = new StringableCursor(
291                 mContext.getContentResolver().query(Data.CONTENT_URI, null,
292                         ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
293                                 ContactsContract.RawContacts.ACCOUNT_TYPE+ "=?",
294                         new String[] {
295                                 targetAccount.name,
296                                 targetAccount.type
297                         }, null));
298         // 3 contacts imported with one row for name and one for phone
299         assertThat(cursor, ContactsMatchers.hasCount(3 * 2));
300 
301         assertThat(cursor, hasRowMatching(allOf(
302                 hasMimeType(Phone.CONTENT_ITEM_TYPE),
303                 hasValueForColumn(Phone.DISPLAY_NAME, "Import One"),
304                 hasValueForColumn(Phone.NUMBER, "5550101")
305         )));
306         assertThat(cursor, hasRowMatching(allOf(
307                 hasMimeType(Phone.CONTENT_ITEM_TYPE),
308                 hasValueForColumn(Phone.DISPLAY_NAME, "Import Three"),
309                 hasValueForColumn(Phone.NUMBER, "5550103")
310         )));
311         assertThat(cursor, hasRowMatching(allOf(
312                 hasMimeType(Phone.CONTENT_ITEM_TYPE),
313                 hasValueForColumn(Phone.DISPLAY_NAME, "Import Six"),
314                 hasValueForColumn(Phone.NUMBER, "5550106")
315         )));
316 
317         cursor.close();
318 
319 
320         mActivity = mInstrumentation.startActivitySync(
321                 new Intent(mContext, SimImportActivity.class)
322                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
323 
324         assertTrue(mDevice.wait(Until.hasObject(By.text("Import One")), TIMEOUT));
325 
326         mDevice.findObject(By.descStartsWith("Show more")).clickAndWait(Until.newWindow(), TIMEOUT);
327         mDevice.findObject(By.textContains(targetAccount.name)).click();
328         mDevice.waitForIdle();
329 
330         assertTrue(mDevice.wait(Until.hasObject(By.text("Import One").checked(false)), TIMEOUT));
331         assertTrue(mDevice.hasObject(By.text("Import Three").checked(false)));
332         assertTrue(mDevice.hasObject(By.text("Import Six").checked(false)));
333 
334         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
335             contactsProviderClient.close();
336         }
337     }
338 
nextImportCompleteBroadcast()339     private ListenableFuture<Intent> nextImportCompleteBroadcast() {
340         final SettableFuture<Intent> result = SettableFuture.create();
341         final BroadcastReceiver receiver = new BroadcastReceiver() {
342             @Override
343             public void onReceive(Context context, Intent intent) {
344                 result.set(intent);
345                 LocalBroadcastManager.getInstance(mContext).unregisterReceiver(this);
346             }
347         };
348         LocalBroadcastManager.getInstance(mContext).registerReceiver(receiver, new IntentFilter(
349                 SimImportService.BROADCAST_SIM_IMPORT_COMPLETE));
350         return result;
351     }
352 
toCursorRow(SimContact contact)353     private Object[] toCursorRow(SimContact contact) {
354         return new Object[] { contact.getId(), contact.getName(), contact.getPhone(), null };
355     }
356 
someSimCard()357     private SimCard someSimCard() {
358         return new SimCard("id", 1, "Carrier", "SIM", "18005550101", "us");
359     }
360 }
361