| /* |
| * Copyright (c) 2008-2009, Motorola, Inc. |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * |
| * - Neither the name of the Motorola, Inc. nor the names of its contributors |
| * may be used to endorse or promote products derived from this software |
| * without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| * POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package com.android.bluetooth.pbap; |
| |
| import static android.Manifest.permission.BLUETOOTH_CONNECT; |
| |
| import android.annotation.RequiresPermission; |
| import android.app.Activity; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.BluetoothSocket; |
| import android.bluetooth.IBluetoothPbap; |
| import android.content.AttributionSource; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.database.ContentObserver; |
| import android.database.sqlite.SQLiteException; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.sysprop.BluetoothProperties; |
| import android.telephony.TelephonyManager; |
| import android.util.Log; |
| |
| import com.android.bluetooth.IObexConnectionHandler; |
| import com.android.bluetooth.ObexServerSockets; |
| import com.android.bluetooth.R; |
| import com.android.bluetooth.Utils; |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.btservice.ProfileService; |
| import com.android.bluetooth.btservice.storage.DatabaseManager; |
| import com.android.bluetooth.sdp.SdpManager; |
| import com.android.bluetooth.util.DevicePolicyUtils; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Objects; |
| |
| public class BluetoothPbapService extends ProfileService implements IObexConnectionHandler { |
| private static final String TAG = "BluetoothPbapService"; |
| |
| /** |
| * To enable PBAP DEBUG/VERBOSE logging - run below cmd in adb shell, and |
| * restart com.android.bluetooth process. only enable DEBUG log: |
| * "setprop log.tag.BluetoothPbapService DEBUG"; enable both VERBOSE and |
| * DEBUG log: "setprop log.tag.BluetoothPbapService VERBOSE" |
| */ |
| |
| public static final boolean DEBUG = true; |
| |
| public static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); |
| |
| /** |
| * The component name of the owned BluetoothPbapActivity |
| */ |
| private static final String PBAP_ACTIVITY = BluetoothPbapActivity.class.getCanonicalName(); |
| |
| /** |
| * Intent indicating incoming obex authentication request which is from |
| * PCE(Carkit) |
| */ |
| static final String AUTH_CHALL_ACTION = "com.android.bluetooth.pbap.authchall"; |
| |
| /** |
| * Intent indicating obex session key input complete by user which is sent |
| * from BluetoothPbapActivity |
| */ |
| static final String AUTH_RESPONSE_ACTION = "com.android.bluetooth.pbap.authresponse"; |
| |
| /** |
| * Intent indicating user canceled obex authentication session key input |
| * which is sent from BluetoothPbapActivity |
| */ |
| static final String AUTH_CANCELLED_ACTION = "com.android.bluetooth.pbap.authcancelled"; |
| |
| /** |
| * Intent indicating timeout for user confirmation, which is sent to |
| * BluetoothPbapActivity |
| */ |
| static final String USER_CONFIRM_TIMEOUT_ACTION = |
| "com.android.bluetooth.pbap.userconfirmtimeout"; |
| |
| /** |
| * Intent Extra name indicating session key which is sent from |
| * BluetoothPbapActivity |
| */ |
| static final String EXTRA_SESSION_KEY = "com.android.bluetooth.pbap.sessionkey"; |
| static final String EXTRA_DEVICE = "com.android.bluetooth.pbap.device"; |
| static final String THIS_PACKAGE_NAME = "com.android.bluetooth"; |
| |
| static final int MSG_ACQUIRE_WAKE_LOCK = 5004; |
| static final int MSG_RELEASE_WAKE_LOCK = 5005; |
| static final int MSG_STATE_MACHINE_DONE = 5006; |
| |
| static final int START_LISTENER = 1; |
| static final int USER_TIMEOUT = 2; |
| static final int SHUTDOWN = 3; |
| static final int LOAD_CONTACTS = 4; |
| static final int CONTACTS_LOADED = 5; |
| static final int CHECK_SECONDARY_VERSION_COUNTER = 6; |
| static final int ROLLOVER_COUNTERS = 7; |
| static final int GET_LOCAL_TELEPHONY_DETAILS = 8; |
| |
| static final int USER_CONFIRM_TIMEOUT_VALUE = 30000; |
| static final int RELEASE_WAKE_LOCK_DELAY = 10000; |
| |
| private PowerManager.WakeLock mWakeLock; |
| |
| private static String sLocalPhoneNum; |
| private static String sLocalPhoneName; |
| |
| private ObexServerSockets mServerSockets = null; |
| private DatabaseManager mDatabaseManager; |
| |
| private static final int SDP_PBAP_SERVER_VERSION = 0x0102; |
| // PBAP v1.2.3, Sec. 7.1.2: local phonebook and favorites |
| private static final int SDP_PBAP_SUPPORTED_REPOSITORIES = 0x000B; |
| private static final int SDP_PBAP_SUPPORTED_FEATURES = 0x021F; |
| |
| /* PBAP will use Bluetooth notification ID from 1000000 (included) to 2000000 (excluded). |
| The notification ID should be unique in Bluetooth package. */ |
| private static final int PBAP_NOTIFICATION_ID_START = 1000000; |
| private static final int PBAP_NOTIFICATION_ID_END = 2000000; |
| |
| private int mSdpHandle = -1; |
| |
| protected Context mContext; |
| |
| private PbapHandler mSessionStatusHandler; |
| private HandlerThread mHandlerThread; |
| private final HashMap<BluetoothDevice, PbapStateMachine> mPbapStateMachineMap = new HashMap<>(); |
| private volatile int mNextNotificationId = PBAP_NOTIFICATION_ID_START; |
| |
| // package and class name to which we send intent to check phone book access permission |
| private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings"; |
| private static final String ACCESS_AUTHORITY_CLASS = |
| "com.android.settings.bluetooth.BluetoothPermissionRequest"; |
| |
| private Thread mThreadLoadContacts; |
| private boolean mContactsLoaded = false; |
| |
| private Thread mThreadUpdateSecVersionCounter; |
| |
| private static BluetoothPbapService sBluetoothPbapService; |
| |
| public static boolean isEnabled() { |
| return BluetoothProperties.isProfilePbapServerEnabled().orElse(false); |
| } |
| |
| private class BluetoothPbapContentObserver extends ContentObserver { |
| BluetoothPbapContentObserver() { |
| super(new Handler()); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| Log.d(TAG, " onChange on contact uri "); |
| sendUpdateRequest(); |
| } |
| } |
| |
| private void sendUpdateRequest() { |
| if (mContactsLoaded) { |
| if (!mSessionStatusHandler.hasMessages(CHECK_SECONDARY_VERSION_COUNTER)) { |
| mSessionStatusHandler.sendMessage( |
| mSessionStatusHandler.obtainMessage(CHECK_SECONDARY_VERSION_COUNTER)); |
| } |
| } |
| } |
| |
| private BluetoothPbapContentObserver mContactChangeObserver; |
| |
| private void parseIntent(final Intent intent) { |
| String action = intent.getAction(); |
| if (DEBUG) { |
| Log.d(TAG, "action: " + action); |
| } |
| if (BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY.equals(action)) { |
| int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, |
| BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); |
| if (requestType != BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) { |
| return; |
| } |
| |
| BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| synchronized (mPbapStateMachineMap) { |
| PbapStateMachine sm = mPbapStateMachineMap.get(device); |
| if (sm == null) { |
| Log.w(TAG, "device not connected! device=" + device); |
| return; |
| } |
| mSessionStatusHandler.removeMessages(USER_TIMEOUT, sm); |
| int access = intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, |
| BluetoothDevice.CONNECTION_ACCESS_NO); |
| boolean savePreference = intent.getBooleanExtra( |
| BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false); |
| |
| if (access == BluetoothDevice.CONNECTION_ACCESS_YES) { |
| if (savePreference) { |
| device.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); |
| if (VERBOSE) { |
| Log.v(TAG, "setPhonebookAccessPermission(ACCESS_ALLOWED)"); |
| } |
| } |
| sm.sendMessage(PbapStateMachine.AUTHORIZED); |
| } else { |
| if (savePreference) { |
| device.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); |
| if (VERBOSE) { |
| Log.v(TAG, "setPhonebookAccessPermission(ACCESS_REJECTED)"); |
| } |
| } |
| sm.sendMessage(PbapStateMachine.REJECTED); |
| } |
| } |
| } else if (AUTH_RESPONSE_ACTION.equals(action)) { |
| String sessionKey = intent.getStringExtra(EXTRA_SESSION_KEY); |
| BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); |
| synchronized (mPbapStateMachineMap) { |
| PbapStateMachine sm = mPbapStateMachineMap.get(device); |
| if (sm == null) { |
| return; |
| } |
| Message msg = sm.obtainMessage(PbapStateMachine.AUTH_KEY_INPUT, sessionKey); |
| sm.sendMessage(msg); |
| } |
| } else if (AUTH_CANCELLED_ACTION.equals(action)) { |
| BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); |
| synchronized (mPbapStateMachineMap) { |
| PbapStateMachine sm = mPbapStateMachineMap.get(device); |
| if (sm == null) { |
| return; |
| } |
| sm.sendMessage(PbapStateMachine.AUTH_CANCELLED); |
| } |
| } else { |
| Log.w(TAG, "Unhandled intent action: " + action); |
| } |
| } |
| |
| private BroadcastReceiver mPbapReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| parseIntent(intent); |
| } |
| }; |
| |
| private void closeService() { |
| if (VERBOSE) { |
| Log.v(TAG, "Pbap Service closeService"); |
| } |
| |
| BluetoothPbapUtils.savePbapParams(this); |
| |
| if (mWakeLock != null) { |
| mWakeLock.release(); |
| mWakeLock = null; |
| } |
| |
| cleanUpServerSocket(); |
| |
| if (mSessionStatusHandler != null) { |
| mSessionStatusHandler.removeCallbacksAndMessages(null); |
| } |
| } |
| |
| private void cleanUpServerSocket() { |
| // Step 1, 2: clean up active server session and connection socket |
| synchronized (mPbapStateMachineMap) { |
| for (PbapStateMachine stateMachine : mPbapStateMachineMap.values()) { |
| stateMachine.sendMessage(PbapStateMachine.DISCONNECT); |
| } |
| } |
| // Step 3: clean up SDP record |
| cleanUpSdpRecord(); |
| // Step 4: clean up existing server sockets |
| if (mServerSockets != null) { |
| mServerSockets.shutdown(false); |
| mServerSockets = null; |
| } |
| } |
| |
| private void createSdpRecord() { |
| if (mSdpHandle > -1) { |
| Log.w(TAG, "createSdpRecord, SDP record already created"); |
| } |
| mSdpHandle = SdpManager.getDefaultManager() |
| .createPbapPseRecord("OBEX Phonebook Access Server", |
| mServerSockets.getRfcommChannel(), mServerSockets.getL2capPsm(), |
| SDP_PBAP_SERVER_VERSION, SDP_PBAP_SUPPORTED_REPOSITORIES, |
| SDP_PBAP_SUPPORTED_FEATURES); |
| if (DEBUG) { |
| Log.d(TAG, "created Sdp record, mSdpHandle=" + mSdpHandle); |
| } |
| } |
| |
| private void cleanUpSdpRecord() { |
| if (mSdpHandle < 0) { |
| Log.w(TAG, "cleanUpSdpRecord, SDP record never created"); |
| return; |
| } |
| int sdpHandle = mSdpHandle; |
| mSdpHandle = -1; |
| SdpManager sdpManager = SdpManager.getDefaultManager(); |
| if (DEBUG) { |
| Log.d(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle); |
| } |
| if (sdpManager == null) { |
| Log.e(TAG, "sdpManager is null"); |
| } else if (!sdpManager.removeSdpRecord(sdpHandle)) { |
| Log.w(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle); |
| } |
| } |
| |
| private class PbapHandler extends Handler { |
| private PbapHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| if (VERBOSE) { |
| Log.v(TAG, "Handler(): got msg=" + msg.what); |
| } |
| |
| switch (msg.what) { |
| case START_LISTENER: |
| mServerSockets = ObexServerSockets.create(BluetoothPbapService.this); |
| if (mServerSockets == null) { |
| Log.w(TAG, "ObexServerSockets.create() returned null"); |
| break; |
| } |
| createSdpRecord(); |
| // fetch Pbap Params to check if significant change has happened to Database |
| BluetoothPbapUtils.fetchPbapParams(mContext); |
| break; |
| case USER_TIMEOUT: |
| Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); |
| intent.setPackage(getString(R.string.pairing_ui_package)); |
| PbapStateMachine stateMachine = (PbapStateMachine) msg.obj; |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, stateMachine.getRemoteDevice()); |
| intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, |
| BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); |
| sendBroadcast(intent, BLUETOOTH_CONNECT, |
| Utils.getTempAllowlistBroadcastOptions()); |
| stateMachine.sendMessage(PbapStateMachine.REJECTED); |
| break; |
| case MSG_ACQUIRE_WAKE_LOCK: |
| if (mWakeLock == null) { |
| PowerManager pm = getSystemService(PowerManager.class); |
| mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, |
| "StartingObexPbapTransaction"); |
| mWakeLock.setReferenceCounted(false); |
| mWakeLock.acquire(); |
| Log.w(TAG, "Acquire Wake Lock"); |
| } |
| mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK); |
| mSessionStatusHandler.sendMessageDelayed( |
| mSessionStatusHandler.obtainMessage(MSG_RELEASE_WAKE_LOCK), |
| RELEASE_WAKE_LOCK_DELAY); |
| break; |
| case MSG_RELEASE_WAKE_LOCK: |
| if (mWakeLock != null) { |
| mWakeLock.release(); |
| mWakeLock = null; |
| } |
| break; |
| case SHUTDOWN: |
| closeService(); |
| break; |
| case LOAD_CONTACTS: |
| loadAllContacts(); |
| break; |
| case CONTACTS_LOADED: |
| mContactsLoaded = true; |
| break; |
| case CHECK_SECONDARY_VERSION_COUNTER: |
| updateSecondaryVersion(); |
| break; |
| case ROLLOVER_COUNTERS: |
| BluetoothPbapUtils.rolloverCounters(); |
| break; |
| case MSG_STATE_MACHINE_DONE: |
| PbapStateMachine sm = (PbapStateMachine) msg.obj; |
| BluetoothDevice remoteDevice = sm.getRemoteDevice(); |
| sm.quitNow(); |
| synchronized (mPbapStateMachineMap) { |
| mPbapStateMachineMap.remove(remoteDevice); |
| } |
| break; |
| case GET_LOCAL_TELEPHONY_DETAILS: |
| getLocalTelephonyDetails(); |
| default: |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Get the current connection state of PBAP with the passed in device |
| * |
| * @param device is the device whose connection state to PBAP we are trying to get |
| * @return current connection state, one of {@link BluetoothProfile#STATE_DISCONNECTED}, |
| * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or |
| * {@link BluetoothProfile#STATE_DISCONNECTING} |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public int getConnectionState(BluetoothDevice device) { |
| enforceCallingOrSelfPermission( |
| BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); |
| if (mPbapStateMachineMap == null) { |
| return BluetoothProfile.STATE_DISCONNECTED; |
| } |
| |
| synchronized (mPbapStateMachineMap) { |
| PbapStateMachine sm = mPbapStateMachineMap.get(device); |
| if (sm == null) { |
| return BluetoothProfile.STATE_DISCONNECTED; |
| } |
| return sm.getConnectionState(); |
| } |
| } |
| |
| List<BluetoothDevice> getConnectedDevices() { |
| if (mPbapStateMachineMap == null) { |
| return new ArrayList<>(); |
| } |
| synchronized (mPbapStateMachineMap) { |
| return new ArrayList<>(mPbapStateMachineMap.keySet()); |
| } |
| } |
| |
| List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { |
| List<BluetoothDevice> devices = new ArrayList<>(); |
| if (mPbapStateMachineMap == null || states == null) { |
| return devices; |
| } |
| synchronized (mPbapStateMachineMap) { |
| for (int state : states) { |
| for (BluetoothDevice device : mPbapStateMachineMap.keySet()) { |
| if (state == mPbapStateMachineMap.get(device).getConnectionState()) { |
| devices.add(device); |
| } |
| } |
| } |
| } |
| return devices; |
| } |
| |
| /** |
| * Set connection policy of the profile and tries to disconnect it if connectionPolicy is |
| * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} |
| * |
| * <p> The device should already be paired. |
| * Connection policy can be one of: |
| * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, |
| * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, |
| * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} |
| * |
| * @param device Paired bluetooth device |
| * @param connectionPolicy is the connection policy to set to for this profile |
| * @return true if connectionPolicy is set, false on error |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { |
| enforceCallingOrSelfPermission( |
| BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); |
| if (DEBUG) { |
| Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); |
| } |
| |
| if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.PBAP, |
| connectionPolicy)) { |
| return false; |
| } |
| if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { |
| disconnect(device); |
| } |
| return true; |
| } |
| |
| /** |
| * Get the connection policy of the profile. |
| * |
| * <p> The connection policy can be any of: |
| * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, |
| * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, |
| * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} |
| * |
| * @param device Bluetooth device |
| * @return connection policy of the device |
| * @hide |
| */ |
| public int getConnectionPolicy(BluetoothDevice device) { |
| if (device == null) { |
| throw new IllegalArgumentException("Null device"); |
| } |
| return mDatabaseManager |
| .getProfileConnectionPolicy(device, BluetoothProfile.PBAP); |
| } |
| |
| /** |
| * Disconnects pbap server profile with device |
| * @param device is the remote bluetooth device |
| */ |
| public void disconnect(BluetoothDevice device) { |
| synchronized (mPbapStateMachineMap) { |
| PbapStateMachine sm = mPbapStateMachineMap.get(device); |
| if (sm != null) { |
| sm.sendMessage(PbapStateMachine.DISCONNECT); |
| } |
| } |
| } |
| |
| static String getLocalPhoneNum() { |
| return sLocalPhoneNum; |
| } |
| |
| static String getLocalPhoneName() { |
| return sLocalPhoneName; |
| } |
| |
| @Override |
| protected IProfileServiceBinder initBinder() { |
| return new PbapBinder(this); |
| } |
| |
| @Override |
| protected boolean start() { |
| if (VERBOSE) { |
| Log.v(TAG, "start()"); |
| } |
| mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(), |
| "DatabaseManager cannot be null when PbapService starts"); |
| |
| // Enable owned Activity component |
| setComponentAvailable(PBAP_ACTIVITY, true); |
| |
| mContext = this; |
| mContactsLoaded = false; |
| mHandlerThread = new HandlerThread("PbapHandlerThread"); |
| mHandlerThread.start(); |
| mSessionStatusHandler = new PbapHandler(mHandlerThread.getLooper()); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); |
| filter.addAction(AUTH_RESPONSE_ACTION); |
| filter.addAction(AUTH_CANCELLED_ACTION); |
| BluetoothPbapConfig.init(this); |
| registerReceiver(mPbapReceiver, filter); |
| try { |
| mContactChangeObserver = new BluetoothPbapContentObserver(); |
| getContentResolver().registerContentObserver( |
| DevicePolicyUtils.getEnterprisePhoneUri(this), false, |
| mContactChangeObserver); |
| } catch (SQLiteException e) { |
| Log.e(TAG, "SQLite exception: " + e); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Illegal state exception, content observer is already registered"); |
| } |
| |
| setBluetoothPbapService(this); |
| |
| mSessionStatusHandler.sendMessage( |
| mSessionStatusHandler.obtainMessage(GET_LOCAL_TELEPHONY_DETAILS)); |
| mSessionStatusHandler.sendMessage(mSessionStatusHandler.obtainMessage(LOAD_CONTACTS)); |
| mSessionStatusHandler.sendMessage(mSessionStatusHandler.obtainMessage(START_LISTENER)); |
| return true; |
| } |
| |
| @Override |
| protected boolean stop() { |
| if (VERBOSE) { |
| Log.v(TAG, "stop()"); |
| } |
| setBluetoothPbapService(null); |
| if (mSessionStatusHandler != null) { |
| mSessionStatusHandler.obtainMessage(SHUTDOWN).sendToTarget(); |
| } |
| if (mHandlerThread != null) { |
| mHandlerThread.quitSafely(); |
| } |
| mContactsLoaded = false; |
| if (mContactChangeObserver == null) { |
| Log.i(TAG, "Avoid unregister when receiver it is not registered"); |
| return true; |
| } |
| unregisterReceiver(mPbapReceiver); |
| getContentResolver().unregisterContentObserver(mContactChangeObserver); |
| mContactChangeObserver = null; |
| setComponentAvailable(PBAP_ACTIVITY, false); |
| return true; |
| } |
| |
| /** |
| * Get the current instance of {@link BluetoothPbapService} |
| * |
| * @return current instance of {@link BluetoothPbapService} |
| */ |
| @VisibleForTesting |
| public static synchronized BluetoothPbapService getBluetoothPbapService() { |
| if (sBluetoothPbapService == null) { |
| Log.w(TAG, "getBluetoothPbapService(): service is null"); |
| return null; |
| } |
| if (!sBluetoothPbapService.isAvailable()) { |
| Log.w(TAG, "getBluetoothPbapService(): service is not available"); |
| return null; |
| } |
| return sBluetoothPbapService; |
| } |
| |
| private static synchronized void setBluetoothPbapService(BluetoothPbapService instance) { |
| if (DEBUG) { |
| Log.d(TAG, "setBluetoothPbapService(): set to: " + instance); |
| } |
| sBluetoothPbapService = instance; |
| } |
| |
| @Override |
| protected void setCurrentUser(int userId) { |
| Log.i(TAG, "setCurrentUser(" + userId + ")"); |
| UserManager userManager = getSystemService(UserManager.class); |
| if (userManager.isUserUnlocked(UserHandle.of(userId))) { |
| setUserUnlocked(userId); |
| } |
| } |
| |
| @Override |
| protected void setUserUnlocked(int userId) { |
| Log.i(TAG, "setUserUnlocked(" + userId + ")"); |
| sendUpdateRequest(); |
| } |
| |
| private static class PbapBinder extends IBluetoothPbap.Stub implements IProfileServiceBinder { |
| private BluetoothPbapService mService; |
| |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) |
| private BluetoothPbapService getService(AttributionSource source) { |
| if (!Utils.checkCallerIsSystemOrActiveUser(TAG) |
| || !Utils.checkServiceAvailable(mService, TAG) |
| || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { |
| return null; |
| } |
| return mService; |
| } |
| |
| PbapBinder(BluetoothPbapService service) { |
| if (VERBOSE) { |
| Log.v(TAG, "PbapBinder()"); |
| } |
| mService = service; |
| } |
| |
| @Override |
| public void cleanup() { |
| mService = null; |
| } |
| |
| @Override |
| public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { |
| if (DEBUG) { |
| Log.d(TAG, "getConnectedDevices"); |
| } |
| BluetoothPbapService service = getService(source); |
| if (service == null) { |
| return new ArrayList<>(0); |
| } |
| return service.getConnectedDevices(); |
| } |
| |
| @Override |
| public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states, |
| AttributionSource source) { |
| if (DEBUG) { |
| Log.d(TAG, "getDevicesMatchingConnectionStates"); |
| } |
| BluetoothPbapService service = getService(source); |
| if (service == null) { |
| return new ArrayList<>(0); |
| } |
| return service.getDevicesMatchingConnectionStates(states); |
| } |
| |
| @Override |
| public int getConnectionState(BluetoothDevice device, AttributionSource source) { |
| if (DEBUG) { |
| Log.d(TAG, "getConnectionState: " + device); |
| } |
| BluetoothPbapService service = getService(source); |
| if (service == null) { |
| return BluetoothAdapter.STATE_DISCONNECTED; |
| } |
| return service.getConnectionState(device); |
| } |
| |
| @Override |
| public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy, |
| AttributionSource source) { |
| if (DEBUG) { |
| Log.d(TAG, "setConnectionPolicy for device: " + device + ", policy:" |
| + connectionPolicy); |
| } |
| BluetoothPbapService service = getService(source); |
| if (service == null) { |
| return false; |
| } |
| return service.setConnectionPolicy(device, connectionPolicy); |
| } |
| |
| @Override |
| public void disconnect(BluetoothDevice device, AttributionSource source) { |
| if (DEBUG) { |
| Log.d(TAG, "disconnect"); |
| } |
| BluetoothPbapService service = getService(source); |
| if (service == null) { |
| return; |
| } |
| service.disconnect(device); |
| } |
| } |
| |
| @Override |
| public boolean onConnect(BluetoothDevice remoteDevice, BluetoothSocket socket) { |
| if (remoteDevice == null || socket == null) { |
| Log.e(TAG, "onConnect(): Unexpected null. remoteDevice=" + remoteDevice |
| + " socket=" + socket); |
| return false; |
| } |
| |
| PbapStateMachine sm = PbapStateMachine.make(this, mHandlerThread.getLooper(), remoteDevice, |
| socket, this, mSessionStatusHandler, mNextNotificationId); |
| mNextNotificationId++; |
| if (mNextNotificationId == PBAP_NOTIFICATION_ID_END) { |
| mNextNotificationId = PBAP_NOTIFICATION_ID_START; |
| } |
| synchronized (mPbapStateMachineMap) { |
| mPbapStateMachineMap.put(remoteDevice, sm); |
| } |
| sm.sendMessage(PbapStateMachine.REQUEST_PERMISSION); |
| return true; |
| } |
| |
| /** |
| * Get the phonebook access permission for the device; if unknown, ask the user. |
| * Send the result to the state machine. |
| * @param stateMachine PbapStateMachine which sends the request |
| */ |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void checkOrGetPhonebookPermission(PbapStateMachine stateMachine) { |
| BluetoothDevice device = stateMachine.getRemoteDevice(); |
| int permission = device.getPhonebookAccessPermission(); |
| if (DEBUG) { |
| Log.d(TAG, "getPhonebookAccessPermission() = " + permission); |
| } |
| |
| if (permission == BluetoothDevice.ACCESS_ALLOWED) { |
| setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| stateMachine.sendMessage(PbapStateMachine.AUTHORIZED); |
| } else if (permission == BluetoothDevice.ACCESS_REJECTED) { |
| stateMachine.sendMessage(PbapStateMachine.REJECTED); |
| } else { // permission == BluetoothDevice.ACCESS_UNKNOWN |
| Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); |
| intent.setClassName(BluetoothPbapService.ACCESS_AUTHORITY_PACKAGE, |
| BluetoothPbapService.ACCESS_AUTHORITY_CLASS); |
| intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, |
| BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); |
| intent.putExtra(BluetoothDevice.EXTRA_PACKAGE_NAME, this.getPackageName()); |
| this.sendOrderedBroadcast(intent, BLUETOOTH_CONNECT, |
| Utils.getTempAllowlistBroadcastOptions(), null/* resultReceiver */, |
| null/* scheduler */, Activity.RESULT_OK/* initialCode */, null/* initialData */, |
| null/* initialExtras */); |
| if (VERBOSE) { |
| Log.v(TAG, "waiting for authorization for connection from: " + device); |
| } |
| /* In case car kit time out and try to use HFP for phonebook |
| * access, while UI still there waiting for user to confirm */ |
| Message msg = mSessionStatusHandler.obtainMessage(BluetoothPbapService.USER_TIMEOUT, |
| stateMachine); |
| mSessionStatusHandler.sendMessageDelayed(msg, USER_CONFIRM_TIMEOUT_VALUE); |
| /* We will continue the process when we receive |
| * BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY from Settings app. */ |
| } |
| } |
| |
| /** |
| * Called when an unrecoverable error occurred in an accept thread. |
| * Close down the server socket, and restart. |
| */ |
| @Override |
| public synchronized void onAcceptFailed() { |
| Log.w(TAG, "PBAP server socket accept thread failed. Restarting the server socket"); |
| |
| if (mWakeLock != null) { |
| mWakeLock.release(); |
| mWakeLock = null; |
| } |
| |
| cleanUpServerSocket(); |
| |
| if (mSessionStatusHandler != null) { |
| mSessionStatusHandler.removeCallbacksAndMessages(null); |
| } |
| |
| synchronized (mPbapStateMachineMap) { |
| mPbapStateMachineMap.clear(); |
| } |
| |
| mSessionStatusHandler.sendMessage(mSessionStatusHandler.obtainMessage(START_LISTENER)); |
| } |
| |
| private void loadAllContacts() { |
| if (mThreadLoadContacts == null) { |
| Runnable r = new Runnable() { |
| @Override |
| public void run() { |
| BluetoothPbapUtils.loadAllContacts(mContext, |
| mSessionStatusHandler); |
| mThreadLoadContacts = null; |
| } |
| }; |
| mThreadLoadContacts = new Thread(r); |
| mThreadLoadContacts.start(); |
| } |
| } |
| |
| private void updateSecondaryVersion() { |
| if (mThreadUpdateSecVersionCounter == null) { |
| Runnable r = new Runnable() { |
| @Override |
| public void run() { |
| BluetoothPbapUtils.updateSecondaryVersionCounter(mContext, |
| mSessionStatusHandler); |
| mThreadUpdateSecVersionCounter = null; |
| } |
| }; |
| mThreadUpdateSecVersionCounter = new Thread(r); |
| mThreadUpdateSecVersionCounter.start(); |
| } |
| } |
| |
| private void getLocalTelephonyDetails() { |
| TelephonyManager tm = getSystemService(TelephonyManager.class); |
| if (tm != null) { |
| sLocalPhoneNum = tm.getLine1Number(); |
| sLocalPhoneName = this.getString(R.string.localPhoneName); |
| } |
| if (VERBOSE) |
| Log.v(TAG, "Local Phone Details- Number:" + sLocalPhoneNum |
| + ", Name:" + sLocalPhoneName); |
| } |
| } |