Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1ce94bc7 authored by Charlie Boutier's avatar Charlie Boutier Committed by Automerger Merge Worker
Browse files

Merge changes I188b534e,I432fccd9 into main am: 155f88c9

parents dd521f9b 155f88c9
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ from mmi2grpc._proxy import ProfileProxy
from pandora.host_grpc import Host
from pandora.host_pb2 import Connection
from pandora_experimental._android_grpc import Android
from pandora_experimental.opp_grpc import Opp


class OPPProxy(ProfileProxy):
@@ -35,6 +36,7 @@ class OPPProxy(ProfileProxy):

        self.host = Host(channel)
        self._android = Android(channel)
        self.opp = Opp(channel)

        self.connection = None

@@ -122,7 +124,7 @@ class OPPProxy(ProfileProxy):
        Take action to create an rfcomm channel for an OBEX connection.
        """

        self._android.SendFile('PTS')
        self.opp.OpenRfcommChannel(address=pts_addr)

        return "OK"

@@ -132,7 +134,7 @@ class OPPProxy(ProfileProxy):
        Take action to create an l2cap channel for an OBEX connection.
        """

        self._android.SendFile('PTS')
        self.opp.OpenL2capChannel(address=pts_addr)

        return "OK"

+1 −125
Original line number Diff line number Diff line
@@ -18,15 +18,7 @@ package com.android.pandora

import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.content.ComponentName
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Environment
import android.provider.MediaStore.Images.Media
import android.provider.MediaStore.MediaColumns
import android.provider.Telephony.*
import android.telephony.SmsManager
import android.telephony.SubscriptionManager
@@ -38,20 +30,15 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import com.google.protobuf.Empty
import io.grpc.stub.StreamObserver
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import pandora.AndroidGrpc.AndroidImplBase
import pandora.AndroidProto.*

private const val TAG = "PandoraAndroidInternal"

@kotlinx.coroutines.ExperimentalCoroutinesApi
class AndroidInternal(val context: Context) : AndroidImplBase(), Closeable {
class AndroidInternal(val context: Context) : AndroidImplBase() {

    private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
    private val INCOMING_FILE_ACCEPT_BTN = "ACCEPT"
@@ -71,24 +58,6 @@ class AndroidInternal(val context: Context) : AndroidImplBase(), Closeable {
    private var device: UiDevice =
        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    init {
        createImageFile()
    }

    override fun close() {
        scope.cancel()

        val file =
            File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                IMAGE_FILE_NAME
            )

        if (file.exists()) {
            file.delete()
        }
    }

    override fun log(request: LogRequest, responseObserver: StreamObserver<LogResponse>) {
        grpcUnary(scope, responseObserver) {
            Log.i(TAG, request.text)
@@ -148,14 +117,6 @@ class AndroidInternal(val context: Context) : AndroidImplBase(), Closeable {
        }
    }

    override fun sendFile(request: SendFileRequest, responseObserver: StreamObserver<Empty>) {
        grpcUnary<Empty>(scope, responseObserver) {
            initiateSendFile(getImageId(IMAGE_FILE_NAME), "image/bmp")
            waitAndSelectBluetoothDevice(request.name)
            Empty.getDefaultInstance()
        }
    }

    override fun sendPing(request: SendPingRequest, responseObserver: StreamObserver<Empty>) {
        grpcUnary<Empty>(scope, responseObserver) {
            val pingStatus =
@@ -163,89 +124,4 @@ class AndroidInternal(val context: Context) : AndroidImplBase(), Closeable {
            Empty.getDefaultInstance()
        }
    }

    suspend private fun waitAndSelectBluetoothDevice(name: String) {
        var selectJob =
            scope.async {
                device
                    .wait(Until.findObject(By.textContains(name)), BT_DEVICE_SELECT_WAIT_TIMEOUT)
                    .click()
            }
        selectJob.await()
    }

    private fun initiateSendFile(imageId: Long, type: String) {
        val contentUri = ContentUris.withAppendedId(Media.EXTERNAL_CONTENT_URI, imageId)

        try {
            var sendingIntent = Intent(Intent.ACTION_SEND)
            sendingIntent.setType(type)
            val activity =
                context.packageManager!!
                    .queryIntentActivities(
                        sendingIntent,
                        PackageManager.ResolveInfoFlags.of(
                            PackageManager.MATCH_DEFAULT_ONLY.toLong()
                        )
                    )
                    .filter { it!!.loadLabel(context.packageManager) == "Bluetooth" }
                    .first()
                    .activityInfo
            sendingIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            sendingIntent.setComponent(
                ComponentName(activity.applicationInfo.packageName, activity.name)
            )
            sendingIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
            context.startActivity(sendingIntent)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun getImageId(fileName: String): Long {
        val selection = MediaColumns.DISPLAY_NAME + "=?"
        val selectionArgs = arrayOf(fileName)
        val cursor =
            context
                .getContentResolver()
                .query(Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, null)

        cursor?.use {
            it.let {
                it.moveToFirst()
                return it.getLong(it.getColumnIndexOrThrow(Media._ID))
            }
        }
        return 0L
    }

    private fun createImageFile() {
        val bitmapImage = Bitmap.createBitmap(30, 20, Bitmap.Config.ARGB_8888)
        val file =
            File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                IMAGE_FILE_NAME
            )
        var fileOutputStream: FileOutputStream? = null

        if (file.exists()) {
            file.delete()
        }
        file.createNewFile()
        try {
            fileOutputStream = FileOutputStream(file)
            bitmapImage.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
            fileOutputStream.flush()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            try {
                if (fileOutputStream != null) {
                    fileOutputStream.close()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}
+192 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.pandora

import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevicePicker
import android.bluetooth.BluetoothManager
import android.content.ComponentName
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Environment
import android.provider.MediaStore.Images.Media
import android.provider.MediaStore.MediaColumns
import com.google.protobuf.Empty
import io.grpc.stub.StreamObserver
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.shareIn
import pandora.OppGrpc.OppImplBase
import pandora.OppProto.*

private const val TAG = "PandoraOpp"

@kotlinx.coroutines.ExperimentalCoroutinesApi
class Opp(val context: Context) : OppImplBase(), Closeable {
    private val IMAGE_FILE_NAME = "OPP_TEST_IMAGE.bmp"
    private val flow: Flow<Intent>
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
    private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
    private val bluetoothAdapter = bluetoothManager.adapter

    init {
        createImageFile()

        val intentFilter = IntentFilter()
        intentFilter.addAction(BluetoothDevice.ACTION_FOUND)
        flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly)
    }

    override fun close() {
        val file =
            File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                IMAGE_FILE_NAME
            )

        if (file.exists()) {
            file.delete()
        }
    }

    override fun openRfcommChannel(
        request: OpenRfcommChannelRequest,
        responseObserver: StreamObserver<Empty>
    ) {
        grpcUnary<Empty>(scope, responseObserver) {
            val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
            sendFile(bluetoothDevice)
            Empty.getDefaultInstance()
        }
    }

    override fun openL2capChannel(
        request: OpenL2capChannelRequest,
        responseObserver: StreamObserver<Empty>
    ) {
        grpcUnary<Empty>(scope, responseObserver) {
            val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
            sendFile(bluetoothDevice)
            Empty.getDefaultInstance()
        }
    }

    private suspend fun sendFile(bluetoothDevice: BluetoothDevice) {
        initiateSendFile(getImageId(IMAGE_FILE_NAME), "image/bmp")
        waitBluetoothDevice(bluetoothDevice)
        val intent = Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED)
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bluetoothDevice)
        context.sendBroadcast(intent)
    }

    suspend private fun waitBluetoothDevice(bluetoothDevice: BluetoothDevice) {
        bluetoothAdapter.startDiscovery()
        flow
            .filter {
                it.action == BluetoothDevice.ACTION_FOUND &&
                    it.getBluetoothDeviceExtra() == bluetoothDevice
            }
            .first()
        bluetoothAdapter.cancelDiscovery()
    }

    private fun initiateSendFile(imageId: Long, type: String) {
        val contentUri = ContentUris.withAppendedId(Media.EXTERNAL_CONTENT_URI, imageId)

        try {
            var sendingIntent = Intent(Intent.ACTION_SEND)
            sendingIntent.setType(type)
            val activity =
                context.packageManager!!
                    .queryIntentActivities(
                        sendingIntent,
                        PackageManager.ResolveInfoFlags.of(
                            PackageManager.MATCH_DEFAULT_ONLY.toLong()
                        )
                    )
                    .filter { it!!.loadLabel(context.packageManager) == "Bluetooth" }
                    .first()
                    .activityInfo
            sendingIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            sendingIntent.setComponent(
                ComponentName(activity.applicationInfo.packageName, activity.name)
            )
            sendingIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
            context.startActivity(sendingIntent)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun createImageFile() {
        val bitmapImage = Bitmap.createBitmap(30, 20, Bitmap.Config.ARGB_8888)
        val file =
            File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                IMAGE_FILE_NAME
            )
        var fileOutputStream: FileOutputStream? = null

        if (file.exists()) {
            file.delete()
        }
        file.createNewFile()
        try {
            fileOutputStream = FileOutputStream(file)
            bitmapImage.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
            fileOutputStream.flush()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            try {
                if (fileOutputStream != null) {
                    fileOutputStream.close()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    private fun getImageId(fileName: String): Long {
        val selection = MediaColumns.DISPLAY_NAME + "=?"
        val selectionArgs = arrayOf(fileName)
        val cursor =
            context
                .getContentResolver()
                .query(Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, null)

        cursor?.use {
            it.let {
                it.moveToFirst()
                return it.getLong(it.getColumnIndexOrThrow(Media._ID))
            }
        }
        return 0L
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ class Server(context: Context) {
                        BluetoothProfile.HID_HOST to ::Hid,
                        BluetoothProfile.PAN to ::Pan,
                        BluetoothProfile.PBAP to ::Pbap,
                        BluetoothProfile.OPP to ::Opp,
                    )
                    .filter { bluetoothAdapter.isEnabled }
                    .filter { bluetoothAdapter.getSupportedProfiles().contains(it.key) == true }
+1 −7
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ option java_outer_classname = "AndroidProto";

package pandora;

import "pandora/host.proto";
import "google/protobuf/empty.proto";

// This file contains Android-specific protos and rpcs that should not be part
@@ -20,8 +21,6 @@ service Android {
  rpc SendSMS(google.protobuf.Empty) returns (google.protobuf.Empty);
  // Accept incoming file
  rpc AcceptIncomingFile(google.protobuf.Empty) returns (google.protobuf.Empty);
  // Send file
  rpc SendFile(SendFileRequest) returns (google.protobuf.Empty);

  // Send ping
  rpc SendPing(SendPingRequest) returns (google.protobuf.Empty);
@@ -46,11 +45,6 @@ message SetAccessPermissionRequest {
  AccessType access_type = 2;
}

message SendFileRequest {
  // Peer Bluetooth Device name.
  string name = 1;
}

// Internal representation of a Connection - not exposed to clients, included here
// just for code-generation convenience. This is what we put in the Connection.cookie.
message InternalConnectionRef {
Loading