Merge "Fix ENAMETOOLONG issue when set trash or pending to file"
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index c9c67b7..0d6494b 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -57,7 +57,6 @@
 import android.system.OsConstants;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
-import android.util.ArraySet;
 import android.util.Log;
 import android.webkit.MimeTypeMap;
 
@@ -86,12 +85,16 @@
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public class FileUtils {
+    // Even though vfat allows 255 UCS-2 chars, we might eventually write to
+    // ext4 through a FUSE layer, so use that limit.
+    @VisibleForTesting
+    static final int MAX_FILENAME_BYTES = 255;
+
     /**
      * Drop-in replacement for {@link ParcelFileDescriptor#open(File, int)}
      * which adds security features like {@link OsConstants#O_CLOEXEC} and
@@ -523,9 +526,8 @@
                 res.append('_');
             }
         }
-        // Even though vfat allows 255 UCS-2 chars, we might eventually write to
-        // ext4 through a FUSE layer, so use that limit.
-        trimFilename(res, 255);
+
+        trimFilename(res, MAX_FILENAME_BYTES);
         return res.toString();
     }
 
@@ -1191,13 +1193,21 @@
         if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) {
             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
                     (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
-            resolvedDisplayName = String.format(
+            final String combinedString = String.format(
                     Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName);
+            // trim the file name to avoid ENAMETOOLONG error
+            // after trim the file, if the user unpending the file,
+            // the file name is not the original one
+            resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
         } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) {
             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
                     (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
-            resolvedDisplayName = String.format(
+            final String combinedString = String.format(
                     Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName);
+            // trim the file name to avoid ENAMETOOLONG error
+            // after trim the file, if the user untrashes the file,
+            // the file name is not the original one
+            resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
         } else {
             resolvedDisplayName = displayName;
         }
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 51d3628..655e7c6 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -21,6 +21,7 @@
 import static com.android.providers.media.util.FileUtils.isDownload;
 import static com.android.providers.media.util.FileUtils.isDownloadDir;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -63,6 +64,7 @@
 import com.android.providers.media.MediaProvider.VolumeNotFoundException;
 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
 import com.android.providers.media.util.FileUtils;
+import com.android.providers.media.util.FileUtilsTest;
 import com.android.providers.media.util.SQLiteQueryBuilder;
 
 import org.junit.AfterClass;
@@ -313,6 +315,59 @@
                 android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION));
     }
 
+    @Test
+    public void testTrashLongFileNameItemHasTrimmedFileName() throws Exception {
+        testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_TRASHED);
+    }
+
+    @Test
+    public void testPendingLongFileNameItemHasTrimmedFileName() throws Exception {
+        testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_PENDING);
+    }
+
+    private void testActionLongFileNameItemHasTrimmedFileName(String columnKey) throws Exception {
+        // We might have old files lurking, so force a clean slate
+        final Context context = InstrumentationRegistry.getTargetContext();
+        sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
+        sIsolatedResolver = sIsolatedContext.getContentResolver();
+        final String[] projection = new String[]{MediaColumns.DATA};
+        final File dir = Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+
+        // create extreme long file name
+        final String originalName = FileUtilsTest.createExtremeFileName("test" + System.nanoTime(),
+                ".jpg");
+
+        File file = stage(R.raw.lg_g4_iso_800_jpg, new File(dir, originalName));
+        final Uri uri = MediaStore.scanFile(sIsolatedResolver, file);
+        Log.v(TAG, "Scanned " + file + " as " + uri);
+
+        try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) {
+            assertNotNull(c);
+            assertEquals(1, c.getCount());
+            assertTrue(c.moveToFirst());
+            final String data = c.getString(0);
+            final String result = FileUtils.extractDisplayName(data);
+            assertEquals(originalName, result);
+        }
+
+        final Bundle extras = new Bundle();
+        extras.putBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, true);
+        final ContentValues values = new ContentValues();
+        values.put(columnKey, 1);
+        sIsolatedResolver.update(uri, values, extras);
+
+        try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) {
+            assertNotNull(c);
+            assertEquals(1, c.getCount());
+            assertTrue(c.moveToFirst());
+            final String data = c.getString(0);
+            final String result = FileUtils.extractDisplayName(data);
+            assertThat(result.length()).isAtMost(FileUtilsTest.MAX_FILENAME_BYTES);
+            assertNotEquals(originalName, result);
+        }
+    }
+
     /**
      * We already have solid coverage of this logic in
      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 5dd17ab..2e85421 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -83,6 +83,9 @@
 
 @RunWith(AndroidJUnit4.class)
 public class FileUtilsTest {
+    // Exposing here since it is also used by MediaProviderTest.java
+    public static final int MAX_FILENAME_BYTES = FileUtils.MAX_FILENAME_BYTES;
+
     /**
      * To help avoid flaky tests, give ourselves a unique nonce to be used for
      * all filesystem paths, so that we don't risk conflicting with previous
@@ -712,6 +715,16 @@
     }
 
     @Test
+    public void testComputeDataFromValues_Trashed_trimFileName() throws Exception {
+        testComputeDataFromValues_withAction_trimFileName(MediaColumns.IS_TRASHED);
+    }
+
+    @Test
+    public void testComputeDataFromValues_Pending_trimFileName() throws Exception {
+        testComputeDataFromValues_withAction_trimFileName(MediaColumns.IS_PENDING);
+    }
+
+    @Test
     public void testGetTopLevelNoMedia_CurrentDir() throws Exception {
         File dirInDownload = getNewDirInDownload("testGetTopLevelNoMedia_CurrentDir");
         File nomedia = new File(dirInDownload, ".nomedia");
@@ -798,4 +811,34 @@
             assertTrue("Unexpected actual file " + actualFile, expectedSet.contains(actualFile));
         }
     }
+
+    public static String createExtremeFileName(String prefix, String extension) {
+        // create extreme long file name
+        final int prefixLength = prefix.length();
+        final int extensionLength = extension.length();
+        StringBuilder str = new StringBuilder(prefix);
+        for (int i = 0; i < (MAX_FILENAME_BYTES - prefixLength - extensionLength); i++) {
+            str.append(i % 10);
+        }
+        return str.append(extension).toString();
+    }
+
+    private void testComputeDataFromValues_withAction_trimFileName(String columnKey) {
+        final String originalName = createExtremeFileName("test", ".jpg");
+        final String volumePath = "/storage/emulated/0/";
+        final ContentValues values = new ContentValues();
+        values.put(columnKey, 1);
+        values.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/");
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+        values.put(MediaColumns.DISPLAY_NAME, originalName);
+
+        FileUtils.computeDataFromValues(values, new File(volumePath), false /* isForFuse */);
+
+        final String data = values.getAsString(MediaColumns.DATA);
+        final String result = FileUtils.extractDisplayName(data);
+        // after adding the prefix .pending-timestamp or .trashed-timestamp,
+        // the largest length of the file name is MAX_FILENAME_BYTES 255
+        Truth.assertThat(result.length()).isAtMost(MAX_FILENAME_BYTES);
+        Truth.assertThat(result).isNotEqualTo(originalName);
+    }
 }