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

Commit fab85a8f authored by Jayden Kim's avatar Jayden Kim
Browse files

Add Auto Batch Scan Feature

- converts regular to batch scan with low duty cycle when screen is
  turned off
- reverts back to regular scan in turning on screen afer flushing the
  batched scan results
- adds unit test cases for auto batch scan

Bug: 261228018
Test: atest BluetoothInstrumentationTests and end-to-end device test
Test: atest FrameworkBluetoothTests
Tag: #feature
Change-Id: I654e97cc5775a2a6226f2627aa8e4e84f3e25234
parent cec70ff5
Loading
Loading
Loading
Loading
+23 −0
Original line number Diff line number Diff line
@@ -80,6 +80,7 @@ import java.util.Objects;
        public boolean isFilterScan;
        public boolean isCallbackScan;
        public boolean isBatchScan;
        public boolean isAutoBatchScan;
        public int results;
        public int scannerId;
        public int scanMode;
@@ -97,6 +98,7 @@ import java.util.Objects;
            this.isFilterScan = isFilterScan;
            this.isCallbackScan = isCallbackScan;
            this.isBatchScan = false;
            this.isAutoBatchScan = false;
            this.scanMode = scanMode;
            this.scanCallbackType = scanCallbackType;
            this.results = 0;
@@ -189,6 +191,14 @@ import java.util.Objects;
        return scan.isDowngraded;
    }

    synchronized boolean isAutoBatchScan(int scannerId) {
        LastScan scan = getScanFromScannerId(scannerId);
        if (scan == null) {
            return false;
        }
        return scan.isAutoBatchScan;
    }

    synchronized void recordScanStart(ScanSettings settings, List<ScanFilter> filters,
            boolean isFilterScan, boolean isCallbackScan, int scannerId) {
        LastScan existingScan = getScanFromScannerId(scannerId);
@@ -363,6 +373,13 @@ import java.util.Objects;
        }
    }

    synchronized void setAutoBatchScan(int scannerId, boolean isBatchScan) {
        LastScan scan = getScanFromScannerId(scannerId);
        if (scan != null) {
            scan.isAutoBatchScan = isBatchScan;
        }
    }

    synchronized boolean isScanningTooFrequently() {
        if (mLastScans.size() < mAdapterService.getScanQuotaCount()) {
            return false;
@@ -477,6 +494,8 @@ import java.util.Objects;
                return "FIRST_MATCH";
            case ScanSettings.CALLBACK_TYPE_MATCH_LOST:
                return "LOST";
            case ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH:
                return "ALL_MATCHES_AUTO_BATCH";
            default:
                return callbackType == (ScanSettings.CALLBACK_TYPE_FIRST_MATCH
                    | ScanSettings.CALLBACK_TYPE_MATCH_LOST) ? "[FIRST_MATCH | LOST]" : "UNKNOWN: "
@@ -591,6 +610,8 @@ import java.util.Objects;
                }
                if (scan.isBatchScan) {
                    sb.append("Batch Scan");
                } else if (scan.isAutoBatchScan) {
                    sb.append("Auto Batch Scan");
                } else {
                    sb.append("Regular Scan");
                }
@@ -639,6 +660,8 @@ import java.util.Objects;
                }
                if (scan.isBatchScan) {
                    sb.append("Batch Scan");
                } else if (scan.isAutoBatchScan) {
                    sb.append("Auto Batch Scan");
                } else {
                    sb.append("Regular Scan");
                }
+19 −4
Original line number Diff line number Diff line
@@ -94,7 +94,6 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;

/**
@@ -1871,7 +1870,9 @@ public class GattService extends ProfileService {
                continue;
            }

            if ((settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_ALL_MATCHES) == 0) {
            int callbackType = settings.getCallbackType();
            if (!(callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
                    || callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH)) {
                if (VDBG) {
                    Log.d(TAG, "Skipping client: CALLBACK_TYPE_ALL_MATCHES");
                }
@@ -2523,7 +2524,7 @@ public class GattService extends ProfileService {
            Log.d(TAG, "onBatchScanReports() - scannerId=" + scannerId + ", status=" + status
                    + ", reportType=" + reportType + ", numRecords=" + numRecords);
        }
        mScanManager.callbackDone(scannerId, status);

        Set<ScanResult> results = parseBatchScanResults(numRecords, reportType, recordData);
        if (reportType == ScanManager.SCAN_RESULT_TYPE_TRUNCATED) {
            // We only support single client for truncated mode.
@@ -2575,13 +2576,27 @@ public class GattService extends ProfileService {
                deliverBatchScan(client, results);
            }
        }
        mScanManager.callbackDone(scannerId, status);
    }

    private void sendBatchScanResults(ScannerMap.App app, ScanClient client,
            ArrayList<ScanResult> results) {
        try {
            if (app.callback != null) {
                if (mScanManager.isAutoBatchScanClientEnabled(client)) {
                    if (DBG) {
                        Log.d(TAG, "sendBatchScanResults() to onScanResult()" + client);
                    }
                    for (ScanResult result : results) {
                        app.appScanStats.addResult(client.scannerId);
                        app.callback.onScanResult(result);
                    }
                } else {
                    if (DBG) {
                        Log.d(TAG, "sendBatchScanResults() to onBatchScanResults()" + client);
                    }
                    app.callback.onBatchScanResults(results);
                }
            } else {
                sendResultsByPendingIntent(app.info, results,
                        ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
+101 −1
Original line number Diff line number Diff line
@@ -354,6 +354,10 @@ public class ScanManager {
        return mBluetoothAdapterProxy.isOffloadedScanFilteringSupported();
    }

    boolean isAutoBatchScanClientEnabled(ScanClient client) {
        return mScanNative.isAutoBatchScanClientEnabled(client);
    }

    // Handler class that handles BLE scan operations.
    private class ClientHandler extends Handler {

@@ -442,8 +446,16 @@ public class ScanManager {
                return;
            }

            if (!mScanNative.isExemptFromAutoBatchScanUpdate(client)) {
                if (mScreenOn) {
                    clearAutoBatchScanClient(client);
                } else {
                    setAutoBatchScanClient(client);
                }
            }

            // Begin scan operations.
            if (isBatchClient(client)) {
            if (isBatchClient(client) || isAutoBatchScanClientEnabled(client)) {
                mBatchClients.add(client);
                mScanNative.startBatchScan(client);
            } else {
@@ -501,6 +513,9 @@ public class ScanManager {
                    mScanNative.configureRegularScanParams();
                }
            } else {
                if (isAutoBatchScanClientEnabled(client)) {
                    handleFlushBatchResults(client);
                }
                mScanNative.stopBatchScan(client);
            }
            if (client.appDied) {
@@ -512,7 +527,13 @@ public class ScanManager {
        }

        void handleFlushBatchResults(ScanClient client) {
            if (DBG) {
                Log.d(TAG, "handleFlushBatchResults() " + client);
            }
            if (!mBatchClients.contains(client)) {
                if (DBG) {
                    Log.d(TAG, "There is no batch scan client to flush " + client);
                }
                return;
            }
            mScanNative.flushBatchResults(client.scannerId);
@@ -549,6 +570,7 @@ public class ScanManager {
            }
            handleSuspendScans();
            updateRegularScanClientsScreenOff();
            updateRegularScanToBatchScanClients();
        }

        void handleConnectingState() {
@@ -618,6 +640,62 @@ public class ScanManager {
            }
        }

        private void updateRegularScanToBatchScanClients() {
            boolean updatedScanParams = false;
            for (ScanClient client : mRegularScanClients) {
                if (!mScanNative.isExemptFromAutoBatchScanUpdate(client)) {
                    if (DBG) {
                        Log.d(TAG, "Updating regular scan to batch scan" + client);
                    }
                    handleStopScan(client);
                    setAutoBatchScanClient(client);
                    handleStartScan(client);
                    updatedScanParams = true;
                }
            }
            if (updatedScanParams) {
                mScanNative.configureRegularScanParams();
            }
        }

        private void updateBatchScanToRegularScanClients() {
            boolean updatedScanParams = false;
            for (ScanClient client : mBatchClients) {
                if (!mScanNative.isExemptFromAutoBatchScanUpdate(client)) {
                    if (DBG) {
                        Log.d(TAG, "Updating batch scan to regular scan" + client);
                    }
                    handleStopScan(client);
                    clearAutoBatchScanClient(client);
                    handleStartScan(client);
                    updatedScanParams = true;
                }
            }
            if (updatedScanParams) {
                mScanNative.configureRegularScanParams();
            }
        }

        private void setAutoBatchScanClient(ScanClient client) {
            if (isAutoBatchScanClientEnabled(client)) {
                return;
            }
            client.updateScanMode(ScanSettings.SCAN_MODE_SCREEN_OFF);
            if (client.stats != null) {
                client.stats.setAutoBatchScan(client.scannerId, true);
            }
        }

        private void clearAutoBatchScanClient(ScanClient client) {
            if (!isAutoBatchScanClientEnabled(client)) {
                return;
            }
            client.updateScanMode(client.scanModeApp);
            if (client.stats != null) {
                client.stats.setAutoBatchScan(client.scannerId, false);
            }
        }

        private void updateRegularScanClientsScreenOff() {
            boolean updatedScanParams = false;
            for (ScanClient client : mRegularScanClients) {
@@ -782,6 +860,7 @@ public class ScanManager {
            if (DBG) {
                Log.d(TAG, "handleScreenOn()");
            }
            updateBatchScanToRegularScanClients();
            handleResumeScans();
            updateRegularScanClientsScreenOn();
        }
@@ -1039,6 +1118,19 @@ public class ScanManager {
            return isOpportunisticScanClient(client) || isFirstMatchScanClient(client);
        }

        private boolean isExemptFromAutoBatchScanUpdate(ScanClient client) {
            return isOpportunisticScanClient(client) || !isAllMatchesAutoBatchScanClient(client);
        }

        private boolean isAutoBatchScanClientEnabled(ScanClient client) {
            return client.stats != null && client.stats.isAutoBatchScan(client.scannerId);
        }

        private boolean isAllMatchesAutoBatchScanClient(ScanClient client) {
            return client.settings.getCallbackType()
                    == ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH;
        }

        private boolean isOpportunisticScanClient(ScanClient client) {
            return client.settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
        }
@@ -1148,6 +1240,8 @@ public class ScanManager {
                        resolver,
                        Settings.Global.BLE_SCAN_BALANCED_WINDOW_MS,
                        SCAN_MODE_BALANCED_WINDOW_MS);
                case ScanSettings.SCAN_MODE_SCREEN_OFF:
                    return mAdapterService.getScreenOffLowPowerWindowMillis();
                default:
                    return Settings.Global.getInt(
                        resolver,
@@ -1164,6 +1258,8 @@ public class ScanManager {
                        resolver,
                        Settings.Global.BLE_SCAN_BALANCED_INTERVAL_MS,
                        SCAN_MODE_BALANCED_INTERVAL_MS);
                case ScanSettings.SCAN_MODE_SCREEN_OFF:
                    return mAdapterService.getScreenOffLowPowerIntervalMillis();
                default:
                    return Settings.Global.getInt(
                        resolver,
@@ -1526,6 +1622,10 @@ public class ScanManager {
                    || (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) {
                return DELIVERY_MODE_ON_FOUND_LOST;
            }
            if (isAllMatchesAutoBatchScanClient(client)) {
                return isAutoBatchScanClientEnabled(client) ? DELIVERY_MODE_BATCH
                        : DELIVERY_MODE_IMMEDIATE;
            }
            return settings.getReportDelayMillis() == 0 ? DELIVERY_MODE_IMMEDIATE
                    : DELIVERY_MODE_BATCH;
        }
+114 −8
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.bluetooth.gatt;

import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH;
import static android.bluetooth.le.ScanSettings.SCAN_MODE_OPPORTUNISTIC;
import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER;
import static android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED;
@@ -87,6 +89,7 @@ public class ScanManagerTest {
    private ScanManager mScanManager;
    private Handler mHandler;
    private CountDownLatch mLatch;
    private long mScanReportDelay;

    @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
    @Mock private AdapterService mAdapterService;
@@ -139,6 +142,8 @@ public class ScanManagerTest {

        mLatch = new CountDownLatch(1);
        assertThat(mLatch).isNotNull();

        mScanReportDelay = DEFAULT_SCAN_REPORT_DELAY_MS;
    }

    @After
@@ -174,9 +179,9 @@ public class ScanManagerTest {
    }

    private ScanClient createScanClient(int id, boolean isFiltered, int scanMode,
            boolean isBatch) {
            boolean isBatch, boolean isAutoBatch) {
        List<ScanFilter> scanFilterList = createScanFilterList(isFiltered);
        ScanSettings scanSettings = createScanSettings(scanMode, isBatch);
        ScanSettings scanSettings = createScanSettings(scanMode, isBatch, isAutoBatch);

        ScanClient client = new ScanClient(id, scanSettings, scanFilterList);
        client.stats = new AppScanStats("Test", null, null, mService);
@@ -185,7 +190,7 @@ public class ScanManagerTest {
    }

    private ScanClient createScanClient(int id, boolean isFiltered, int scanMode) {
        return createScanClient(id, isFiltered, scanMode, false);
        return createScanClient(id, isFiltered, scanMode, false, false);
    }

    private List<ScanFilter> createScanFilterList(boolean isFiltered) {
@@ -197,12 +202,17 @@ public class ScanManagerTest {
        return scanFilterList;
    }

    private ScanSettings createScanSettings(int scanMode, boolean isBatch) {
    private ScanSettings createScanSettings(int scanMode, boolean isBatch, boolean isAutoBatch) {

        ScanSettings scanSettings = null;
        if (isBatch) {
        if (isBatch && isAutoBatch) {
            int autoCallbackType = CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH;
            scanSettings = new ScanSettings.Builder().setScanMode(scanMode)
                    .setReportDelay(mScanReportDelay).setCallbackType(autoCallbackType)
                    .build();
        } else if (isBatch) {
            scanSettings = new ScanSettings.Builder().setScanMode(scanMode)
                    .setReportDelay(DEFAULT_SCAN_REPORT_DELAY_MS).build();
                    .setReportDelay(mScanReportDelay).build();
        } else {
            scanSettings = new ScanSettings.Builder().setScanMode(scanMode).build();
        }
@@ -806,6 +816,7 @@ public class ScanManagerTest {
        // Set filtered and batch scan flag
        final boolean isFiltered = false;
        final boolean isBatch = true;
        final boolean isAutoBatch = false;
        // Set scan mode map {original scan mode (ScanMode) : expected scan mode (expectedScanMode)}
        SparseIntArray scanModeMap = new SparseIntArray();
        scanModeMap.put(SCAN_MODE_LOW_POWER, SCAN_MODE_LOW_POWER);
@@ -822,7 +833,7 @@ public class ScanManagerTest {
            // Turn off screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(false));
            // Create scan client
            ScanClient client = createScanClient(i, isFiltered, ScanMode, isBatch);
            ScanClient client = createScanClient(i, isFiltered, ScanMode, isBatch, isAutoBatch);
            // Start scan
            sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isFalse();
@@ -842,6 +853,7 @@ public class ScanManagerTest {
        // Set filtered and batch scan flag
        final boolean isFiltered = true;
        final boolean isBatch = true;
        final boolean isAutoBatch = false;
        // Set scan mode map {original scan mode (ScanMode) : expected scan mode (expectedScanMode)}
        SparseIntArray scanModeMap = new SparseIntArray();
        scanModeMap.put(SCAN_MODE_LOW_POWER, SCAN_MODE_LOW_POWER);
@@ -858,7 +870,94 @@ public class ScanManagerTest {
            // Turn off screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(false));
            // Create scan client
            ScanClient client = createScanClient(i, isFiltered, ScanMode, isBatch);
            ScanClient client = createScanClient(i, isFiltered, ScanMode, isBatch, isAutoBatch);
            // Start scan
            sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanQueue().contains(client)).isTrue();
            assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
            // Turn on screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(true));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanQueue().contains(client)).isTrue();
            assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
        }
    }

    @Test
    public void testUnfilteredAutoBatchScan() {
        // Set filtered and batch scan flag
        final boolean isFiltered = false;
        final boolean isBatch = true;
        final boolean isAutoBatch = true;
        // Set report delay for auto batch scan callback type
        mScanReportDelay = ScanSettings.AUTO_BATCH_MIN_REPORT_DELAY_MILLIS;
        // Set scan mode map {original scan mode (ScanMode) : expected scan mode (expectedScanMode)}
        SparseIntArray scanModeMap = new SparseIntArray();
        scanModeMap.put(SCAN_MODE_LOW_POWER, SCAN_MODE_SCREEN_OFF);
        scanModeMap.put(SCAN_MODE_BALANCED, SCAN_MODE_SCREEN_OFF);
        scanModeMap.put(SCAN_MODE_LOW_LATENCY, SCAN_MODE_SCREEN_OFF);
        scanModeMap.put(SCAN_MODE_AMBIENT_DISCOVERY, SCAN_MODE_SCREEN_OFF);

        for (int i = 0; i < scanModeMap.size(); i++) {
            int ScanMode = scanModeMap.keyAt(i);
            int expectedScanMode = scanModeMap.get(ScanMode);
            Log.d(TAG, "ScanMode: " + String.valueOf(ScanMode)
                    + " expectedScanMode: " + String.valueOf(expectedScanMode));

            // Turn off screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(false));
            // Create scan client
            ScanClient client = createScanClient(i, isFiltered, ScanMode, isBatch, isAutoBatch);
            // Start scan
            sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isTrue();
            assertThat(mScanManager.getBatchScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanParams()).isNull();
            // Turn on screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(true));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isTrue();
            assertThat(client.settings.getScanMode()).isEqualTo(ScanMode);
            assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanParams()).isNull();
            // Turn off screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(false));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isTrue();
            assertThat(mScanManager.getBatchScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanParams()).isNull();
        }
    }

    @Test
    public void testFilteredAutoBatchScan() {
        // Set filtered and batch scan flag
        final boolean isFiltered = true;
        final boolean isBatch = true;
        final boolean isAutoBatch = true;
        // Set report delay for auto batch scan callback type
        mScanReportDelay = ScanSettings.AUTO_BATCH_MIN_REPORT_DELAY_MILLIS;
        // Set scan mode map {original scan mode (ScanMode) : expected scan mode (expectedScanMode)}
        SparseIntArray scanModeMap = new SparseIntArray();
        scanModeMap.put(SCAN_MODE_LOW_POWER, SCAN_MODE_SCREEN_OFF);
        scanModeMap.put(SCAN_MODE_BALANCED, SCAN_MODE_SCREEN_OFF);
        scanModeMap.put(SCAN_MODE_LOW_LATENCY, SCAN_MODE_SCREEN_OFF);
        scanModeMap.put(SCAN_MODE_AMBIENT_DISCOVERY, SCAN_MODE_SCREEN_OFF);

        for (int i = 0; i < scanModeMap.size(); i++) {
            int ScanMode = scanModeMap.keyAt(i);
            int expectedScanMode = scanModeMap.get(ScanMode);
            Log.d(TAG, "ScanMode: " + String.valueOf(ScanMode)
                    + " expectedScanMode: " + String.valueOf(expectedScanMode));

            // Turn off screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(false));
            // Create scan client
            ScanClient client = createScanClient(i, isFiltered, ScanMode, isBatch, isAutoBatch);
            // Start scan
            sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isFalse();
@@ -867,6 +966,13 @@ public class ScanManagerTest {
            assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
            // Turn on screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(true));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isTrue();
            assertThat(client.settings.getScanMode()).isEqualTo(ScanMode);
            assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanParams()).isNull();
            // Turn off screen
            sendMessageWaitForProcessed(createScreenOnOffMessage(false));
            assertThat(mScanManager.getRegularScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse();
            assertThat(mScanManager.getBatchScanQueue().contains(client)).isTrue();
+2 −0
Original line number Diff line number Diff line
@@ -1396,7 +1396,9 @@ package android.bluetooth.le {
    method public int getScanMode();
    method public int getScanResultType();
    method public void writeToParcel(android.os.Parcel, int);
    field public static final long AUTO_BATCH_MIN_REPORT_DELAY_MILLIS = 600000L; // 0x927c0L
    field public static final int CALLBACK_TYPE_ALL_MATCHES = 1; // 0x1
    field public static final int CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH = 8; // 0x8
    field public static final int CALLBACK_TYPE_FIRST_MATCH = 2; // 0x2
    field public static final int CALLBACK_TYPE_MATCH_LOST = 4; // 0x4
    field @NonNull public static final android.os.Parcelable.Creator<android.bluetooth.le.ScanSettings> CREATOR;
Loading