diff --git a/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java b/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java index c6633daab22..d765a8012ed 100644 --- a/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java +++ b/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java @@ -463,6 +463,13 @@ void sendIntent(BluetoothDevice device, int newState, int reason, } } + if (newState == BluetoothDevice.BOND_NONE) { + // Remove the permissions for unbonded devices + mAdapterService.setMessageAccessPermission(device, BluetoothDevice.ACCESS_UNKNOWN); + mAdapterService.setPhonebookAccessPermission(device, BluetoothDevice.ACCESS_UNKNOWN); + mAdapterService.setSimAccessPermission(device, BluetoothDevice.ACCESS_UNKNOWN); + } + Intent intent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, newState); diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java index 2adb8e5f44c..7ce134341aa 100644 --- a/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java +++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java @@ -32,6 +32,8 @@ package com.android.bluetooth.opp; +import static android.os.UserHandle.myUserId; + import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; @@ -39,6 +41,7 @@ import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.OpenableColumns; +import android.text.TextUtils; import android.util.EventLog; import android.util.Log; @@ -49,6 +52,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Objects; /** * This class stores information about a single sending file It will only be @@ -117,6 +121,11 @@ public static BluetoothOppSendFileInfo generateFileInfo(Context context, Uri uri return SEND_FILE_INFO_ERROR; } + if (isContentUriForOtherUser(uri)) { + Log.e(TAG, "Uri: " + uri + " is invalid for user " + myUserId()); + return SEND_FILE_INFO_ERROR; + } + contentType = contentResolver.getType(uri); Cursor metadataCursor; try { @@ -253,6 +262,12 @@ public static BluetoothOppSendFileInfo generateFileInfo(Context context, Uri uri return new BluetoothOppSendFileInfo(fileName, contentType, length, is, 0); } + private static boolean isContentUriForOtherUser(Uri uri) { + String uriUserId = uri.getUserInfo(); + return !TextUtils.isEmpty(uriUserId) + && !Objects.equals(uriUserId, String.valueOf(myUserId())); + } + private static long getStreamSize(FileInputStream is) throws IOException { long length = 0; byte[] unused = new byte[4096]; 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 index 756836afaa8..9fb93ac9683 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppSendFileInfoTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppSendFileInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2024 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. @@ -13,208 +13,193 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package com.android.bluetooth.opp; +import static android.os.UserHandle.myUserId; + 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 static org.mockito.Mockito.spy; +import android.content.ContentResolver; import android.content.Context; +import android.content.IContentProvider; +import android.content.pm.ApplicationInfo; import android.content.res.AssetFileDescriptor; import android.database.MatrixCursor; import android.net.Uri; +import android.os.Build; import android.provider.OpenableColumns; -import android.util.Log; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContext; -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; + public static final String PROVIDER_NAME_MEDIA = "media"; + TestContext mContext; + TestContentResolver mContentResolver; + MockContentProvider mContentProvider; MatrixCursor mCursor; - @Mock - BluetoothMethodProxy mCallProxy; + private static Uri buildContentUriWithEncodedAuthority(String authority) { + return new Uri.Builder().scheme("content") + .encodedAuthority(authority) + .path("external/images/media/1") + .build(); + } @Before public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - BluetoothMethodProxy.setInstanceForTesting(mCallProxy); - } - - @After - public void tearDown() { - BluetoothMethodProxy.setInstanceForTesting(null); + mContext = new TestContext(); + mContentResolver = mContext.getContentResolver(); + mContentProvider = mContext.getContentProvider(); } @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"; + public void generateFileInfo_withContentUriForOtherUser_returnsSendFileInfoError() + throws Exception { + String type = "image/jpeg"; + Uri uri = buildContentUriWithEncodedAuthority((myUserId() + 1) + "@" + PROVIDER_NAME_MEDIA); + doReturn(type).when(mContentProvider).getType(any()); + long fileLength = 1000; + String fileName = "pic.jpg"; + FileInputStream fs = mock(FileInputStream.class); + AssetFileDescriptor fd = mock(AssetFileDescriptor.class); + doReturn(fileLength).when(fd).getLength(); + doReturn(fs).when(fd).createInputStream(); + doReturn(fd).when(mContentProvider).openAssetFile(eq(uri), any(), any()); + mCursor = + new MatrixCursor(new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}); + mCursor.addRow(new Object[]{fileName, fileLength}); + doReturn(mCursor).when(mContentProvider).query(eq(uri), any(), any(), any(), any()); 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); + 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); + public void generateFileInfo_withContentUriForImplicitUser_returnsInfoWithCorrectLength() + throws Exception { + String type = "image/jpeg"; + Uri uri = buildContentUriWithEncodedAuthority(PROVIDER_NAME_MEDIA); + doReturn(type).when(mContentProvider).getType(any()); + long fileLength = 1000; + String fileName = "pic.jpg"; FileInputStream fs = mock(FileInputStream.class); - - mCursor = new MatrixCursor(new String[]{ - OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE - }); + AssetFileDescriptor fd = mock(AssetFileDescriptor.class); + doReturn(fileLength).when(fd).getLength(); + doReturn(fs).when(fd).createInputStream(); + doReturn(fd).when(mContentProvider).openAssetFile(eq(uri), any(), any()); + 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); + doReturn(mCursor).when(mContentProvider).query(eq(uri), any(), any(), any(), any()); + BluetoothOppSendFileInfo info = + BluetoothOppSendFileInfo.generateFileInfo(mContext, uri, type, true); + assertThat(info.mInputStream).isEqualTo(fs); + assertThat(info.mFileName).isEqualTo(fileName); + assertThat(info.mLength).isEqualTo(fileLength); + assertThat(info.mStatus).isEqualTo(0); } @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); + public void generateFileInfo_withContentUriForSameUser_returnsInfoWithCorrectLength() + throws Exception { + String type = "image/jpeg"; + Uri uri = buildContentUriWithEncodedAuthority(myUserId() + "@" + PROVIDER_NAME_MEDIA); + doReturn(type).when(mContentProvider).getType(any()); + long fileLength = 1000; + String fileName = "pic.jpg"; 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(); + AssetFileDescriptor fd = mock(AssetFileDescriptor.class); + doReturn(fileLength).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); - + doReturn(fd).when(mContentProvider).openAssetFile(eq(uri), any(), any()); + mCursor = + new MatrixCursor(new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}); + mCursor.addRow(new Object[]{fileName, fileLength}); + doReturn(mCursor).when(mContentProvider).query(eq(uri), any(), any(), any(), any()); + 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.mLength).isEqualTo(fileLength); 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); + public static final class TestContext extends MockContext { + private final TestContentResolver mContentResolver; + private final MockContentProvider mContentProvider; + + public TestContext() { + mContentProvider = spy(new MockContentProvider(this)); + mContentResolver = new TestContentResolver(this, mContentProvider); + } + + @Override + public TestContentResolver getContentResolver() { + return mContentResolver; + } + + public MockContentProvider getContentProvider() { + return mContentProvider; + } + + @Override + public String getOpPackageName() { + return "test.package"; + } + + @Override + public ApplicationInfo getApplicationInfo() { + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = Build.VERSION.SDK_INT; + return applicationInfo; + } + } - assertThat(info).isEqualTo(BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR); + public static final class TestContentResolver extends ContentResolver { + private final MockContentProvider mContentProvider; + + public TestContentResolver(Context context, MockContentProvider contentProvider) { + super(context, contentProvider); + mContentProvider = contentProvider; + } + + @Override + protected IContentProvider acquireProvider(Context c, String name) { + return mContentProvider.getIContentProvider(); + } + + @Override + public boolean releaseProvider(IContentProvider icp) { + return true; + } + + @Override + protected IContentProvider acquireUnstableProvider(Context c, String name) { + return mContentProvider.getIContentProvider(); + } + + @Override + public boolean releaseUnstableProvider(IContentProvider icp) { + return true; + } + + @Override + public void unstableProviderDied(IContentProvider icp) { + } } }