blob: dde39114108f4c58087411f6b00c3bd32bc359e1 [file] [log] [blame]
/*
* Copyright (C) 2019 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.media.client;
import static android.provider.MediaStore.rewriteToLegacy;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.UiAutomation;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.os.storage.StorageManager;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.DownloadColumns;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.MediaStore.Video.VideoColumns;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.test.filters.FlakyTest;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.google.common.truth.Truth;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* Verify that we preserve information from the old "legacy" provider from
* before we migrated into a Mainline module.
* <p>
* Specifically, values like {@link BaseColumns#_ID} and user edits like
* {@link MediaColumns#IS_FAVORITE} should be retained.
*/
@RunWith(AndroidJUnit4.class)
@FlakyTest(bugId = 176977253)
public class LegacyProviderMigrationTest {
private static final String TAG = "LegacyTest";
// TODO: expand test to cover secondary storage devices
private String mVolumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
private static final long POLLING_SLEEP_MILLIS = 100;
/**
* Number of media items to insert for {@link #testLegacy_Extreme()}.
*/
private static final int EXTREME_COUNT = 10_000;
private Uri mExternalAudio;
private Uri mExternalVideo;
private Uri mExternalImages;
private Uri mExternalDownloads;
private Uri mExternalPlaylists;
@Before
public void setUp() throws Exception {
Log.d(TAG, "Using volume " + mVolumeName);
mExternalAudio = MediaStore.Audio.Media.getContentUri(mVolumeName);
mExternalVideo = MediaStore.Video.Media.getContentUri(mVolumeName);
mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName);
mExternalDownloads = MediaStore.Downloads.getContentUri(mVolumeName);
Uri playlists = MediaStore.Audio.Playlists.getContentUri(mVolumeName);
mExternalPlaylists = playlists.buildUpon()
.appendQueryParameter("silent", "true").build();
}
private ContentValues generateValues(int mediaType, String mimeType, String dirName)
throws Exception {
return generateValues(mediaType, mimeType, dirName, 0);
}
private ContentValues generateValues(int mediaType, String mimeType, String dirName, int resId)
throws Exception {
final Context context = InstrumentationRegistry.getContext();
final File dir = context.getSystemService(StorageManager.class)
.getStorageVolume(MediaStore.Files.getContentUri(mVolumeName)).getDirectory();
final File subDir = new File(dir, dirName);
File file = new File(subDir, "legacy" + System.nanoTime() + "."
+ MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType));
if (resId != 0) {
file = stageFile(resId, file.getAbsolutePath());
}
final ContentValues values = new ContentValues();
values.put(FileColumns.MEDIA_TYPE, mediaType);
values.put(MediaColumns.DATA, file.getAbsolutePath());
values.put(MediaColumns.DISPLAY_NAME, file.getName());
values.put(MediaColumns.MIME_TYPE, mimeType);
values.put(MediaColumns.VOLUME_NAME, mVolumeName);
values.put(MediaColumns.DATE_ADDED, String.valueOf(System.currentTimeMillis() / 1_000));
values.put(MediaColumns.OWNER_PACKAGE_NAME,
InstrumentationRegistry.getContext().getPackageName());
return values;
}
private static File stageFile(int resId, String path) throws Exception {
final Context context = InstrumentationRegistry.getContext();
final File file = new File(path);
try (InputStream in = context.getResources().openRawResource(resId);
OutputStream out = new FileOutputStream(file)) {
FileUtils.copy(in, out);
}
return file;
}
@Test
public void testLegacy_Orientation() throws Exception {
// Use an image file with orientation of 90 degrees
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE,
"image/jpeg", Environment.DIRECTORY_PICTURES, R.raw.orientation_90);
values.put(MediaColumns.ORIENTATION, String.valueOf(90));
doLegacy(mExternalImages, values);
}
@Test
public void testLegacy_Pending() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE,
"image/png", Environment.DIRECTORY_PICTURES);
values.put(MediaColumns.IS_PENDING, String.valueOf(1));
values.put(MediaColumns.DATE_EXPIRES, String.valueOf(System.currentTimeMillis() / 1_000));
doLegacy(mExternalImages, values);
}
@Test
public void testLegacy_Trashed() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE,
"image/png", Environment.DIRECTORY_PICTURES);
values.put(MediaColumns.IS_TRASHED, String.valueOf(1));
doLegacy(mExternalImages, values);
}
@Test
public void testLegacy_Favorite() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE,
"image/png", Environment.DIRECTORY_PICTURES);
values.put(MediaColumns.IS_FAVORITE, String.valueOf(1));
doLegacy(mExternalImages, values);
}
@Test
public void testLegacy_Orphaned() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE,
"image/png", Environment.DIRECTORY_PICTURES);
values.putNull(MediaColumns.OWNER_PACKAGE_NAME);
doLegacy(mExternalImages, values);
}
@Test
public void testLegacy_Audio() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_AUDIO,
"audio/mpeg", Environment.DIRECTORY_MUSIC);
values.put(AudioColumns.BOOKMARK, String.valueOf(42));
doLegacy(mExternalAudio, values);
}
@Test
public void testLegacy_Video() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_VIDEO,
"video/mpeg", Environment.DIRECTORY_MOVIES);
values.put(VideoColumns.BOOKMARK, String.valueOf(42));
values.put(VideoColumns.TAGS, "My Tags");
values.put(VideoColumns.CATEGORY, "My Category");
values.put(VideoColumns.IS_PRIVATE, String.valueOf(1));
doLegacy(mExternalVideo, values);
}
@Test
public void testLegacy_Image() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE,
"image/png", Environment.DIRECTORY_PICTURES);
values.put(ImageColumns.IS_PRIVATE, String.valueOf(1));
doLegacy(mExternalImages, values);
}
@Test
public void testLegacy_Download() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_NONE,
"application/x-iso9660-image", Environment.DIRECTORY_DOWNLOADS);
values.put(DownloadColumns.DOWNLOAD_URI, "http://example.com/download");
values.put(DownloadColumns.REFERER_URI, "http://example.com/referer");
doLegacy(mExternalDownloads, values);
}
/**
* Test that migrating from legacy database with volume_name=NULL doesn't
* result in empty cursor when queried.
*/
@Test
public void testMigrateNullVolumeName() throws Exception {
final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE,
"image/png", Environment.DIRECTORY_PICTURES);
values.remove(MediaColumns.VOLUME_NAME);
doLegacy(mExternalImages, values);
}
@Test
public void testLegacy_PlaylistMap() throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
final ContentValues audios[] = new ContentValues[] {
generateValues(FileColumns.MEDIA_TYPE_AUDIO, "audio/mpeg",
Environment.DIRECTORY_MUSIC),
generateValues(FileColumns.MEDIA_TYPE_AUDIO, "audio/mpeg",
Environment.DIRECTORY_MUSIC),
};
final String playlistMimeType = "audio/mpegurl";
final ContentValues playlist = generateValues(FileColumns.MEDIA_TYPE_PLAYLIST,
playlistMimeType, "Playlists");
final String playlistName = "LegacyPlaylistName_" + System.nanoTime();
playlist.put(MediaStore.Audio.PlaylistsColumns.NAME, playlistName);
File playlistFile = new File(playlist.getAsString(MediaColumns.DATA));
playlistFile.delete();
final ContentValues playlistMap = new ContentValues();
playlistMap.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, 1);
prepareProviders(context, ui);
try (ContentProviderClient legacy = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) {
// Step 1: Insert the playlist entry into the playlists table.
final Uri playlistUri = rewriteToLegacy(legacy.insert(
rewriteToLegacy(mExternalPlaylists), playlist));
long playlistId = ContentUris.parseId(playlistUri);
final Uri playlistMemberUri = MediaStore.rewriteToLegacy(
MediaStore.Audio.Playlists.Members.getContentUri(mVolumeName, playlistId)
.buildUpon()
.appendQueryParameter("silent", "true").build());
for (ContentValues values : audios) {
// Step 2: Write the audio file to the legacy mediastore.
final Uri audioUri =
rewriteToLegacy(legacy.insert(rewriteToLegacy(mExternalAudio), values));
// Remember our ID to check it later
values.put(MediaColumns._ID, audioUri.getLastPathSegment());
long audioId = ContentUris.parseId(audioUri);
playlistMap.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId);
playlistMap.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId);
// Step 3: Add a mapping to playlist members.
legacy.insert(playlistMemberUri, playlistMap);
}
// Insert a stale row, We only have 3 items in the database. #4 is a stale row
// and will be skipped from the playlist during the migration.
playlistMap.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, 4);
legacy.insert(playlistMemberUri, playlistMap);
}
// This will delete MediaProvider data and restarts MediaProvider, and mounts storage.
clearProviders(context, ui);
// Verify scan on DEMAND doesn't delete any virtual playlist files.
MediaStore.scanFile(context.getContentResolver(),
Environment.getExternalStorageDirectory());
// Playlist files are created from playlist NAME
final File musicDir = new File(context.getSystemService(StorageManager.class)
.getStorageVolume(MediaStore.Files.getContentUri(mVolumeName)).getDirectory(),
Environment.DIRECTORY_MUSIC);
playlistFile = new File(musicDir, playlistName + "."
+ MimeTypeMap.getSingleton().getExtensionFromMimeType(playlistMimeType));
// Wait for scan on MEDIA_MOUNTED to create "real" playlist files.
pollForFile(playlistFile);
// Scan again to verify updated playlist metadata
MediaStore.scanFile(context.getContentResolver(), playlistFile);
try (ContentProviderClient modern = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY)) {
long legacyPlaylistId =
playlistMap.getAsLong(MediaStore.Audio.Playlists.Members.PLAYLIST_ID);
long legacyAudioId1 = audios[0].getAsLong(MediaColumns._ID);
long legacyAudioId2 = audios[1].getAsLong(MediaColumns._ID);
// Verify that playlist_id matches with legacy playlist_id
{
Uri playlists = MediaStore.Audio.Playlists.getContentUri(mVolumeName);
final String[] project = {FileColumns._ID, MediaStore.Audio.PlaylistsColumns.NAME};
try (Cursor cursor = modern.query(playlists, project, null, null, null)) {
boolean found = false;
while(cursor.moveToNext()) {
if (cursor.getLong(0) == legacyPlaylistId) {
found = true;
assertEquals(playlistName, cursor.getString(1));
break;
}
}
assertTrue(found);
}
}
// Verify that playlist_members map matches legacy playlist_members map.
{
Uri members = MediaStore.Audio.Playlists.Members.getContentUri(
mVolumeName, legacyPlaylistId);
final String[] project = { MediaStore.Audio.Playlists.Members.AUDIO_ID };
try (Cursor cursor = modern.query(members, project, null, null,
MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER)) {
assertTrue(cursor.moveToNext());
assertEquals(legacyAudioId1, cursor.getLong(0));
assertTrue(cursor.moveToNext());
assertEquals(legacyAudioId2, cursor.getLong(0));
assertFalse(cursor.moveToNext());
}
}
// Verify that migrated playlist audio_id refers to legacy audio file.
{
Uri modernAudioUri = ContentUris.withAppendedId(
MediaStore.Audio.Media.getContentUri(mVolumeName), legacyAudioId1);
final String[] project = {FileColumns.DATA};
try (Cursor cursor = modern.query(modernAudioUri, project, null, null, null)) {
assertTrue(cursor.moveToFirst());
assertEquals(audios[0].getAsString(MediaColumns.DATA), cursor.getString(0));
}
}
}
}
private static void prepareProviders(Context context, UiAutomation ui) throws Exception {
final ProviderInfo legacyProvider = context.getPackageManager()
.resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0);
final ProviderInfo modernProvider = context.getPackageManager()
.resolveContentProvider(MediaStore.AUTHORITY, 0);
// Only continue if we have both providers to test against
Assume.assumeNotNull(legacyProvider);
Assume.assumeNotNull(modernProvider);
// Clear data on the legacy provider so that we create a database
waitForMountedAndIdle(context.getContentResolver());
executeShellCommand("sync", ui);
executeShellCommand("pm clear " + legacyProvider.applicationInfo.packageName, ui);
waitForMountedAndIdle(context.getContentResolver());
}
private static void clearProviders(Context context, UiAutomation ui) throws Exception {
final ProviderInfo modernProvider = context.getPackageManager()
.resolveContentProvider(MediaStore.AUTHORITY, 0);
// Clear data on the modern provider so that the initial scan recovers
// metadata from the legacy provider
waitForMountedAndIdle(context.getContentResolver());
executeShellCommand("sync", ui);
executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui);
waitForMountedAndIdle(context.getContentResolver());
}
/**
* Verify that a legacy database with thousands of media entries can be
* successfully migrated.
*/
@Test
public void testLegacy_Extreme() throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
prepareProviders(context, ui);
// Create thousands of items in the legacy provider
try (ContentProviderClient legacy = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) {
// We're purposefully "silent" to avoid creating the raw file on
// disk, since otherwise this test would take several minutes
final Uri insertTarget = rewriteToLegacy(
mExternalImages.buildUpon().appendQueryParameter("silent", "true").build());
final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (int i = 0; i < EXTREME_COUNT; i++) {
ops.add(ContentProviderOperation.newInsert(insertTarget)
.withValues(generateValues(FileColumns.MEDIA_TYPE_IMAGE, "image/png",
Environment.DIRECTORY_PICTURES))
.build());
if ((ops.size() > 1_000) || (i == (EXTREME_COUNT - 1))) {
Log.v(TAG, "Inserting items...");
legacy.applyBatch(MediaStore.AUTHORITY_LEGACY, ops);
ops.clear();
}
}
}
clearProviders(context, ui);
// Confirm that details from legacy provider have migrated
try (ContentProviderClient modern = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY)) {
try (Cursor cursor = modern.query(mExternalImages, null, null, null)) {
Truth.assertThat(cursor.getCount()).isAtLeast(EXTREME_COUNT);
}
}
}
private void doLegacy(Uri collectionUri, ContentValues values) throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
prepareProviders(context, ui);
// Create a well-known entry in legacy provider, and write data into
// place to ensure the file is created on disk
final Uri legacyUri;
final File legacyFile;
try (ContentProviderClient legacy = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) {
legacyUri = rewriteToLegacy(legacy.insert(rewriteToLegacy(collectionUri), values));
legacyFile = new File(values.getAsString(MediaColumns.DATA));
// Remember our ID to check it later
values.put(MediaColumns._ID, legacyUri.getLastPathSegment());
// Drop media type from the columns we check, since it's implicitly
// verified via the collection Uri
values.remove(FileColumns.MEDIA_TYPE);
// Drop raw path, since we may rename pending or trashed files
values.remove(FileColumns.DATA);
}
// This will delete MediaProvider data and restarts MediaProvider, and mounts storage.
clearProviders(context, ui);
// Make sure we do not lose the ORIENTATION column after database migration
// We check this column again after the scan
if (values.getAsString(MediaColumns.ORIENTATION) != null) {
assertOrientationColumn(collectionUri, values, context, legacyFile);
}
// And force a scan to confirm upgraded data survives
MediaStore.scanVolume(context.getContentResolver(),
MediaStore.getVolumeName(collectionUri));
assertColumnsHaveExpectedValues(collectionUri, values, context, legacyFile);
}
private void assertOrientationColumn(Uri collectionUri, ContentValues originalValues,
Context context, File legacyFile) throws Exception {
final ContentValues values = new ContentValues();
values.put(MediaColumns.ORIENTATION, (String) originalValues.get(MediaColumns.ORIENTATION));
assertColumnsHaveExpectedValues(collectionUri, values, context, legacyFile);
}
private void assertColumnsHaveExpectedValues(Uri collectionUri, ContentValues values,
Context context, File legacyFile) throws Exception {
// Confirm that details from legacy provider have migrated
try (ContentProviderClient modern = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY)) {
final Bundle extras = new Bundle();
extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
MediaColumns.DISPLAY_NAME + "=?");
extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
new String[] { legacyFile.getName() });
extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
extras.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
try (Cursor cursor = pollForCursor(modern, collectionUri, extras)) {
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
for (String key : values.keySet()) {
assertWithMessage("Checking key %s", key)
.that(cursor.getString(cursor.getColumnIndexOrThrow(key)))
.isEqualTo(values.get(key));
}
}
}
}
private static void waitForMountedAndIdle(ContentResolver resolver) {
// We purposefully perform these operations twice in this specific
// order, since clearing the data on a package can asynchronously
// perform a vold reset, which can make us think storage is ready and
// mounted when it's moments away from being torn down.
pollForExternalStorageState();
MediaStore.waitForIdle(resolver);
pollForExternalStorageState();
MediaStore.waitForIdle(resolver);
}
private static Cursor pollForCursor(ContentProviderClient modern, Uri collectionUri,
Bundle extras) throws Exception {
Cursor cursor = null;
for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
try {
cursor = modern.query(collectionUri, null, extras, null);
return cursor;
} catch (IllegalArgumentException e) {
// try again
}
Log.v(TAG, "Waiting for..." + collectionUri);
SystemClock.sleep(POLLING_SLEEP_MILLIS);
}
fail("Timed out while waiting for uri " + collectionUri);
return cursor;
}
private static void pollForFile(File file) {
for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
if (file.exists()) return;
Log.v(TAG, "Waiting for..." + file);
SystemClock.sleep(POLLING_SLEEP_MILLIS);
}
fail("Timed out while waiting for file " + file);
}
private static void pollForExternalStorageState() {
final File target = Environment.getExternalStorageDirectory();
for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
try {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState(target))
&& Os.statvfs(target.getAbsolutePath()).f_blocks > 0) {
return;
}
} catch (ErrnoException ignored) {
}
Log.v(TAG, "Waiting for external storage...");
SystemClock.sleep(POLLING_SLEEP_MILLIS);
}
fail("Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
}
public static String executeShellCommand(String command, UiAutomation uiAutomation)
throws IOException {
int attempt = 0;
while (attempt++ < 5) {
try {
return executeShellCommandInternal(command, uiAutomation);
} catch (InterruptedIOException e) {
// Hmm, we had trouble executing the shell command; the best we
// can do is try again a few more times
Log.v(TAG, "Trouble executing " + command + "; trying again", e);
}
}
throw new IOException("Failed to execute " + command);
}
public static String executeShellCommandInternal(String command, UiAutomation uiAutomation)
throws IOException {
Log.v(TAG, "$ " + command);
ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
BufferedReader br = null;
try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String str = null;
StringBuilder out = new StringBuilder();
while ((str = br.readLine()) != null) {
Log.v(TAG, "> " + str);
out.append(str);
}
return out.toString();
} finally {
if (br != null) {
br.close();
}
}
}
public static void copyFromCursorToContentValues(@NonNull String column, @NonNull Cursor cursor,
@NonNull ContentValues values) {
final int index = cursor.getColumnIndex(column);
if (index != -1) {
if (cursor.isNull(index)) {
values.putNull(column);
} else {
values.put(column, cursor.getString(index));
}
}
}
}