1 /* 2 * Copyright (C) 2012 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 com.android.providers.contacts; 18 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.net.Uri; 25 import android.os.FileUtils; 26 import android.provider.ContactsContract.Contacts; 27 import android.provider.ContactsContract.Profile; 28 import android.provider.ContactsContract.RawContacts; 29 import android.util.Log; 30 31 import androidx.annotation.Nullable; 32 33 import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 34 35 import junit.framework.Assert; 36 37 import java.io.BufferedReader; 38 import java.io.File; 39 import java.io.FileOutputStream; 40 import java.io.IOException; 41 import java.io.InputStream; 42 import java.io.InputStreamReader; 43 44 public class TestUtils { 45 private static final String TAG = "ContactsTestUtils"; 46 TestUtils()47 private TestUtils() { 48 } 49 50 /** 51 * We normally use in-memory DBs in unit tests, because that's faster, but it's impossible to 52 * look at intermediate databases when something is failing. When this flag is set to true, 53 * we'll switch to file-based DBs, so we can call {@link #createDatabaseSnapshot} 54 * , pull the snapshot DBs and take a look at them. 55 */ 56 public static final boolean ENABLE_DATABASE_SNAPSHOT = false; // DO NOT SUBMIT WITH TRUE. 57 58 private static final Object sDatabasePathLock = new Object(); 59 private static File sDatabasePath = null; 60 getDatabaseFile(Context context, @Nullable String name)61 private static String getDatabaseFile(Context context, @Nullable String name) { 62 if (!ENABLE_DATABASE_SNAPSHOT) { 63 return null; // Use the in-memory DB. 64 } 65 synchronized (sDatabasePathLock) { 66 if (sDatabasePath == null) { 67 final File path = new File(context.getCacheDir(), "test-db"); 68 if (path.exists()) { 69 Assert.assertTrue("Unable to delete directory: " + path, 70 FileUtils.deleteContents(path)); 71 } else { 72 Assert.assertTrue("Unable to create directory: " + path, path.mkdirs()); 73 } 74 Log.i(TAG, "Test DB directory: " + path); 75 76 sDatabasePath = path; 77 } 78 final File ret; 79 if (name == null) { 80 ret = sDatabasePath; 81 } else { 82 ret = new File(sDatabasePath, name); 83 Log.i(TAG, "Test DB file: " + ret); 84 } 85 return ret.getAbsolutePath(); 86 } 87 } 88 getContactsDatabaseFilename(Context context)89 public static String getContactsDatabaseFilename(Context context) { 90 return getContactsDatabaseFilename(context, ""); 91 } 92 getContactsDatabaseFilename(Context context, String suffix)93 public static String getContactsDatabaseFilename(Context context, String suffix) { 94 return getDatabaseFile(context, "contacts2" + suffix + ".db"); 95 } 96 getProfileDatabaseFilename(Context context)97 public static String getProfileDatabaseFilename(Context context) { 98 return getProfileDatabaseFilename(context, ""); 99 } 100 getProfileDatabaseFilename(Context context, String suffix)101 public static String getProfileDatabaseFilename(Context context, String suffix) { 102 return getDatabaseFile(context, "profile.db" + suffix + ".db"); 103 } 104 createDatabaseSnapshot(Context context, String name)105 public static void createDatabaseSnapshot(Context context, String name) { 106 Assert.assertTrue( 107 "ENABLE_DATABASE_SNAPSHOT must be set to true to create database snapshot", 108 ENABLE_DATABASE_SNAPSHOT); 109 110 final File fromDir = new File(getDatabaseFile(context, null)); 111 final File toDir = new File(context.getCacheDir(), "snapshot-" + name); 112 if (toDir.exists()) { 113 Assert.assertTrue("Unable to delete directory: " + toDir, 114 FileUtils.deleteContents(toDir)); 115 } else { 116 Assert.assertTrue("Unable to create directory: " + toDir, toDir.mkdirs()); 117 } 118 119 Log.w(TAG, "Copying database files from '" + fromDir + "' into '" + toDir + "'..."); 120 121 for (File file : fromDir.listFiles()) { 122 try { 123 final File to = new File(toDir, file.getName()); 124 FileUtils.copyFileOrThrow(file, to); 125 Log.i(TAG, "Created: " + to); 126 } catch (IOException e) { 127 Assert.fail("Failed to copy file: " + e.toString()); 128 } 129 } 130 } 131 132 /** Convenient method to create a ContentValues */ cv(Object... namesAndValues)133 public static ContentValues cv(Object... namesAndValues) { 134 Assert.assertTrue((namesAndValues.length % 2) == 0); 135 136 final ContentValues ret = new ContentValues(); 137 for (int i = 1; i < namesAndValues.length; i += 2) { 138 final String name = namesAndValues[i - 1].toString(); 139 final Object value = namesAndValues[i]; 140 if (value == null) { 141 ret.putNull(name); 142 } else if (value instanceof String) { 143 ret.put(name, (String) value); 144 } else if (value instanceof Integer) { 145 ret.put(name, (Integer) value); 146 } else if (value instanceof Long) { 147 ret.put(name, (Long) value); 148 } else { 149 Assert.fail("Unsupported type: " + value.getClass().getSimpleName()); 150 } 151 } 152 return ret; 153 } 154 155 /** 156 * Writes the content of a cursor to the log. 157 */ dumpCursor(Cursor c)158 public static final void dumpCursor(Cursor c) { 159 final StringBuilder sb = new StringBuilder(); 160 for (int i = 0; i < c.getColumnCount(); i++) { 161 if (i > 0) sb.append("|"); 162 sb.append(c.getColumnName(i)); 163 } 164 Log.i(TAG, sb.toString()); 165 166 final int pos = c.getPosition(); 167 168 c.moveToPosition(-1); 169 while (c.moveToNext()) { 170 sb.setLength(0); 171 for (int i = 0; i < c.getColumnCount(); i++) { 172 if (i > 0) sb.append("|"); 173 174 if (c.getType(i) == Cursor.FIELD_TYPE_BLOB) { 175 byte[] blob = c.getBlob(i); 176 sb.append("([blob] "); 177 sb.append(blob == null ? "null" : blob.length + "b"); 178 sb.append(")"); 179 } else { 180 sb.append(c.getString(i)); 181 } 182 } 183 Log.i(TAG, sb.toString()); 184 } 185 186 c.moveToPosition(pos); 187 } 188 dumpTable(SQLiteDatabase db, String name)189 public static void dumpTable(SQLiteDatabase db, String name) { 190 Log.i(TAG, "Dumping table: " + name); 191 try (Cursor c = db.rawQuery(String.format("SELECT * FROM %s", name), null)) { 192 dumpCursor(c); 193 } 194 } 195 dumpUri(Context context, Uri uri)196 public static void dumpUri(Context context, Uri uri) { 197 Log.i(TAG, "Dumping URI: " + uri); 198 try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) { 199 dumpCursor(c); 200 } 201 } 202 dumpUri(ContentResolver resolver, Uri uri)203 public static void dumpUri(ContentResolver resolver, Uri uri) { 204 Log.i(TAG, "Dumping URI: " + uri); 205 try (Cursor c = resolver.query(uri, null, null, null, null)) { 206 dumpCursor(c); 207 } 208 } 209 210 /** 211 * Writes an arbitrary byte array to the test apk's cache directory. 212 */ dumpToCacheDir(Context context, String prefix, String suffix, byte[] data)213 public static final String dumpToCacheDir(Context context, String prefix, String suffix, 214 byte[] data) { 215 try { 216 File file = File.createTempFile(prefix, suffix, context.getCacheDir()); 217 FileOutputStream fos = new FileOutputStream(file); 218 fos.write(data); 219 fos.close(); 220 return file.getAbsolutePath(); 221 } catch (IOException e) { 222 return "[Failed to write to file: " + e.getMessage() + "]"; 223 } 224 } 225 insertRawContact( ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values)226 public static Uri insertRawContact( 227 ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) { 228 return insertRawContact(RawContacts.CONTENT_URI, resolver, dbh, values); 229 } 230 insertProfileRawContact( ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values)231 public static Uri insertProfileRawContact( 232 ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) { 233 return insertRawContact(Profile.CONTENT_RAW_CONTACTS_URI, resolver, dbh, values); 234 } 235 insertRawContact(Uri tableUri, ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values)236 private static Uri insertRawContact(Uri tableUri, 237 ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) { 238 final SQLiteDatabase db = dbh.getWritableDatabase(); 239 240 final Uri rowUri = resolver.insert(tableUri, values); 241 Long timesContacted = values.getAsLong(RawContacts.LR_TIMES_CONTACTED); 242 if (timesContacted != null) { 243 // TIMES_CONTACTED is no longer modifiable via resolver, so we update the DB directly. 244 final long rid = Long.parseLong(rowUri.getLastPathSegment()); 245 246 final String[] args = {String.valueOf(rid)}; 247 248 db.update(Tables.RAW_CONTACTS, 249 cv(RawContacts.RAW_TIMES_CONTACTED, (long) timesContacted), 250 "_id=?", args); 251 252 // Then propagate it to contacts too. 253 db.execSQL("UPDATE " + Tables.CONTACTS 254 + " SET " + Contacts.RAW_TIMES_CONTACTED + " = (" 255 + " SELECT sum(" + RawContacts.RAW_TIMES_CONTACTED + ") FROM " 256 + Tables.RAW_CONTACTS + " AS r " 257 + " WHERE " + Tables.CONTACTS + "._id = r." + RawContacts.CONTACT_ID 258 + " GROUP BY r." + RawContacts.CONTACT_ID + ")"); 259 } 260 return rowUri; 261 } 262 executeSqlFromAssetFile( Context context, SQLiteDatabase db, String assetName)263 public static void executeSqlFromAssetFile( 264 Context context, SQLiteDatabase db, String assetName) { 265 try (InputStream input = context.getAssets().open(assetName);) { 266 BufferedReader r = new BufferedReader(new InputStreamReader(input)); 267 String query; 268 while ((query = r.readLine()) != null) { 269 if (query.trim().length() == 0 || query.startsWith("--")) { 270 continue; 271 } 272 db.execSQL(query); 273 } 274 } catch (IOException e) { 275 throw new RuntimeException(e.toString()); 276 } 277 } 278 } 279