/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.providers.telephony; import android.annotation.TargetApi; import android.app.backup.FullBackupDataOutput; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.ContextWrapper; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.BaseColumns; import android.provider.Telephony; import android.test.AndroidTestCase; import android.test.mock.MockContentProvider; import android.test.mock.MockContentResolver; import android.test.mock.MockCursor; import android.util.ArrayMap; import android.util.ArraySet; import android.util.JsonReader; import android.util.JsonWriter; import android.util.SparseArray; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; /** * Tests for testing backup/restore of SMS and text MMS messages. * For backup it creates fake provider and checks resulting json array. * For restore provides json array and checks inserts of the messages into provider. */ @TargetApi(Build.VERSION_CODES.M) public class TelephonyBackupAgentTest extends AndroidTestCase { /* Map subscriptionId -> phone number */ private SparseArray mSubId2Phone; /* Map phone number -> subscriptionId */ private ArrayMap mPhone2SubId; /* Table being used for sms cursor */ private final List mSmsTable = new ArrayList<>(); /* Table begin used for mms cursor */ private final List mMmsTable = new ArrayList<>(); /* Table contains parts, addresses of mms */ private final List mMmsAllContentValues = new ArrayList<>(); /* Cursors being used to access sms, mms tables */ private FakeCursor mSmsCursor, mMmsCursor; /* Test data with sms and mms */ private ContentValues[] mSmsRows, mMmsRows; /* Json representation for the test data */ private String[] mSmsJson, mMmsJson; /* sms, mms json concatenated as json array */ private String mAllSmsJson, mAllMmsJson; private StringWriter mStringWriter; /* Content resolver passed to the backupAgent */ private MockContentResolver mMockContentResolver = new MockContentResolver(); /* Map uri -> cursors. Being used for contentprovider. */ private Map mCursors; /* Content provider with threadIds.*/ private ThreadProvider mThreadProvider = new ThreadProvider(); private static final String EMPTY_JSON_ARRAY = "[]"; TelephonyBackupAgent mTelephonyBackupAgent; @Override protected void setUp() throws Exception { super.setUp(); /* Filling up subscription maps */ mStringWriter = new StringWriter(); mSubId2Phone = new SparseArray(); mSubId2Phone.append(1, "+111111111111111"); mSubId2Phone.append(3, "+333333333333333"); mPhone2SubId = new ArrayMap<>(); for (int i=0; i(); /* Bind tables to the cursors */ mSmsCursor = new FakeCursor(mSmsTable, TelephonyBackupAgent.SMS_PROJECTION); mCursors.put(Telephony.Sms.CONTENT_URI, mSmsCursor); mMmsCursor = new FakeCursor(mMmsTable, TelephonyBackupAgent.MMS_PROJECTION); mCursors.put(Telephony.Mms.CONTENT_URI, mMmsCursor); /* Generating test data */ mSmsRows = new ContentValues[4]; mSmsJson = new String[4]; mSmsRows[0] = createSmsRow(1, 1, "+1232132214124", "sms 1", "sms subject", 9087978987l, 999999999, 3, 44, 1); mSmsJson[0] = "{\"self_phone\":\"+111111111111111\",\"address\":" + "\"+1232132214124\",\"body\":\"sms 1\",\"subject\":\"sms subject\",\"date\":" + "\"9087978987\",\"date_sent\":\"999999999\",\"status\":\"3\",\"type\":\"44\"," + "\"recipients\":[\"+123 (213) 2214124\"],\"archived\":true}"; mThreadProvider.setArchived( mThreadProvider.getOrCreateThreadId(new String[]{"+123 (213) 2214124"})); mSmsRows[1] = createSmsRow(2, 2, "+1232132214124", "sms 2", null, 9087978987l, 999999999, 0, 4, 1); mSmsJson[1] = "{\"address\":\"+1232132214124\",\"body\":\"sms 2\",\"date\":" + "\"9087978987\",\"date_sent\":\"999999999\",\"status\":\"0\",\"type\":\"4\"," + "\"recipients\":[\"+123 (213) 2214124\"]}"; mSmsRows[2] = createSmsRow(4, 3, "+1232221412433 +1232221412444", "sms 3", null, 111111111111l, 999999999, 2, 3, 2); mSmsJson[2] = "{\"self_phone\":\"+333333333333333\",\"address\":" + "\"+1232221412433 +1232221412444\",\"body\":\"sms 3\",\"date\":\"111111111111\"," + "\"date_sent\":" + "\"999999999\",\"status\":\"2\",\"type\":\"3\"," + "\"recipients\":[\"+1232221412433\",\"+1232221412444\"]}"; mThreadProvider.getOrCreateThreadId(new String[]{"+1232221412433", "+1232221412444"}); mSmsRows[3] = createSmsRow(5, 3, null, "sms 4", null, 111111111111l, 999999999, 2, 3, 5); mSmsJson[3] = "{\"self_phone\":\"+333333333333333\"," + "\"body\":\"sms 4\",\"date\":\"111111111111\"," + "\"date_sent\":" + "\"999999999\",\"status\":\"2\",\"type\":\"3\"}"; mAllSmsJson = makeJsonArray(mSmsJson); mMmsRows = new ContentValues[3]; mMmsJson = new String[3]; mMmsRows[0] = createMmsRow(1 /*id*/, 1 /*subid*/, "Subject 1" /*subject*/, 100 /*subcharset*/, 111111 /*date*/, 111112 /*datesent*/, 3 /*type*/, 17 /*version*/, 1 /*textonly*/, 11 /*msgBox*/, "location 1" /*contentLocation*/, "MMs body 1" /*body*/, 111 /*body charset*/, new String[]{"+111 (111) 11111111", "+11121212", "example@example.com", "+999999999"} /*addresses*/, 3 /*threadId*/); mMmsJson[0] = "{\"self_phone\":\"+111111111111111\",\"sub\":\"Subject 1\"," + "\"date\":\"111111\",\"date_sent\":\"111112\",\"m_type\":\"3\",\"v\":\"17\"," + "\"msg_box\":\"11\",\"ct_l\":\"location 1\"," + "\"recipients\":[\"+11121212\",\"example@example.com\",\"+999999999\"]," + "\"mms_addresses\":" + "[{\"type\":10,\"address\":\"+111 (111) 11111111\",\"charset\":100}," + "{\"type\":11,\"address\":\"+11121212\",\"charset\":101},{\"type\":12,\"address\":"+ "\"example@example.com\",\"charset\":102},{\"type\":13,\"address\":\"+999999999\"" + ",\"charset\":103}],\"mms_body\":\"MMs body 1\",\"mms_charset\":111,\"" + "sub_cs\":\"100\"}"; mThreadProvider.getOrCreateThreadId(new String[]{"+11121212", "example@example.com", "+999999999"}); mMmsRows[1] = createMmsRow(2 /*id*/, 2 /*subid*/, null /*subject*/, 100 /*subcharset*/, 111122 /*date*/, 1111112 /*datesent*/, 4 /*type*/, 18 /*version*/, 1 /*textonly*/, 222 /*msgBox*/, "location 2" /*contentLocation*/, "MMs body 2" /*body*/, 121 /*body charset*/, new String[]{"+7 (333) ", "example@example.com", "+999999999"} /*addresses*/, 4 /*threadId*/); mMmsJson[1] = "{\"date\":\"111122\",\"date_sent\":\"1111112\",\"m_type\":\"4\"," + "\"v\":\"18\",\"msg_box\":\"222\",\"ct_l\":\"location 2\"," + "\"recipients\":[\"example@example.com\",\"+999999999\"]," + "\"mms_addresses\":" + "[{\"type\":10,\"address\":\"+7 (333) \",\"charset\":100}," + "{\"type\":11,\"address\":\"example@example.com\",\"charset\":101}," + "{\"type\":12,\"address\":\"+999999999\",\"charset\":102}]," + "\"mms_body\":\"MMs body 2\",\"mms_charset\":121}"; mThreadProvider.getOrCreateThreadId(new String[]{"example@example.com", "+999999999"}); mMmsRows[2] = createMmsRow(9 /*id*/, 3 /*subid*/, "Subject 10" /*subject*/, 10 /*subcharset*/, 111133 /*date*/, 1111132 /*datesent*/, 5 /*type*/, 19 /*version*/, 1 /*textonly*/, 333 /*msgBox*/, null /*contentLocation*/, "MMs body 3" /*body*/, 131 /*body charset*/, new String[]{"333 333333333333", "+1232132214124"} /*addresses*/, 1 /*threadId*/); mMmsJson[2] = "{\"self_phone\":\"+333333333333333\",\"sub\":\"Subject 10\"," + "\"date\":\"111133\",\"date_sent\":\"1111132\",\"m_type\":\"5\",\"v\":\"19\"," + "\"msg_box\":\"333\"," + "\"recipients\":[\"+123 (213) 2214124\"],\"archived\":true," + "\"mms_addresses\":" + "[{\"type\":10,\"address\":\"333 333333333333\",\"charset\":100}," + "{\"type\":11,\"address\":\"+1232132214124\",\"charset\":101}]," + "\"mms_body\":\"MMs body 3\",\"mms_charset\":131," + "\"sub_cs\":\"10\"}"; mAllMmsJson = makeJsonArray(mMmsJson); ContentProvider contentProvider = new MockContentProvider() { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (mCursors.containsKey(uri)) { FakeCursor fakeCursor = mCursors.get(uri); if (projection != null) { fakeCursor.setProjection(projection); } fakeCursor.nextRow = 0; return fakeCursor; } fail("No cursor for " + uri.toString()); return null; } }; mMockContentResolver.addProvider("sms", contentProvider); mMockContentResolver.addProvider("mms", contentProvider); mMockContentResolver.addProvider("mms-sms", mThreadProvider); mTelephonyBackupAgent = new TelephonyBackupAgent(); mTelephonyBackupAgent.attach(new ContextWrapper(getContext()) { @Override public ContentResolver getContentResolver() { return mMockContentResolver; } }); mTelephonyBackupAgent.clearSharedPreferences(); mTelephonyBackupAgent.setContentResolver(mMockContentResolver); mTelephonyBackupAgent.setSubId(mSubId2Phone, mPhone2SubId); } @Override protected void tearDown() throws Exception { mTelephonyBackupAgent.clearSharedPreferences(); super.tearDown(); } private static String makeJsonArray(String[] json) { StringBuilder stringBuilder = new StringBuilder("["); for (int i=0; i 0) { stringBuilder.append(","); } stringBuilder.append(json[i]); } stringBuilder.append("]"); return stringBuilder.toString(); } private static ContentValues createSmsRow(int id, int subId, String address, String body, String subj, long date, long dateSent, int status, int type, long threadId) { ContentValues smsRow = new ContentValues(); smsRow.put(Telephony.Sms._ID, id); smsRow.put(Telephony.Sms.SUBSCRIPTION_ID, subId); if (address != null) { smsRow.put(Telephony.Sms.ADDRESS, address); } if (body != null) { smsRow.put(Telephony.Sms.BODY, body); } if (subj != null) { smsRow.put(Telephony.Sms.SUBJECT, subj); } smsRow.put(Telephony.Sms.DATE, String.valueOf(date)); smsRow.put(Telephony.Sms.DATE_SENT, String.valueOf(dateSent)); smsRow.put(Telephony.Sms.STATUS, String.valueOf(status)); smsRow.put(Telephony.Sms.TYPE, String.valueOf(type)); smsRow.put(Telephony.Sms.THREAD_ID, threadId); return smsRow; } private ContentValues createMmsRow(int id, int subId, String subj, int subCharset, long date, long dateSent, int type, int version, int textOnly, int msgBox, String contentLocation, String body, int bodyCharset, String[] addresses, long threadId) { ContentValues mmsRow = new ContentValues(); mmsRow.put(Telephony.Mms._ID, id); mmsRow.put(Telephony.Mms.SUBSCRIPTION_ID, subId); if (subj != null) { mmsRow.put(Telephony.Mms.SUBJECT, subj); mmsRow.put(Telephony.Mms.SUBJECT_CHARSET, String.valueOf(subCharset)); } mmsRow.put(Telephony.Mms.DATE, String.valueOf(date)); mmsRow.put(Telephony.Mms.DATE_SENT, String.valueOf(dateSent)); mmsRow.put(Telephony.Mms.MESSAGE_TYPE, String.valueOf(type)); mmsRow.put(Telephony.Mms.MMS_VERSION, String.valueOf(version)); mmsRow.put(Telephony.Mms.TEXT_ONLY, textOnly); mmsRow.put(Telephony.Mms.MESSAGE_BOX, String.valueOf(msgBox)); if (contentLocation != null) { mmsRow.put(Telephony.Mms.CONTENT_LOCATION, contentLocation); } mmsRow.put(Telephony.Mms.THREAD_ID, threadId); final Uri partUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)). appendPath("part").build(); mCursors.put(partUri, createBodyCursor(body, bodyCharset)); mMmsAllContentValues.add(mmsRow); final Uri addrUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)). appendPath("addr").build(); mCursors.put(addrUri, createAddrCursor(addresses)); return mmsRow; } private static final String APP_SMIL = "application/smil"; private static final String TEXT_PLAIN = "text/plain"; // Cursor with parts of Mms. private FakeCursor createBodyCursor(String body, int charset) { List table = new ArrayList<>(); final String srcName = String.format("text.%06d.txt", 0); final String smilBody = String.format(TelephonyBackupAgent.sSmilTextPart, srcName); final String smil = String.format(TelephonyBackupAgent.sSmilTextOnly, smilBody); final ContentValues smilPart = new ContentValues(); smilPart.put(Telephony.Mms.Part.SEQ, -1); smilPart.put(Telephony.Mms.Part.CONTENT_TYPE, APP_SMIL); smilPart.put(Telephony.Mms.Part.NAME, "smil.xml"); smilPart.put(Telephony.Mms.Part.CONTENT_ID, ""); smilPart.put(Telephony.Mms.Part.CONTENT_LOCATION, "smil.xml"); smilPart.put(Telephony.Mms.Part.TEXT, smil); mMmsAllContentValues.add(smilPart); final ContentValues bodyPart = new ContentValues(); bodyPart.put(Telephony.Mms.Part.SEQ, 0); bodyPart.put(Telephony.Mms.Part.CONTENT_TYPE, TEXT_PLAIN); bodyPart.put(Telephony.Mms.Part.NAME, srcName); bodyPart.put(Telephony.Mms.Part.CONTENT_ID, "<"+srcName+">"); bodyPart.put(Telephony.Mms.Part.CONTENT_LOCATION, srcName); bodyPart.put(Telephony.Mms.Part.CHARSET, charset); bodyPart.put(Telephony.Mms.Part.TEXT, body); table.add(bodyPart); mMmsAllContentValues.add(bodyPart); return new FakeCursor(table, TelephonyBackupAgent.MMS_TEXT_PROJECTION); } // Cursor with addresses of Mms. private FakeCursor createAddrCursor(String[] addresses) { List table = new ArrayList<>(); for (int i=0; i mValues; private long mDummyMsgId = -1; private long mMsgId = -1; public FakeMmsProvider(List values) { this.mValues = values; } @Override public Uri insert(Uri uri, ContentValues values) { Uri retUri = Uri.parse("dummy_uri"); ContentValues modifiedValues = new ContentValues(mValues.get(nextRow++)); if (APP_SMIL.equals(values.get(Telephony.Mms.Part.CONTENT_TYPE))) { // Smil part. assertEquals(-1, mDummyMsgId); mDummyMsgId = values.getAsLong(Telephony.Mms.Part.MSG_ID); } if (values.get(Telephony.Mms.Part.SEQ) != null) { // Part of mms. final Uri expectedUri = Telephony.Mms.CONTENT_URI.buildUpon() .appendPath(String.valueOf(mDummyMsgId)) .appendPath("part") .build(); assertEquals(expectedUri, uri); } if (values.get(Telephony.Mms.Part.MSG_ID) != null) { modifiedValues.put(Telephony.Mms.Part.MSG_ID, mDummyMsgId); } if (values.get(Telephony.Mms.SUBSCRIPTION_ID) != null) { assertEquals(Telephony.Mms.CONTENT_URI, uri); if (mSubId2Phone.get(modifiedValues.getAsInteger(Telephony.Sms.SUBSCRIPTION_ID)) == null) { modifiedValues.put(Telephony.Sms.SUBSCRIPTION_ID, -1); } // Mms. modifiedValues.put(Telephony.Mms.READ, 1); modifiedValues.put(Telephony.Mms.SEEN, 1); mMsgId = modifiedValues.getAsInteger(BaseColumns._ID); retUri = Uri.withAppendedPath(Telephony.Mms.CONTENT_URI, String.valueOf(mMsgId)); modifiedValues.remove(BaseColumns._ID); } if (values.get(Telephony.Mms.Addr.ADDRESS) != null) { // Address. final Uri expectedUri = Telephony.Mms.CONTENT_URI.buildUpon() .appendPath(String.valueOf(mMsgId)) .appendPath("addr") .build(); assertEquals(expectedUri, uri); assertNotSame(-1, mMsgId); modifiedValues.put(Telephony.Mms.Addr.MSG_ID, mMsgId); mDummyMsgId = -1; } for (String key : modifiedValues.keySet()) { assertEquals("Key:"+key, modifiedValues.get(key), values.get(key)); } assertEquals(modifiedValues.size(), values.size()); return retUri; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { final Uri expectedUri = Telephony.Mms.CONTENT_URI.buildUpon() .appendPath(String.valueOf(mDummyMsgId)) .appendPath("part") .build(); assertEquals(expectedUri, uri); ContentValues expected = new ContentValues(); expected.put(Telephony.Mms.Part.MSG_ID, mMsgId); assertEquals(expected, values); return 2; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } public int getRowsAdded() { return nextRow; } } /** * class that implements MmsSms provider for thread ids. */ private static class ThreadProvider extends MockContentProvider { ArrayList > id2Thread = new ArrayList<>(); ArrayList id2Recipient = new ArrayList<>(); Set mIsThreadArchived = new HashSet<>(); Set mUpdateThreadsArchived = new HashSet<>(); public int getOrCreateThreadId(final String[] recipients) { if (recipients == null || recipients.length == 0) { throw new IllegalArgumentException("Unable to find or allocate a thread ID."); } Set ids = new ArraySet<>(); for (String rec : recipients) { if (!id2Recipient.contains(rec)) { id2Recipient.add(rec); } ids.add(id2Recipient.indexOf(rec)+1); } if (!id2Thread.contains(ids)) { id2Thread.add(ids); } return id2Thread.indexOf(ids)+1; } public void setArchived(int threadId) { mIsThreadArchived.add(threadId); } private String getSpaceSepIds(int threadId) { if (id2Thread.size() < threadId) { return null; } String spaceSepIds = null; for (Integer id : id2Thread.get(threadId-1)) { spaceSepIds = (spaceSepIds == null ? "" : spaceSepIds + " ") + String.valueOf(id); } return spaceSepIds; } private String getRecipient(int recipientId) { return id2Recipient.get(recipientId-1); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (uri.equals(TelephonyBackupAgent.ALL_THREADS_URI)) { final int threadId = Integer.parseInt(selectionArgs[0]); final String spaceSepIds = getSpaceSepIds(threadId); List table = new ArrayList<>(); ContentValues row = new ContentValues(); row.put(Telephony.Threads.RECIPIENT_IDS, spaceSepIds); table.add(row); return new FakeCursor(table, projection); } else if (uri.toString().startsWith(Telephony.Threads.CONTENT_URI.toString())) { assertEquals(1, projection.length); assertEquals(Telephony.Threads.ARCHIVED, projection[0]); List segments = uri.getPathSegments(); final int threadId = Integer.parseInt(segments.get(segments.size() - 2)); List table = new ArrayList<>(); ContentValues row = new ContentValues(); row.put(Telephony.Threads.ARCHIVED, mIsThreadArchived.contains(threadId) ? 1 : 0); table.add(row); return new FakeCursor(table, projection); } else if (uri.toString().startsWith( TelephonyBackupAgent.SINGLE_CANONICAL_ADDRESS_URI.toString())) { final int recipientId = (int)ContentUris.parseId(uri); final String recipient = getRecipient(recipientId); List table = new ArrayList<>(); ContentValues row = new ContentValues(); row.put(Telephony.CanonicalAddressesColumns.ADDRESS, recipient); table.add(row); return new FakeCursor(table, projection != null ? projection : new String[] { Telephony.CanonicalAddressesColumns.ADDRESS }); } else if (uri.toString().startsWith( TelephonyBackupAgent.THREAD_ID_CONTENT_URI.toString())) { List recipients = uri.getQueryParameters("recipient"); final int threadId = getOrCreateThreadId(recipients.toArray(new String[recipients.size()])); List table = new ArrayList<>(); ContentValues row = new ContentValues(); row.put(BaseColumns._ID, String.valueOf(threadId)); table.add(row); return new FakeCursor(table, projection); } else { fail("Unknown URI"); } return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { assertEquals(uri, Telephony.Threads.CONTENT_URI); assertEquals(values.getAsInteger(Telephony.Threads.ARCHIVED).intValue(), 1); final int threadId = Integer.parseInt(selectionArgs[0]); mUpdateThreadsArchived.add(threadId); return 1; } } /** * general cursor for serving queries. */ private static class FakeCursor extends MockCursor { String[] projection; List rows; int nextRow = 0; public FakeCursor(List rows, String[] projection) { this.projection = projection; this.rows = rows; } public void setProjection(String[] projection) { this.projection = projection; } @Override public int getColumnCount() { return projection.length; } @Override public String getColumnName(int columnIndex) { return projection[columnIndex]; } @Override public String getString(int columnIndex) { return rows.get(nextRow).getAsString(projection[columnIndex]); } @Override public int getInt(int columnIndex) { return rows.get(nextRow).getAsInteger(projection[columnIndex]); } @Override public long getLong(int columnIndex) { return rows.get(nextRow).getAsLong(projection[columnIndex]); } @Override public boolean isAfterLast() { return nextRow >= getCount(); } @Override public boolean isLast() { return nextRow == getCount() - 1; } @Override public boolean moveToFirst() { nextRow = 0; return getCount() > 0; } @Override public boolean moveToNext() { return getCount() > ++nextRow; } @Override public int getCount() { return rows.size(); } @Override public int getColumnIndex(String columnName) { for (int i=0; i