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