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