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 
17 package android.provider.cts.contactsproviderwipe;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ProviderInfo;
25 import android.database.ContentObserver;
26 import android.database.Cursor;
27 import android.net.Uri;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.ParcelFileDescriptor;
31 import android.platform.test.annotations.AppModeFull;
32 import android.provider.ContactsContract;
33 import android.provider.ContactsContract.Intents;
34 import android.provider.ContactsContract.ProviderStatus;
35 import android.test.AndroidTestCase;
36 import android.text.TextUtils;
37 import android.util.Log;
38 
39 import com.android.compatibility.common.util.TestUtils;
40 
41 import androidx.test.InstrumentationRegistry;
42 
43 import java.io.BufferedReader;
44 import java.io.FileReader;
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.concurrent.CountDownLatch;
49 import java.util.concurrent.TimeUnit;
50 import java.util.concurrent.atomic.AtomicReference;
51 
52 /**
53  * CTS tests for CP2 regarding data wipe.
54  *
55  * <p>We can't use CtsProviderTestCases for this test because CtsProviderTestCases creates
56  * a stable connection to the contacts provider, which would cause the test process to be killed
57  * when the CP2 process gets killed (for "pm clear").
58  */
59 @AppModeFull // Requires READ/WRITE_CONTACTS.
60 public class ContactsContract_Wipe extends AndroidTestCase {
61     public static final String TAG = "ContactsContract_PS";
62 
63     /** 1 hour in milliseconds. */
64     private static final long ONE_HOUR_IN_MILLIS = 1000L * 60 * 60;
65 
getDatabaseCreationTimestamp()66     private long getDatabaseCreationTimestamp() {
67         try (Cursor cursor = getContext().getContentResolver().query(
68                 ProviderStatus.CONTENT_URI, null, null, null, null)) {
69             assertTrue(cursor.moveToFirst());
70 
71             final Long timestamp = cursor.getLong(
72                     cursor.getColumnIndexOrThrow(ProviderStatus.DATABASE_CREATION_TIMESTAMP));
73             assertNotNull(timestamp);
74 
75             return timestamp;
76         }
77     }
78 
assertBigger(long bigger, long smaller)79     private void assertBigger(long bigger, long smaller) {
80         assertTrue("Expecting " + bigger + " > " + smaller, bigger > smaller);
81     }
82 
getContactsProviderPackageName()83     private String getContactsProviderPackageName() {
84         final List<ProviderInfo> list = getContext().getPackageManager().queryContentProviders(
85                 null, 0, PackageManager.MATCH_ALL);
86         assertNotNull(list);
87         for (ProviderInfo pi : list) {
88             if (TextUtils.isEmpty(pi.authority)) {
89                 continue;
90             }
91             for (String authority : pi.authority.split(";")) {
92                 Log.i(TAG, "Found " + authority);
93                 if (ContactsContract.AUTHORITY.equals(authority)) {
94                     return pi.packageName;
95                 }
96             }
97         }
98         fail("Contacts provider package not found.");
99         return null;
100     }
101 
readAll(ParcelFileDescriptor pfd)102     static List<String> readAll(ParcelFileDescriptor pfd) {
103         try {
104             try {
105                 final ArrayList<String> ret = new ArrayList<>();
106                 try (BufferedReader r = new BufferedReader(
107                         new FileReader(pfd.getFileDescriptor()))) {
108                     String line;
109                     while ((line = r.readLine()) != null) {
110                         ret.add(line);
111                     }
112                     r.readLine();
113                 }
114                 return ret;
115             } finally {
116                 pfd.close();
117             }
118         } catch (IOException e) {
119             throw new RuntimeException(e);
120         }
121     }
122 
concatResult(List<String> result)123     static String concatResult(List<String> result) {
124         final StringBuilder sb = new StringBuilder();
125         for (String s : result) {
126             sb.append(s);
127             sb.append("\n");
128         }
129         return sb.toString().trim();
130     }
131 
wipeContactsProvider()132     private void wipeContactsProvider() throws Exception {
133         final String providerPackage = getContactsProviderPackageName();
134 
135         Log.i(TAG, "Wiping "  + providerPackage + "...");
136 
137         final String result = concatResult(readAll(
138                 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
139                         "pm clear --user current " + providerPackage)));
140         Log.i(TAG, "Result:" + result);
141 
142         assertEquals("Success", result);
143 
144         // Wait until CP2 is ready to be used.
145         TestUtils.waitUntil("CP2 still not ready", () -> {
146             try (Cursor cursor = getContext().getContentResolver().query(
147                     ProviderStatus.CONTENT_URI, null, null, null, null)) {
148                 return cursor != null;
149             }
150         });
151     }
152 
testCreationTimestamp()153     public void testCreationTimestamp() throws Exception {
154         final long originalTimestamp = getDatabaseCreationTimestamp();
155 
156         Thread.sleep(1);
157 
158         final long start = System.currentTimeMillis();
159 
160         Log.i(TAG, "start="  + start);
161         Log.i(TAG, "originalTimestamp="  + originalTimestamp);
162 
163         // Check: the (old) creation time should be smaller than the start time (=now).
164         // Add 1 hour to compensate for possible day light saving.
165         assertBigger(start + ONE_HOUR_IN_MILLIS, originalTimestamp);
166 
167         Thread.sleep(1);
168 
169         wipeContactsProvider();
170 
171         // Check: the creation time should be bigger than the start time.
172         final long newTimestamp = getDatabaseCreationTimestamp();
173         Log.i(TAG, "newTimestamp="  + newTimestamp);
174 
175         assertBigger(newTimestamp, start);
176     }
177 
testDatabaseWipeNotification()178     public void testDatabaseWipeNotification() throws Exception {
179         final CountDownLatch latch = new CountDownLatch(1);
180         final AtomicReference<Uri> notifiedUri = new AtomicReference<>();
181 
182         getContext().getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI,
183                 /* notifyForDescendants=*/ false,
184                 new ContentObserver(new Handler(Looper.getMainLooper())) {
185             @Override
186             public void onChange(boolean selfChange, Uri uri) {
187                 Log.i(TAG, "Received notification on " + uri);
188                 if (uri == null || !uri.equals(ProviderStatus.CONTENT_URI)) {
189                     return; // we only care about a change to ProviderStatus.CONTENT_URI
190                 }
191                 notifiedUri.set(uri);
192                 latch.countDown();
193             }
194         });
195 
196         wipeContactsProvider();
197 
198         // Accessing CP2 to make sure the process starts.
199         getDatabaseCreationTimestamp();
200 
201         assertTrue("Didn't receive content change notification",
202                 latch.await(120, TimeUnit.SECONDS));
203 
204         assertEquals(ProviderStatus.CONTENT_URI, notifiedUri.get());
205     }
206 
testDatabaseWipeBroadcast()207     public void testDatabaseWipeBroadcast() throws Exception {
208         final CountDownLatch latch = new CountDownLatch(1);
209 
210         final IntentFilter filter = new IntentFilter();
211         filter.addAction(Intents.CONTACTS_DATABASE_CREATED);
212 
213         getContext().registerReceiver(new BroadcastReceiver() {
214             @Override
215             public void onReceive(Context context, Intent intent) {
216                 Log.i(TAG, "Received broadcast: " + intent);
217                 latch.countDown();
218             }
219         }, filter, Context.RECEIVER_EXPORTED);
220 
221         wipeContactsProvider();
222 
223         // Accessing CP2 to make sure the process starts.
224         getDatabaseCreationTimestamp();
225 
226         assertTrue("Didn't receive contacts wipe broadcast",
227                 latch.await(120, TimeUnit.SECONDS));
228     }
229 }
230