1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bluetooth.opp;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import static org.mockito.ArgumentMatchers.argThat;
23 import static org.mockito.ArgumentMatchers.nullable;
24 import static org.mockito.Mockito.any;
25 import static org.mockito.Mockito.anyInt;
26 import static org.mockito.Mockito.doAnswer;
27 import static org.mockito.Mockito.doNothing;
28 import static org.mockito.Mockito.doReturn;
29 import static org.mockito.Mockito.eq;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.spy;
32 import static org.mockito.Mockito.verify;
33 
34 import android.bluetooth.BluetoothAdapter;
35 import android.bluetooth.BluetoothDevice;
36 import android.content.Context;
37 import android.content.ContextWrapper;
38 import android.content.Intent;
39 import android.content.pm.PackageManager;
40 import android.content.pm.ResolveInfo;
41 import android.database.Cursor;
42 import android.net.Uri;
43 import android.os.ParcelFileDescriptor;
44 
45 import androidx.test.platform.app.InstrumentationRegistry;
46 
47 import com.android.bluetooth.BluetoothMethodProxy;
48 import com.android.bluetooth.R;
49 import com.android.bluetooth.TestUtils;
50 import com.android.bluetooth.opp.BluetoothOppTestUtils.CursorMockData;
51 
52 import org.junit.After;
53 import org.junit.Before;
54 import org.junit.Rule;
55 import org.junit.Test;
56 import org.mockito.Mock;
57 import org.mockito.Spy;
58 import org.mockito.junit.MockitoJUnit;
59 import org.mockito.junit.MockitoRule;
60 
61 import java.util.List;
62 import java.util.Objects;
63 import java.util.concurrent.atomic.AtomicInteger;
64 
65 public class BluetoothOppUtilityTest {
66 
67     private static final Uri CORRECT_FORMAT_BUT_INVALID_FILE_URI =
68             Uri.parse("content://com.android.bluetooth.opp/btopp/0123455343467");
69     private static final Uri INCORRECT_FORMAT_URI = Uri.parse("www.google.com");
70 
71     Context mContext;
72     @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
73 
74     @Mock Cursor mCursor;
75 
76     @Spy BluetoothMethodProxy mCallProxy = BluetoothMethodProxy.getInstance();
77 
78     @Before
setUp()79     public void setUp() throws Exception {
80         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
81         BluetoothMethodProxy.setInstanceForTesting(mCallProxy);
82         TestUtils.setUpUiTest();
83     }
84 
85     @After
tearDown()86     public void tearDown() throws Exception {
87         TestUtils.tearDownUiTest();
88 
89         BluetoothMethodProxy.setInstanceForTesting(null);
90     }
91 
92     @Test
isBluetoothShareUri_correctlyCheckUri()93     public void isBluetoothShareUri_correctlyCheckUri() {
94         assertThat(BluetoothOppUtility.isBluetoothShareUri(INCORRECT_FORMAT_URI)).isFalse();
95         assertThat(BluetoothOppUtility.isBluetoothShareUri(CORRECT_FORMAT_BUT_INVALID_FILE_URI))
96                 .isTrue();
97     }
98 
99     @Test
queryRecord_withInvalidFileUrl_returnsNull()100     public void queryRecord_withInvalidFileUrl_returnsNull() {
101         doReturn(null)
102                 .when(mCallProxy)
103                 .contentResolverQuery(
104                         any(),
105                         eq(CORRECT_FORMAT_BUT_INVALID_FILE_URI),
106                         eq(null),
107                         eq(null),
108                         eq(null),
109                         eq(null));
110         assertThat(BluetoothOppUtility.queryRecord(mContext, CORRECT_FORMAT_BUT_INVALID_FILE_URI))
111                 .isNull();
112     }
113 
114     @Test
queryRecord_mockCursor_returnsInstance()115     public void queryRecord_mockCursor_returnsInstance() {
116         String destinationValue = "AA:BB:CC:00:11:22";
117 
118         doReturn(mCursor)
119                 .when(mCallProxy)
120                 .contentResolverQuery(
121                         any(),
122                         eq(CORRECT_FORMAT_BUT_INVALID_FILE_URI),
123                         eq(null),
124                         eq(null),
125                         eq(null),
126                         eq(null));
127         doReturn(true).when(mCursor).moveToFirst();
128         doReturn(destinationValue).when(mCursor).getString(anyInt());
129         assertThat(BluetoothOppUtility.queryRecord(mContext, CORRECT_FORMAT_BUT_INVALID_FILE_URI))
130                 .isInstanceOf(BluetoothOppTransferInfo.class);
131     }
132 
133     @Test
queryTransfersInBatch_returnsCorrectUrlArrayList()134     public void queryTransfersInBatch_returnsCorrectUrlArrayList() {
135         long timestampValue = 123456;
136         String where = BluetoothShare.TIMESTAMP + " == " + timestampValue;
137         AtomicInteger cnt = new AtomicInteger(1);
138 
139         doReturn(mCursor)
140                 .when(mCallProxy)
141                 .contentResolverQuery(
142                         any(),
143                         eq(BluetoothShare.CONTENT_URI),
144                         eq(new String[] {BluetoothShare._DATA}),
145                         eq(where),
146                         eq(null),
147                         eq(BluetoothShare._ID));
148 
149         doAnswer(invocation -> cnt.incrementAndGet() > 5).when(mCursor).isAfterLast();
150         doReturn(CORRECT_FORMAT_BUT_INVALID_FILE_URI.toString()).when(mCursor).getString(0);
151 
152         List<String> answer = BluetoothOppUtility.queryTransfersInBatch(mContext, timestampValue);
153         for (String url : answer) {
154             assertThat(url).isEqualTo(CORRECT_FORMAT_BUT_INVALID_FILE_URI.toString());
155         }
156     }
157 
158     @Test
openReceivedFile_fileNotExist()159     public void openReceivedFile_fileNotExist() {
160 
161         Uri contentResolverUri = Uri.parse("content://com.android.bluetooth.opp/btopp/0123");
162         Uri fileUri = Uri.parse("content:///tmp/randomFileName.txt");
163 
164         Context spiedContext = spy(new ContextWrapper(mContext));
165 
166         doReturn(0).when(mCallProxy).contentResolverDelete(any(), any(), any(), any());
167         doReturn(mCursor)
168                 .when(mCallProxy)
169                 .contentResolverQuery(
170                         any(), eq(contentResolverUri), any(), eq(null), eq(null), eq(null));
171 
172         doReturn(true).when(mCursor).moveToFirst();
173         doReturn(fileUri.toString()).when(mCursor).getString(anyInt());
174 
175         doReturn(0)
176                 .when(mCallProxy)
177                 .contentResolverDelete(
178                         any(), any(), nullable(String.class), nullable(String[].class));
179 
180         // Do nothing since we don't need the actual activity to be launched.
181         doNothing().when(spiedContext).startActivity(any());
182 
183         BluetoothOppUtility.openReceivedFile(
184                 spiedContext, "randomFileName.txt", "text/plain", 0L, contentResolverUri);
185 
186         verify(spiedContext)
187                 .startActivity(
188                         argThat(
189                                 argument ->
190                                         Objects.equals(
191                                                 argument.getComponent().getClassName(),
192                                                 BluetoothOppBtErrorActivity.class.getName())));
193     }
194 
195     @Test
openReceivedFile_fileExist_HandlingApplicationExist()196     public void openReceivedFile_fileExist_HandlingApplicationExist() throws Exception {
197         Uri contentResolverUri = Uri.parse("content://com.android.bluetooth.opp/btopp/0123");
198         Uri fileUri = Uri.parse("content:///tmp/randomFileName.txt");
199 
200         ParcelFileDescriptor pfd = mock(ParcelFileDescriptor.class);
201 
202         Context spiedContext = spy(new ContextWrapper(mContext));
203         // Control BluetoothOppUtility#fileExists flow
204         doReturn(mCursor)
205                 .when(mCallProxy)
206                 .contentResolverQuery(
207                         any(), eq(contentResolverUri), any(), eq(null), eq(null), eq(null));
208 
209         doReturn(true).when(mCursor).moveToFirst();
210         doReturn(fileUri.toString()).when(mCursor).getString(anyInt());
211 
212         doReturn(0).when(mCallProxy).contentResolverDelete(any(), any(), any(), any());
213         doReturn(pfd).when(mCallProxy).contentResolverOpenFileDescriptor(any(), eq(fileUri), any());
214 
215         // Control BluetoothOppUtility#isRecognizedFileType flow
216         PackageManager mockManager = mock(PackageManager.class);
217         doReturn(mockManager).when(spiedContext).getPackageManager();
218         doReturn(List.of(new ResolveInfo()))
219                 .when(mockManager)
220                 .queryIntentActivities(any(), anyInt());
221 
222         BluetoothOppUtility.openReceivedFile(
223                 spiedContext, "randomFileName.txt", "text/plain", 0L, contentResolverUri);
224 
225         verify(spiedContext)
226                 .startActivity(
227                         argThat(
228                                 argument ->
229                                         Objects.equals(
230                                                         argument.getData(),
231                                                         Uri.parse(
232                                                                 "content:///tmp/randomFileName.txt"))
233                                                 && Objects.equals(
234                                                         argument.getAction(), Intent.ACTION_VIEW)));
235 
236         verify(pfd).close();
237     }
238 
239     @Test
openReceivedFile_fileExist_HandlingApplicationNotExist()240     public void openReceivedFile_fileExist_HandlingApplicationNotExist() throws Exception {
241         Uri contentResolverUri = Uri.parse("content://com.android.bluetooth.opp/btopp/0123");
242         Uri fileUri = Uri.parse("content:///tmp/randomFileName.txt");
243         ParcelFileDescriptor pfd = mock(ParcelFileDescriptor.class);
244 
245         Context spiedContext = spy(new ContextWrapper(mContext));
246         // Control BluetoothOppUtility#fileExists flow
247         doReturn(mCursor)
248                 .when(mCallProxy)
249                 .contentResolverQuery(
250                         any(), eq(contentResolverUri), any(), eq(null), eq(null), eq(null));
251 
252         doReturn(true).when(mCursor).moveToFirst();
253         doReturn(fileUri.toString()).when(mCursor).getString(anyInt());
254 
255         doReturn(0).when(mCallProxy).contentResolverDelete(any(), any(), any(), any());
256         doReturn(pfd).when(mCallProxy).contentResolverOpenFileDescriptor(any(), eq(fileUri), any());
257 
258         // Control BluetoothOppUtility#isRecognizedFileType flow
259         PackageManager mockManager = mock(PackageManager.class);
260         doReturn(mockManager).when(spiedContext).getPackageManager();
261         doReturn(List.of()).when(mockManager).queryIntentActivities(any(), anyInt());
262 
263         // Do nothing since we don't need the actual activity to be launched.
264         doNothing().when(spiedContext).startActivity(any());
265 
266         BluetoothOppUtility.openReceivedFile(
267                 spiedContext, "randomFileName.txt", "text/plain", 0L, contentResolverUri);
268 
269         verify(spiedContext)
270                 .startActivity(
271                         argThat(
272                                 argument ->
273                                         argument.getComponent()
274                                                 .getClassName()
275                                                 .equals(
276                                                         BluetoothOppBtErrorActivity.class
277                                                                 .getName())));
278         verify(pfd).close();
279     }
280 
281     @Test
fillRecord_filledAllProperties()282     public void fillRecord_filledAllProperties() {
283         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
284         int idValue = 1234;
285         int directionValue = BluetoothShare.DIRECTION_OUTBOUND;
286         long totalBytesValue = 10;
287         long currentBytesValue = 1;
288         int statusValue = BluetoothShare.STATUS_PENDING;
289         Long timestampValue = 123456789L;
290         String destinationValue = "AA:BB:CC:00:11:22";
291         String fileNameValue = mContext.getString(R.string.unknown_file);
292         String fileTypeValue = "text/plain";
293         BluetoothDevice remoteDevice = adapter.getRemoteDevice(destinationValue);
294         String deviceNameValue =
295                 BluetoothOppManager.getInstance(mContext).getDeviceName(remoteDevice);
296 
297         List<CursorMockData> cursorMockDataList =
298                 List.of(
299                         new CursorMockData(BluetoothShare._ID, 0, idValue),
300                         new CursorMockData(BluetoothShare.STATUS, 1, statusValue),
301                         new CursorMockData(BluetoothShare.DIRECTION, 2, directionValue),
302                         new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, totalBytesValue),
303                         new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, currentBytesValue),
304                         new CursorMockData(BluetoothShare.TIMESTAMP, 5, timestampValue),
305                         new CursorMockData(BluetoothShare.DESTINATION, 6, destinationValue),
306                         new CursorMockData(BluetoothShare._DATA, 7, null),
307                         new CursorMockData(BluetoothShare.FILENAME_HINT, 8, null),
308                         new CursorMockData(BluetoothShare.MIMETYPE, 9, fileTypeValue));
309 
310         BluetoothOppTestUtils.setUpMockCursor(mCursor, cursorMockDataList);
311 
312         BluetoothOppTransferInfo info = new BluetoothOppTransferInfo();
313         BluetoothOppUtility.fillRecord(mContext, mCursor, info);
314 
315         assertThat(info.mID).isEqualTo(idValue);
316         assertThat(info.mStatus).isEqualTo(statusValue);
317         assertThat(info.mDirection).isEqualTo(directionValue);
318         assertThat(info.mTotalBytes).isEqualTo(totalBytesValue);
319         assertThat(info.mCurrentBytes).isEqualTo(currentBytesValue);
320         assertThat(info.mTimeStamp).isEqualTo(timestampValue);
321         assertThat(info.mDestAddr).isEqualTo(destinationValue);
322         assertThat(info.mFileUri).isEqualTo(null);
323         assertThat(info.mFileType).isEqualTo(fileTypeValue);
324         assertThat(info.mDeviceName).isEqualTo(deviceNameValue);
325         assertThat(info.mHandoverInitiated).isEqualTo(false);
326         assertThat(info.mFileName).isEqualTo(fileNameValue);
327     }
328 
329     @Test
fileExists_returnFalse()330     public void fileExists_returnFalse() {
331         assertThat(BluetoothOppUtility.fileExists(mContext, CORRECT_FORMAT_BUT_INVALID_FILE_URI))
332                 .isFalse();
333     }
334 
335     @Test
isRecognizedFileType_withWrongFileUriAndMimeType_returnFalse()336     public void isRecognizedFileType_withWrongFileUriAndMimeType_returnFalse() {
337         assertThat(
338                         BluetoothOppUtility.isRecognizedFileType(
339                                 mContext, CORRECT_FORMAT_BUT_INVALID_FILE_URI, "aWrongMimeType"))
340                 .isFalse();
341     }
342 
343     @Test
formatProgressText()344     public void formatProgressText() {
345         assertThat(BluetoothOppUtility.formatProgressText(100, 42)).isEqualTo("42%");
346     }
347 
348     @Test
formatResultText()349     public void formatResultText() {
350         String text = BluetoothOppUtility.formatResultText(1, 2, mContext);
351 
352         assertThat(text.contains("1") && text.contains("2")).isTrue();
353     }
354 
355     @Test
getStatusDescription_returnCorrectString()356     public void getStatusDescription_returnCorrectString() {
357         String deviceName = "randomName";
358         assertThat(
359                         BluetoothOppUtility.getStatusDescription(
360                                 mContext, BluetoothShare.STATUS_PENDING, deviceName))
361                 .isEqualTo(mContext.getString(R.string.status_pending));
362         assertThat(
363                         BluetoothOppUtility.getStatusDescription(
364                                 mContext, BluetoothShare.STATUS_RUNNING, deviceName))
365                 .isEqualTo(mContext.getString(R.string.status_running));
366         assertThat(
367                         BluetoothOppUtility.getStatusDescription(
368                                 mContext, BluetoothShare.STATUS_SUCCESS, deviceName))
369                 .isEqualTo(mContext.getString(R.string.status_success));
370         assertThat(
371                         BluetoothOppUtility.getStatusDescription(
372                                 mContext, BluetoothShare.STATUS_NOT_ACCEPTABLE, deviceName))
373                 .isEqualTo(mContext.getString(R.string.status_not_accept));
374         assertThat(
375                         BluetoothOppUtility.getStatusDescription(
376                                 mContext, BluetoothShare.STATUS_FORBIDDEN, deviceName))
377                 .isEqualTo(mContext.getString(R.string.status_forbidden));
378         assertThat(
379                         BluetoothOppUtility.getStatusDescription(
380                                 mContext, BluetoothShare.STATUS_CANCELED, deviceName))
381                 .isEqualTo(mContext.getString(R.string.status_canceled));
382         assertThat(
383                         BluetoothOppUtility.getStatusDescription(
384                                 mContext, BluetoothShare.STATUS_FILE_ERROR, deviceName))
385                 .isEqualTo(mContext.getString(R.string.status_file_error));
386         assertThat(
387                         BluetoothOppUtility.getStatusDescription(
388                                 mContext, BluetoothShare.STATUS_CONNECTION_ERROR, deviceName))
389                 .isEqualTo(mContext.getString(R.string.status_connection_error));
390         assertThat(
391                         BluetoothOppUtility.getStatusDescription(
392                                 mContext, BluetoothShare.STATUS_ERROR_NO_SDCARD, deviceName))
393                 .isEqualTo(
394                         mContext.getString(
395                                 BluetoothOppUtility.deviceHasNoSdCard()
396                                         ? R.string.status_no_sd_card_nosdcard
397                                         : R.string.status_no_sd_card_default));
398         assertThat(
399                         BluetoothOppUtility.getStatusDescription(
400                                 mContext, BluetoothShare.STATUS_ERROR_SDCARD_FULL, deviceName))
401                 .isEqualTo(
402                         mContext.getString(
403                                 BluetoothOppUtility.deviceHasNoSdCard()
404                                         ? R.string.bt_sm_2_1_nosdcard
405                                         : R.string.bt_sm_2_1_default));
406         assertThat(
407                         BluetoothOppUtility.getStatusDescription(
408                                 mContext, BluetoothShare.STATUS_BAD_REQUEST, deviceName))
409                 .isEqualTo(mContext.getString(R.string.status_protocol_error));
410         assertThat(BluetoothOppUtility.getStatusDescription(mContext, 12345465, deviceName))
411                 .isEqualTo(mContext.getString(R.string.status_unknown_error));
412     }
413 
414     @Test
originalUri_trimBeforeAt()415     public void originalUri_trimBeforeAt() {
416         Uri originalUri = Uri.parse("com.android.bluetooth.opp.BluetoothOppSendFileInfo");
417         Uri uri = Uri.parse("com.android.bluetooth.opp.BluetoothOppSendFileInfo@dfe15a6");
418         assertThat(BluetoothOppUtility.originalUri(uri)).isEqualTo(originalUri);
419     }
420 
421     @Test
fileInfo_testFileInfoFunctions()422     public void fileInfo_testFileInfoFunctions() {
423         assertThat(BluetoothOppUtility.getSendFileInfo(CORRECT_FORMAT_BUT_INVALID_FILE_URI))
424                 .isEqualTo(BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
425         assertThat(
426                         BluetoothOppUtility.generateUri(
427                                         CORRECT_FORMAT_BUT_INVALID_FILE_URI,
428                                         BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR)
429                                 .toString())
430                 .contains(CORRECT_FORMAT_BUT_INVALID_FILE_URI.toString());
431         try {
432             BluetoothOppUtility.putSendFileInfo(
433                     CORRECT_FORMAT_BUT_INVALID_FILE_URI,
434                     BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
435             BluetoothOppUtility.closeSendFileInfo(CORRECT_FORMAT_BUT_INVALID_FILE_URI);
436         } catch (Exception e) {
437             assertWithMessage("Exception should not happen. " + e).fail();
438         }
439     }
440 }
441