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

Commit c105b9e9 authored by chelseahao's avatar chelseahao Committed by Chelsea Hao
Browse files

Branch some audio sharing functions to settingslib.

Test: atest -c com.android.settingslib.bluetooth
Bug: 327332332
Flag: NA
Change-Id: Ia1a167f609d59ca26e284a26b6236ed218697af1
parent ac45efa0
Loading
Loading
Loading
Loading
+183 −128
Original line number Diff line number Diff line
@@ -3,10 +3,13 @@ package com.android.settingslib.bluetooth;
import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -30,6 +33,7 @@ import androidx.annotation.WorkerThread;
import androidx.core.graphics.drawable.IconCompat;

import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.settingslib.widget.AdaptiveOutlineDrawable;

@@ -52,8 +56,8 @@ public class BluetoothUtils {
    public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
    private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
    private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
    private static final Set<String> EXCLUSIVE_MANAGERS = ImmutableSet.of(
            "com.google.android.gms.dck");
    private static final Set<String> EXCLUSIVE_MANAGERS =
            ImmutableSet.of("com.google.android.gms.dck");

    private static ErrorListener sErrorListener;

@@ -89,23 +93,23 @@ public class BluetoothUtils {
    /**
     * @param context to access resources from
     * @param cachedDevice to get class from
     * @return pair containing the drawable and the description of the Bluetooth class
     *         of the device.
     * @return pair containing the drawable and the description of the Bluetooth class of the
     *     device.
     */
    public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context,
            CachedBluetoothDevice cachedDevice) {
    public static Pair<Drawable, String> getBtClassDrawableWithDescription(
            Context context, CachedBluetoothDevice cachedDevice) {
        BluetoothClass btClass = cachedDevice.getBtClass();
        if (btClass != null) {
            switch (btClass.getMajorDeviceClass()) {
                case BluetoothClass.Device.Major.COMPUTER:
                    return new Pair<>(getBluetoothDrawable(context,
                            com.android.internal.R.drawable.ic_bt_laptop),
                    return new Pair<>(
                            getBluetoothDrawable(
                                    context, com.android.internal.R.drawable.ic_bt_laptop),
                            context.getString(R.string.bluetooth_talkback_computer));

                case BluetoothClass.Device.Major.PHONE:
                    return new Pair<>(
                            getBluetoothDrawable(context,
                                    com.android.internal.R.drawable.ic_phone),
                            getBluetoothDrawable(context, com.android.internal.R.drawable.ic_phone),
                            context.getString(R.string.bluetooth_talkback_phone));

                case BluetoothClass.Device.Major.PERIPHERAL:
@@ -115,8 +119,8 @@ public class BluetoothUtils {

                case BluetoothClass.Device.Major.IMAGING:
                    return new Pair<>(
                            getBluetoothDrawable(context,
                                    com.android.internal.R.drawable.ic_settings_print),
                            getBluetoothDrawable(
                                    context, com.android.internal.R.drawable.ic_settings_print),
                            context.getString(R.string.bluetooth_talkback_imaging));

                default:
@@ -125,8 +129,9 @@ public class BluetoothUtils {
        }

        if (cachedDevice.isHearingAidDevice()) {
            return new Pair<>(getBluetoothDrawable(context,
                    com.android.internal.R.drawable.ic_bt_hearing_aid),
            return new Pair<>(
                    getBluetoothDrawable(
                            context, com.android.internal.R.drawable.ic_bt_hearing_aid),
                    context.getString(R.string.bluetooth_talkback_hearing_aids));
        }

@@ -138,7 +143,8 @@ public class BluetoothUtils {
                // The device should show hearing aid icon if it contains any hearing aid related
                // profiles
                if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
                    return new Pair<>(getBluetoothDrawable(context, profileResId),
                    return new Pair<>(
                            getBluetoothDrawable(context, profileResId),
                            context.getString(R.string.bluetooth_talkback_hearing_aids));
                }
                if (resId == 0) {
@@ -153,42 +159,40 @@ public class BluetoothUtils {
        if (btClass != null) {
            if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) {
                return new Pair<>(
                        getBluetoothDrawable(context,
                                com.android.internal.R.drawable.ic_bt_headset_hfp),
                        getBluetoothDrawable(
                                context, com.android.internal.R.drawable.ic_bt_headset_hfp),
                        context.getString(R.string.bluetooth_talkback_headset));
            }
            if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) {
                return new Pair<>(
                        getBluetoothDrawable(context,
                                com.android.internal.R.drawable.ic_bt_headphones_a2dp),
                        getBluetoothDrawable(
                                context, com.android.internal.R.drawable.ic_bt_headphones_a2dp),
                        context.getString(R.string.bluetooth_talkback_headphone));
            }
        }
        return new Pair<>(
                getBluetoothDrawable(context,
                        com.android.internal.R.drawable.ic_settings_bluetooth).mutate(),
                getBluetoothDrawable(context, com.android.internal.R.drawable.ic_settings_bluetooth)
                        .mutate(),
                context.getString(R.string.bluetooth_talkback_bluetooth));
    }

    /**
     * Get bluetooth drawable by {@code resId}
     */
    /** Get bluetooth drawable by {@code resId} */
    public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) {
        return context.getDrawable(resId);
    }

    /**
     * Get colorful bluetooth icon with description
     */
    public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(Context context,
            CachedBluetoothDevice cachedDevice) {
    /** Get colorful bluetooth icon with description */
    public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(
            Context context, CachedBluetoothDevice cachedDevice) {
        final Resources resources = context.getResources();
        final Pair<Drawable, String> pair = BluetoothUtils.getBtDrawableWithDescription(context,
                cachedDevice);
        final Pair<Drawable, String> pair =
                BluetoothUtils.getBtDrawableWithDescription(context, cachedDevice);

        if (pair.first instanceof BitmapDrawable) {
            return new Pair<>(new AdaptiveOutlineDrawable(
                    resources, ((BitmapDrawable) pair.first).getBitmap()), pair.second);
            return new Pair<>(
                    new AdaptiveOutlineDrawable(
                            resources, ((BitmapDrawable) pair.first).getBitmap()),
                    pair.second);
        }

        int hashCode;
@@ -198,15 +202,12 @@ public class BluetoothUtils {
            hashCode = cachedDevice.getAddress().hashCode();
        }

        return new Pair<>(buildBtRainbowDrawable(context,
                pair.first, hashCode), pair.second);
        return new Pair<>(buildBtRainbowDrawable(context, pair.first, hashCode), pair.second);
    }

    /**
     * Build Bluetooth device icon with rainbow
     */
    private static Drawable buildBtRainbowDrawable(Context context, Drawable drawable,
            int hashCode) {
    /** Build Bluetooth device icon with rainbow */
    private static Drawable buildBtRainbowDrawable(
            Context context, Drawable drawable, int hashCode) {
        final Resources resources = context.getResources();

        // Deal with normal headset
@@ -222,38 +223,37 @@ public class BluetoothUtils {
        return adaptiveIcon;
    }

    /**
     * Get bluetooth icon with description
     */
    public static Pair<Drawable, String> getBtDrawableWithDescription(Context context,
            CachedBluetoothDevice cachedDevice) {
        final Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription(
                context, cachedDevice);
    /** Get bluetooth icon with description */
    public static Pair<Drawable, String> getBtDrawableWithDescription(
            Context context, CachedBluetoothDevice cachedDevice) {
        final Pair<Drawable, String> pair =
                BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice);
        final BluetoothDevice bluetoothDevice = cachedDevice.getDevice();
        final int iconSize = context.getResources().getDimensionPixelSize(
                R.dimen.bt_nearby_icon_size);
        final int iconSize =
                context.getResources().getDimensionPixelSize(R.dimen.bt_nearby_icon_size);
        final Resources resources = context.getResources();

        // Deal with advanced device icon
        if (isAdvancedDetailsHeader(bluetoothDevice)) {
            final Uri iconUri = getUriMetaData(bluetoothDevice,
                    BluetoothDevice.METADATA_MAIN_ICON);
            final Uri iconUri = getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON);
            if (iconUri != null) {
                try {
                    context.getContentResolver().takePersistableUriPermission(iconUri,
                            Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    context.getContentResolver()
                            .takePersistableUriPermission(
                                    iconUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
                } catch (SecurityException e) {
                    Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e);
                }
                try {
                    final Bitmap bitmap = MediaStore.Images.Media.getBitmap(
                    final Bitmap bitmap =
                            MediaStore.Images.Media.getBitmap(
                                    context.getContentResolver(), iconUri);
                    if (bitmap != null) {
                        final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
                                iconSize, false);
                        final Bitmap resizedBitmap =
                                Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false);
                        bitmap.recycle();
                        return new Pair<>(new BitmapDrawable(resources,
                                resizedBitmap), pair.second);
                        return new Pair<>(
                                new BitmapDrawable(resources, resizedBitmap), pair.second);
                    }
                } catch (IOException e) {
                    Log.e(TAG, "Failed to get drawable for: " + iconUri, e);
@@ -280,8 +280,8 @@ public class BluetoothUtils {
            return true;
        }
        // The metadata is for Android S
        String deviceType = getStringMetaData(bluetoothDevice,
                BluetoothDevice.METADATA_DEVICE_TYPE);
        String deviceType =
                getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
        if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)
                || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH)
                || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT)
@@ -306,8 +306,8 @@ public class BluetoothUtils {
            return true;
        }
        // The metadata is for Android S
        String deviceType = getStringMetaData(bluetoothDevice,
                BluetoothDevice.METADATA_DEVICE_TYPE);
        String deviceType =
                getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
        if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) {
            Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device ");
            return true;
@@ -321,15 +321,15 @@ public class BluetoothUtils {
     * @param device Must be one of the public constants in {@link BluetoothClass.Device}
     * @return true if device class matches, false otherwise.
     */
    public static boolean isDeviceClassMatched(@NonNull BluetoothDevice bluetoothDevice,
            int device) {
    public static boolean isDeviceClassMatched(
            @NonNull BluetoothDevice bluetoothDevice, int device) {
        final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass();
        return bluetoothClass != null && bluetoothClass.getDeviceClass() == device;
    }

    private static boolean isAdvancedHeaderEnabled() {
        if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED,
                true)) {
        if (!DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, true)) {
            Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false");
            return false;
        }
@@ -345,9 +345,7 @@ public class BluetoothUtils {
        return false;
    }

    /**
     * Create an Icon pointing to a drawable.
     */
    /** Create an Icon pointing to a drawable. */
    public static IconCompat createIconWithDrawable(Drawable drawable) {
        Bitmap bitmap;
        if (drawable instanceof BitmapDrawable) {
@@ -355,19 +353,15 @@ public class BluetoothUtils {
        } else {
            final int width = drawable.getIntrinsicWidth();
            final int height = drawable.getIntrinsicHeight();
            bitmap = createBitmap(drawable,
                    width > 0 ? width : 1,
                    height > 0 ? height : 1);
            bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1);
        }
        return IconCompat.createWithBitmap(bitmap);
    }

    /**
     * Build device icon with advanced outline
     */
    /** Build device icon with advanced outline */
    public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) {
        final int iconSize = context.getResources().getDimensionPixelSize(
                R.dimen.advanced_icon_size);
        final int iconSize =
                context.getResources().getDimensionPixelSize(R.dimen.advanced_icon_size);
        final Resources resources = context.getResources();

        Bitmap bitmap = null;
@@ -376,14 +370,12 @@ public class BluetoothUtils {
        } else {
            final int width = drawable.getIntrinsicWidth();
            final int height = drawable.getIntrinsicHeight();
            bitmap = createBitmap(drawable,
                    width > 0 ? width : 1,
                    height > 0 ? height : 1);
            bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1);
        }

        if (bitmap != null) {
            final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
                    iconSize, false);
            final Bitmap resizedBitmap =
                    Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false);
            bitmap.recycle();
            return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED);
        }
@@ -391,9 +383,7 @@ public class BluetoothUtils {
        return drawable;
    }

    /**
     * Creates a drawable with specified width and height.
     */
    /** Creates a drawable with specified width and height. */
    public static Bitmap createBitmap(Drawable drawable, int width, int height) {
        final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(bitmap);
@@ -487,11 +477,8 @@ public class BluetoothUtils {
    }

    /**
     * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means:
     * 1) currently connected
     * 2) is Hearing Aid or LE Audio
     *    OR
     * 3) connected profile matches currentAudioProfile
     * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means: 1) currently
     * connected 2) is Hearing Aid or LE Audio OR 3) connected profile matches currentAudioProfile
     *
     * @param cachedDevice the CachedBluetoothDevice
     * @param audioManager audio manager to get the current audio profile
@@ -519,8 +506,11 @@ public class BluetoothUtils {
            // It would show in Available Devices group.
            if (cachedDevice.isConnectedAshaHearingAidDevice()
                    || cachedDevice.isConnectedLeAudioDevice()) {
                Log.d(TAG, "isFilterMatched() device : "
                        + cachedDevice.getName() + ", the profile is connected.");
                Log.d(
                        TAG,
                        "isFilterMatched() device : "
                                + cachedDevice.getName()
                                + ", the profile is connected.");
                return true;
            }
            // According to the current audio profile type,
@@ -541,11 +531,79 @@ public class BluetoothUtils {
        return isFilterMatched;
    }

    /** Returns if the le audio sharing is enabled. */
    public static boolean isAudioSharingEnabled() {
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        return Flags.enableLeAudioSharing()
                && adapter.isLeAudioBroadcastSourceSupported()
                        == BluetoothStatusCodes.FEATURE_SUPPORTED
                && adapter.isLeAudioBroadcastAssistantSupported()
                        == BluetoothStatusCodes.FEATURE_SUPPORTED;
    }

    /** Returns if the broadcast is on-going. */
    @WorkerThread
    public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
        if (manager == null) return false;
        LocalBluetoothLeBroadcast broadcast =
                manager.getProfileManager().getLeAudioBroadcastProfile();
        return broadcast != null && broadcast.isEnabled(null);
    }

    /**
     * Checks if the Bluetooth device is an available hearing device, which means:
     * 1) currently connected
     * 2) is Hearing Aid
     * 3) connected profile match hearing aid related profiles (e.g. ASHA, HAP)
     * Check if {@link CachedBluetoothDevice} has connected to a broadcast source.
     *
     * @param cachedDevice The cached bluetooth device to check.
     * @param localBtManager The BT manager to provide BT functions.
     * @return Whether the device has connected to a broadcast source.
     */
    @WorkerThread
    public static boolean hasConnectedBroadcastSource(
            CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
        if (localBtManager == null) {
            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
            return false;
        }
        LocalBluetoothLeBroadcastAssistant assistant =
                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
        if (assistant == null) {
            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null");
            return false;
        }
        List<BluetoothLeBroadcastReceiveState> sourceList =
                assistant.getAllSources(cachedDevice.getDevice());
        if (!sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected)) {
            Log.d(
                    TAG,
                    "Lead device has connected broadcast source, device = "
                            + cachedDevice.getDevice().getAnonymizedAddress());
            return true;
        }
        // Return true if member device is in broadcast.
        for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
            List<BluetoothLeBroadcastReceiveState> list =
                    assistant.getAllSources(device.getDevice());
            if (!list.isEmpty() && list.stream().anyMatch(BluetoothUtils::isConnected)) {
                Log.d(
                        TAG,
                        "Member device has connected broadcast source, device = "
                                + device.getDevice().getAnonymizedAddress());
                return true;
            }
        }
        return false;
    }

    /** Checks the connectivity status based on the provided broadcast receive state. */
    @WorkerThread
    public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
        return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
    }

    /**
     * Checks if the Bluetooth device is an available hearing device, which means: 1) currently
     * connected 2) is Hearing Aid 3) connected profile match hearing aid related profiles (e.g.
     * ASHA, HAP)
     *
     * @param cachedDevice the CachedBluetoothDevice
     * @return if the device is Available hearing device
@@ -553,19 +611,20 @@ public class BluetoothUtils {
    @WorkerThread
    public static boolean isAvailableHearingDevice(CachedBluetoothDevice cachedDevice) {
        if (isDeviceConnected(cachedDevice) && cachedDevice.isConnectedHearingAidDevice()) {
            Log.d(TAG, "isFilterMatched() device : "
                    + cachedDevice.getName() + ", the profile is connected.");
            Log.d(
                    TAG,
                    "isFilterMatched() device : "
                            + cachedDevice.getName()
                            + ", the profile is connected.");
            return true;
        }
        return false;
    }

    /**
     * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means:
     * 1) currently connected
     * 2) is not Hearing Aid or LE Audio
     *    AND
     * 3) connected profile does not match currentAudioProfile
     * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means: 1) currently
     * connected 2) is not Hearing Aid or LE Audio AND 3) connected profile does not match
     * currentAudioProfile
     *
     * @param cachedDevice the CachedBluetoothDevice
     * @param audioManager audio manager to get the current audio profile
@@ -675,29 +734,28 @@ public class BluetoothUtils {
    }

    /**
     * Returns the BluetoothDevice's exclusive manager
     * ({@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the
     * given set, otherwise null.
     * Returns the BluetoothDevice's exclusive manager ({@link
     * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the given
     * set, otherwise null.
     */
    @Nullable
    private static String getAllowedExclusiveManager(BluetoothDevice bluetoothDevice) {
        byte[] exclusiveManagerNameBytes = bluetoothDevice.getMetadata(
                BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
        byte[] exclusiveManagerNameBytes =
                bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
        if (exclusiveManagerNameBytes == null) {
            Log.d(TAG, "Bluetooth device " + bluetoothDevice.getName()
            Log.d(
                    TAG,
                    "Bluetooth device "
                            + bluetoothDevice.getName()
                            + " doesn't have exclusive manager");
            return null;
        }
        String exclusiveManagerName = new String(exclusiveManagerNameBytes);
        return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName
                : null;
        return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName : null;
    }

    /**
     * Checks if given package is installed
     */
    private static boolean isPackageInstalled(Context context,
            String packageName) {
    /** Checks if given package is installed */
    private static boolean isPackageInstalled(Context context, String packageName) {
        PackageManager packageManager = context.getPackageManager();
        try {
            packageManager.getPackageInfo(packageName, 0);
@@ -709,13 +767,12 @@ public class BluetoothUtils {
    }

    /**
     * A BluetoothDevice is exclusively managed if
     * 1) it has field {@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata.
     * 2) the exclusive manager app name is in the allowlist.
     * 3) the exclusive manager app is installed.
     * A BluetoothDevice is exclusively managed if 1) it has field {@link
     * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app name is
     * in the allowlist. 3) the exclusive manager app is installed.
     */
    public static boolean isExclusivelyManagedBluetoothDevice(@NonNull Context context,
            @NonNull BluetoothDevice bluetoothDevice) {
    public static boolean isExclusivelyManagedBluetoothDevice(
            @NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) {
        String exclusiveManagerName = getAllowedExclusiveManager(bluetoothDevice);
        if (exclusiveManagerName == null) {
            return false;
@@ -728,9 +785,7 @@ public class BluetoothUtils {
        }
    }

    /**
     * Return the allowlist for exclusive manager names.
     */
    /** Return the allowlist for exclusive manager names. */
    @NonNull
    public static Set<String> getExclusiveManagers() {
        return EXCLUSIVE_MANAGERS;
+42 −0

File changed.

Preview size limit exceeded, changes collapsed.