Add BluetoothOppSendFileInfoTest

Test: atest BluetoothInstrumentionTests
Bug: 237467631
Tag: #refactor
Change-Id: I8f25a113cb700fe545a525d7cf76753b751a27a4
(cherry picked from commit 8d86ccbad1272648b237f9c15ff140e8ad31403c)
Merged-In: I8f25a113cb700fe545a525d7cf76753b751a27a4
diff --git a/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
index 8c494ed..8ce48c4 100644
--- a/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
+++ b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
@@ -22,6 +22,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
@@ -34,6 +35,7 @@
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
 
 /**
  * Proxy class for method calls to help with unit testing
@@ -133,6 +135,23 @@
     }
 
     /**
+     * Proxies {@link ContentResolver#openAssetFileDescriptor(Uri, String)}.
+     */
+    public AssetFileDescriptor contentResolverOpenAssetFileDescriptor(
+            ContentResolver contentResolver, final Uri uri, final String mode)
+            throws FileNotFoundException {
+        return contentResolver.openAssetFileDescriptor(uri, mode);
+    }
+
+    /**
+     * Proxies {@link ContentResolver#openInputStream(Uri)}.
+     */
+    public InputStream contentResolverOpenInputStream(ContentResolver contentResolver,
+            final Uri uri) throws FileNotFoundException {
+        return contentResolver.openInputStream(uri);
+    }
+
+    /**
      * Proxies {@link Context#sendBroadcast(Intent)}.
      */
     public void contextSendBroadcast(Context context, @RequiresPermission Intent intent) {
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java
index 46e3ba1..2adb8e5 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java
@@ -42,6 +42,7 @@
 import android.util.EventLog;
 import android.util.Log;
 
+import com.android.bluetooth.BluetoothMethodProxy;
 import com.android.bluetooth.R;
 
 import java.io.File;
@@ -119,9 +120,10 @@
             contentType = contentResolver.getType(uri);
             Cursor metadataCursor;
             try {
-                metadataCursor = contentResolver.query(uri, new String[]{
-                        OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE
-                }, null, null, null);
+                metadataCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
+                        contentResolver, uri, new String[]{
+                                OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE
+                        }, null, null, null);
             } catch (SQLiteException e) {
                 // some content providers don't support the DISPLAY_NAME or SIZE columns
                 metadataCursor = null;
@@ -180,7 +182,8 @@
                 // right size in _OpenableColumns.SIZE
                 // As a second source of getting the correct file length,
                 // get a file descriptor and get the stat length
-                AssetFileDescriptor fd = contentResolver.openAssetFileDescriptor(uri, "r");
+                AssetFileDescriptor fd = BluetoothMethodProxy.getInstance()
+                        .contentResolverOpenAssetFileDescriptor(contentResolver, uri, "r");
                 long statLength = fd.getLength();
                 if (length != statLength && statLength > 0) {
                     Log.e(TAG, "Content provider length is wrong (" + Long.toString(length)
@@ -200,7 +203,8 @@
                         length = getStreamSize(is);
                         Log.w(TAG, "File length not provided. Length from stream = " + length);
                         // Reset the stream
-                        fd = contentResolver.openAssetFileDescriptor(uri, "r");
+                        fd = BluetoothMethodProxy.getInstance()
+                                .contentResolverOpenAssetFileDescriptor(contentResolver, uri, "r");
                         is = fd.createInputStream();
                     }
                 } catch (IOException e) {
@@ -219,14 +223,16 @@
 
         if (is == null) {
             try {
-                is = (FileInputStream) contentResolver.openInputStream(uri);
+                is = (FileInputStream) BluetoothMethodProxy.getInstance()
+                        .contentResolverOpenInputStream(contentResolver, uri);
 
                 // If the database doesn't contain the file size, get the size
                 // by reading through the entire stream
                 if (length == 0) {
                     length = getStreamSize(is);
                     // Reset the stream
-                    is = (FileInputStream) contentResolver.openInputStream(uri);
+                    is = (FileInputStream) BluetoothMethodProxy.getInstance()
+                            .contentResolverOpenInputStream(contentResolver, uri);
                 }
             } catch (FileNotFoundException e) {
                 return SEND_FILE_INFO_ERROR;
diff --git a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppSendFileInfoTest.java b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppSendFileInfoTest.java
new file mode 100644
index 0000000..756836a
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppSendFileInfoTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2022 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.bluetooth.opp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.BluetoothMethodProxy;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothOppSendFileInfoTest {
+    Context mContext;
+    MatrixCursor mCursor;
+
+    @Mock
+    BluetoothMethodProxy mCallProxy;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        BluetoothMethodProxy.setInstanceForTesting(mCallProxy);
+    }
+
+    @After
+    public void tearDown() {
+        BluetoothMethodProxy.setInstanceForTesting(null);
+    }
+
+    @Test
+    public void createInstance_withFileInputStream() {
+        String fileName = "abc.txt";
+        String type = "text/plain";
+        long length = 10000;
+        FileInputStream inputStream = mock(FileInputStream.class);
+        int status = BluetoothShare.STATUS_SUCCESS;
+        BluetoothOppSendFileInfo info =
+                new BluetoothOppSendFileInfo(fileName, type, length, inputStream, status);
+
+        assertThat(info.mStatus).isEqualTo(status);
+        assertThat(info.mFileName).isEqualTo(fileName);
+        assertThat(info.mLength).isEqualTo(length);
+        assertThat(info.mInputStream).isEqualTo(inputStream);
+        assertThat(info.mMimetype).isEqualTo(type);
+    }
+
+    @Test
+    public void createInstance_withoutFileInputStream() {
+        String type = "text/plain";
+        long length = 10000;
+        int status = BluetoothShare.STATUS_SUCCESS;
+        String data = "Testing is boring";
+        BluetoothOppSendFileInfo info =
+                new BluetoothOppSendFileInfo(data, type, length, status);
+
+        assertThat(info.mStatus).isEqualTo(status);
+        assertThat(info.mData).isEqualTo(data);
+        assertThat(info.mLength).isEqualTo(length);
+        assertThat(info.mMimetype).isEqualTo(type);
+    }
+
+    @Test
+    public void generateFileInfo_withUnsupportedScheme_returnsSendFileInfoError() {
+        String type = "text/plain";
+        Uri uri = Uri.parse("https://www.google.com");
+
+        BluetoothOppSendFileInfo info = BluetoothOppSendFileInfo.generateFileInfo(mContext, uri,
+                type, true);
+        assertThat(info).isEqualTo(BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
+    }
+
+    @Test
+    public void generateFileInfo_withForbiddenExternalUri_returnsSendFileInfoError() {
+        String type = "text/plain";
+        Uri uri = Uri.parse("content://com.android.bluetooth.map.MmsFileProvider:8080");
+
+        BluetoothOppSendFileInfo info = BluetoothOppSendFileInfo.generateFileInfo(mContext, uri,
+                type, true);
+        assertThat(info).isEqualTo(BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
+    }
+
+    @Test
+    public void generateFileInfo_withoutPermissionForAccessingUri_returnsSendFileInfoError() {
+        String type = "text/plain";
+        Uri uri = Uri.parse("content:///hello/world");
+
+        doThrow(new SecurityException()).when(mCallProxy).contentResolverQuery(
+                any(), eq(uri), any(), any(), any(),
+                any());
+
+        BluetoothOppSendFileInfo info = BluetoothOppSendFileInfo.generateFileInfo(mContext, uri,
+                type, true);
+        assertThat(info).isEqualTo(BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
+    }
+
+    @Test
+    public void generateFileInfo_withUncorrectableMismatch_returnsSendFileInfoError()
+            throws IOException {
+        String type = "text/plain";
+        Uri uri = Uri.parse("content:///hello/world");
+
+        long fileLength = 0;
+        String fileName = "coolName.txt";
+
+        AssetFileDescriptor fd = mock(AssetFileDescriptor.class);
+        FileInputStream fs = mock(FileInputStream.class);
+
+        mCursor = new MatrixCursor(new String[]{
+                OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE
+        });
+        mCursor.addRow(new Object[]{fileName, fileLength});
+
+        doReturn(mCursor).when(mCallProxy).contentResolverQuery(
+                any(), eq(uri), any(), any(), any(),
+                any());
+
+        doReturn(fd).when(mCallProxy).contentResolverOpenAssetFileDescriptor(
+                any(), eq(uri), any());
+        doReturn(0L).when(fd).getLength();
+        doThrow(new IOException()).when(fd).createInputStream();
+        doReturn(fs).when(mCallProxy).contentResolverOpenInputStream(any(), eq(uri));
+        doReturn(0, -1).when(fs).read(any(), anyInt(), anyInt());
+
+        BluetoothOppSendFileInfo info = BluetoothOppSendFileInfo.generateFileInfo(mContext, uri,
+                type, true);
+
+        assertThat(info).isEqualTo(BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
+    }
+
+    @Test
+    public void generateFileInfo_withCorrectableMismatch_returnInfoWithCorrectLength()
+            throws IOException {
+        String type = "text/plain";
+        Uri uri = Uri.parse("content:///hello/world");
+
+        long fileLength = 0;
+        long correctFileLength = 1000;
+        String fileName = "coolName.txt";
+
+        AssetFileDescriptor fd = mock(AssetFileDescriptor.class);
+        FileInputStream fs = mock(FileInputStream.class);
+
+        mCursor = new MatrixCursor(new String[]{
+                OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE
+        });
+        mCursor.addRow(new Object[]{fileName, fileLength});
+
+        doReturn(mCursor).when(mCallProxy).contentResolverQuery(
+                any(), eq(uri), any(), any(), any(),
+                any());
+
+        doReturn(fd).when(mCallProxy).contentResolverOpenAssetFileDescriptor(
+                any(), eq(uri), any());
+        doReturn(0L).when(fd).getLength();
+        doReturn(fs).when(fd).createInputStream();
+
+        // the real size will be returned in getStreamSize(fs)
+        doReturn((int) correctFileLength, -1).when(fs).read(any(), anyInt(), anyInt());
+
+        BluetoothOppSendFileInfo info = BluetoothOppSendFileInfo.generateFileInfo(mContext, uri,
+                type, true);
+
+        assertThat(info.mInputStream).isEqualTo(fs);
+        assertThat(info.mFileName).isEqualTo(fileName);
+        assertThat(info.mLength).isEqualTo(correctFileLength);
+        assertThat(info.mStatus).isEqualTo(0);
+    }
+
+    @Test
+    public void generateFileInfo_withFileUriNotInExternalStorageDir_returnFileErrorInfo() {
+        String type = "text/plain";
+        Uri uri = Uri.parse("file:///obviously/not/in/external/storage");
+
+        BluetoothOppSendFileInfo info = BluetoothOppSendFileInfo.generateFileInfo(mContext, uri,
+                type, true);
+
+        assertThat(info).isEqualTo(BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
+    }
+}