1 /* 2 * Copyright (C) 2023 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.pandora 18 19 import android.bluetooth.BluetoothDevice 20 import android.bluetooth.BluetoothDevicePicker 21 import android.bluetooth.BluetoothManager 22 import android.content.ComponentName 23 import android.content.ContentUris 24 import android.content.Context 25 import android.content.Intent 26 import android.content.IntentFilter 27 import android.content.pm.PackageManager 28 import android.graphics.Bitmap 29 import android.os.Environment 30 import android.provider.MediaStore.Images.Media 31 import android.provider.MediaStore.MediaColumns 32 import androidx.test.InstrumentationRegistry 33 import androidx.test.uiautomator.By 34 import androidx.test.uiautomator.UiDevice 35 import androidx.test.uiautomator.Until 36 import com.google.protobuf.Empty 37 import io.grpc.stub.StreamObserver 38 import java.io.Closeable 39 import java.io.File 40 import java.io.FileOutputStream 41 import kotlinx.coroutines.CoroutineScope 42 import kotlinx.coroutines.Dispatchers 43 import kotlinx.coroutines.flow.Flow 44 import kotlinx.coroutines.flow.SharingStarted 45 import kotlinx.coroutines.flow.filter 46 import kotlinx.coroutines.flow.first 47 import kotlinx.coroutines.flow.shareIn 48 import pandora.OppGrpc.OppImplBase 49 import pandora.OppProto.* 50 51 private const val TAG = "PandoraOpp" 52 53 @kotlinx.coroutines.ExperimentalCoroutinesApi 54 class Opp(val context: Context) : OppImplBase(), Closeable { 55 private val IMAGE_FILE_NAME = "OPP_TEST_IMAGE.bmp" 56 private val INCOMING_FILE_TITLE = "Incoming file" 57 private val INCOMING_FILE_ACCEPT_BTN = "ACCEPT" 58 private val INCOMING_FILE_WAIT_TIMEOUT = 2000L 59 private val flow: Flow<Intent> 60 private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1)) 61 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 62 private val bluetoothAdapter = bluetoothManager.adapter 63 private var uiDevice: UiDevice = 64 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 65 66 init { 67 createImageFile() 68 69 val intentFilter = IntentFilter() 70 intentFilter.addAction(BluetoothDevice.ACTION_FOUND) 71 flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly) 72 } 73 closenull74 override fun close() { 75 val file = 76 File( 77 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), 78 IMAGE_FILE_NAME 79 ) 80 81 if (file.exists()) { 82 file.delete() 83 } 84 } 85 openRfcommChannelnull86 override fun openRfcommChannel( 87 request: OpenRfcommChannelRequest, 88 responseObserver: StreamObserver<Empty> 89 ) { 90 grpcUnary<Empty>(scope, responseObserver) { 91 val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter) 92 sendFile(bluetoothDevice) 93 Empty.getDefaultInstance() 94 } 95 } 96 openL2capChannelnull97 override fun openL2capChannel( 98 request: OpenL2capChannelRequest, 99 responseObserver: StreamObserver<Empty> 100 ) { 101 grpcUnary<Empty>(scope, responseObserver) { 102 val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter) 103 sendFile(bluetoothDevice) 104 Empty.getDefaultInstance() 105 } 106 } 107 acceptPutOperationnull108 override fun acceptPutOperation( 109 request: Empty, 110 responseObserver: StreamObserver<AcceptPutOperationResponse> 111 ) { 112 grpcUnary<AcceptPutOperationResponse>(scope, responseObserver) { 113 acceptIncomingFile() 114 AcceptPutOperationResponse.newBuilder().setStatus(PutStatus.ACCEPTED).build() 115 } 116 } 117 acceptIncomingFilenull118 fun acceptIncomingFile() { 119 uiDevice 120 .wait(Until.findObject(By.text(INCOMING_FILE_TITLE)), INCOMING_FILE_WAIT_TIMEOUT) 121 .click() 122 uiDevice.wait( 123 Until.hasObject(By.text(INCOMING_FILE_ACCEPT_BTN)), 124 INCOMING_FILE_WAIT_TIMEOUT 125 ) 126 uiDevice.waitForIdle() 127 uiDevice 128 .wait(Until.findObject(By.text(INCOMING_FILE_ACCEPT_BTN)), INCOMING_FILE_WAIT_TIMEOUT) 129 .click() 130 } 131 sendFilenull132 private suspend fun sendFile(bluetoothDevice: BluetoothDevice) { 133 initiateSendFile(getImageId(IMAGE_FILE_NAME), "image/bmp") 134 waitBluetoothDevice(bluetoothDevice) 135 val intent = Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED) 136 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bluetoothDevice) 137 context.sendBroadcast(intent) 138 } 139 waitBluetoothDevicenull140 suspend private fun waitBluetoothDevice(bluetoothDevice: BluetoothDevice) { 141 bluetoothAdapter.startDiscovery() 142 flow 143 .filter { 144 it.action == BluetoothDevice.ACTION_FOUND && 145 it.getBluetoothDeviceExtra() == bluetoothDevice 146 } 147 .first() 148 bluetoothAdapter.cancelDiscovery() 149 } 150 initiateSendFilenull151 private fun initiateSendFile(imageId: Long, type: String) { 152 val contentUri = ContentUris.withAppendedId(Media.EXTERNAL_CONTENT_URI, imageId) 153 154 try { 155 var sendingIntent = Intent(Intent.ACTION_SEND) 156 sendingIntent.setType(type) 157 val activity = 158 context.packageManager!! 159 .queryIntentActivities( 160 sendingIntent, 161 PackageManager.ResolveInfoFlags.of( 162 PackageManager.MATCH_DEFAULT_ONLY.toLong() 163 ) 164 ) 165 .filter { it!!.loadLabel(context.packageManager) == "Bluetooth" } 166 .first() 167 .activityInfo 168 sendingIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 169 sendingIntent.setComponent( 170 ComponentName(activity.applicationInfo.packageName, activity.name) 171 ) 172 sendingIntent.putExtra(Intent.EXTRA_STREAM, contentUri) 173 context.startActivity(sendingIntent) 174 } catch (e: Exception) { 175 e.printStackTrace() 176 } 177 } 178 createImageFilenull179 private fun createImageFile() { 180 val bitmapImage = Bitmap.createBitmap(30, 20, Bitmap.Config.ARGB_8888) 181 val file = 182 File( 183 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), 184 IMAGE_FILE_NAME 185 ) 186 var fileOutputStream: FileOutputStream? = null 187 188 if (file.exists()) { 189 file.delete() 190 } 191 file.createNewFile() 192 try { 193 fileOutputStream = FileOutputStream(file) 194 bitmapImage.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream) 195 fileOutputStream.flush() 196 } catch (e: Exception) { 197 e.printStackTrace() 198 } finally { 199 try { 200 if (fileOutputStream != null) { 201 fileOutputStream.close() 202 } 203 } catch (e: Exception) { 204 e.printStackTrace() 205 } 206 } 207 } 208 getImageIdnull209 private fun getImageId(fileName: String): Long { 210 val selection = MediaColumns.DISPLAY_NAME + "=?" 211 val selectionArgs = arrayOf(fileName) 212 val cursor = 213 context 214 .getContentResolver() 215 .query(Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, null) 216 217 cursor?.use { 218 it.let { 219 it.moveToFirst() 220 return it.getLong(it.getColumnIndexOrThrow(Media._ID)) 221 } 222 } 223 return 0L 224 } 225 } 226