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

Commit 54fee7e2 authored by Anil Admal's avatar Anil Admal Committed by Android (Google) Code Review
Browse files

Merge "Non-framework location access visibility and control (framework2)"

parents cdfa6ed8 94ec76ab
Loading
Loading
Loading
Loading
+30 −0
Original line number Original line Diff line number Diff line
@@ -29,7 +29,10 @@ import libcore.io.IoUtils;
import java.io.File;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Properties;
@@ -66,6 +69,7 @@ class GnssConfiguration {
            "USE_EMERGENCY_PDN_FOR_EMERGENCY_SUPL";
            "USE_EMERGENCY_PDN_FOR_EMERGENCY_SUPL";
    private static final String CONFIG_GPS_LOCK = "GPS_LOCK";
    private static final String CONFIG_GPS_LOCK = "GPS_LOCK";
    private static final String CONFIG_ES_EXTENSION_SEC = "ES_EXTENSION_SEC";
    private static final String CONFIG_ES_EXTENSION_SEC = "ES_EXTENSION_SEC";
    public static final String CONFIG_NFW_PROXY_APPS = "NFW_PROXY_APPS";


    // Limit on NI emergency mode time extension after emergency sessions ends
    // Limit on NI emergency mode time extension after emergency sessions ends
    private static final int MAX_EMERGENCY_MODE_EXTENSION_SECONDS = 300;  // 5 minute maximum
    private static final int MAX_EMERGENCY_MODE_EXTENSION_SECONDS = 300;  // 5 minute maximum
@@ -170,6 +174,32 @@ class GnssConfiguration {
        return mProperties.getProperty(CONFIG_LPP_PROFILE);
        return mProperties.getProperty(CONFIG_LPP_PROFILE);
    }
    }


    /**
     * Returns the list of proxy apps from the value of config parameter NFW_PROXY_APPS or
     * {@Collections.EMPTY_LIST} if no value is provided.
     */
    List<String> getProxyApps() {
        // Space separated list of Android proxy app package names.
        String proxyAppsStr = mProperties.getProperty(CONFIG_NFW_PROXY_APPS);
        if (TextUtils.isEmpty(proxyAppsStr)) {
            return Collections.EMPTY_LIST;
        }

        String[] proxyAppsArray = proxyAppsStr.trim().split("\\s+");
        if (proxyAppsArray.length == 0) {
            return Collections.EMPTY_LIST;
        }

        // TODO(b/122856486): Validate proxy app names so that a system app or some popular app
        //                    with location permission is not specified as a proxy app.
        ArrayList proxyApps = new ArrayList(proxyAppsArray.length);
        for (String proxyApp : proxyAppsArray) {
            proxyApps.add(proxyApp);
        }

        return proxyApps;
    }

    /**
    /**
     * Updates the GNSS HAL satellite blacklist.
     * Updates the GNSS HAL satellite blacklist.
     */
     */
+33 −0
Original line number Original line Diff line number Diff line
@@ -372,6 +372,7 @@ public class GnssLocationProvider extends AbstractLocationProvider implements
    private boolean mSuplEsEnabled = false;
    private boolean mSuplEsEnabled = false;


    private final Context mContext;
    private final Context mContext;
    private final Looper mLooper;
    private final LocationExtras mLocationExtras = new LocationExtras();
    private final LocationExtras mLocationExtras = new LocationExtras();
    private final GnssStatusListenerHelper mGnssStatusListenerHelper;
    private final GnssStatusListenerHelper mGnssStatusListenerHelper;
    private final GnssSatelliteBlacklistHelper mGnssSatelliteBlacklistHelper;
    private final GnssSatelliteBlacklistHelper mGnssSatelliteBlacklistHelper;
@@ -382,6 +383,7 @@ public class GnssLocationProvider extends AbstractLocationProvider implements
    private final NtpTimeHelper mNtpTimeHelper;
    private final NtpTimeHelper mNtpTimeHelper;
    private final GnssBatchingProvider mGnssBatchingProvider;
    private final GnssBatchingProvider mGnssBatchingProvider;
    private final GnssGeofenceProvider mGnssGeofenceProvider;
    private final GnssGeofenceProvider mGnssGeofenceProvider;
    private GnssVisibilityControl mGnssVisibilityControl;


    // Handler for processing events
    // Handler for processing events
    private Handler mHandler;
    private Handler mHandler;
@@ -556,6 +558,9 @@ public class GnssLocationProvider extends AbstractLocationProvider implements
        mC2KServerPort = mGnssConfiguration.getC2KPort(TCP_MIN_PORT);
        mC2KServerPort = mGnssConfiguration.getC2KPort(TCP_MIN_PORT);
        mNIHandler.setEmergencyExtensionSeconds(mGnssConfiguration.getEsExtensionSec());
        mNIHandler.setEmergencyExtensionSeconds(mGnssConfiguration.getEsExtensionSec());
        mSuplEsEnabled = mGnssConfiguration.getSuplEs(0) == 1;
        mSuplEsEnabled = mGnssConfiguration.getSuplEs(0) == 1;
        if (mGnssVisibilityControl != null) {
            mGnssVisibilityControl.updateProxyApps(mGnssConfiguration.getProxyApps());
        }
    }
    }


    public GnssLocationProvider(Context context, LocationProviderManager locationProviderManager,
    public GnssLocationProvider(Context context, LocationProviderManager locationProviderManager,
@@ -563,6 +568,7 @@ public class GnssLocationProvider extends AbstractLocationProvider implements
        super(locationProviderManager);
        super(locationProviderManager);


        mContext = context;
        mContext = context;
        mLooper = looper;


        // Create a wake lock
        // Create a wake lock
        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
@@ -1869,6 +1875,27 @@ public class GnssLocationProvider extends AbstractLocationProvider implements
        }
        }
    }
    }


    // Implements method nfwNotifyCb() in IGnssVisibilityControlCallback.hal.
    @NativeEntryPoint
    private void reportNfwNotification(String proxyAppPackageName, byte protocolStack,
            String otherProtocolStackName, byte requestor, String requestorId, byte responseType,
            boolean inEmergencyMode, boolean isCachedLocation) {
        if (mGnssVisibilityControl == null) {
            Log.e(TAG, "reportNfwNotification: mGnssVisibilityControl is not initialized.");
            return;
        }

        mGnssVisibilityControl.reportNfwNotification(proxyAppPackageName, protocolStack,
                otherProtocolStackName, requestor, requestorId, responseType, inEmergencyMode,
                isCachedLocation);
    }

    // Implements method isInEmergencySession() in IGnssVisibilityControlCallback.hal.
    @NativeEntryPoint
    boolean isInEmergencySession() {
        return mNIHandler.getInEmergency();
    }

    private void sendMessage(int message, int arg, Object obj) {
    private void sendMessage(int message, int arg, Object obj) {
        // hold a wake lock until this message is delivered
        // hold a wake lock until this message is delivered
        // note that this assumes the message will not be removed from the queue before
        // note that this assumes the message will not be removed from the queue before
@@ -1957,6 +1984,10 @@ public class GnssLocationProvider extends AbstractLocationProvider implements
                native_cleanup();
                native_cleanup();
            }
            }


            if (native_is_gnss_visibility_control_supported()) {
                mGnssVisibilityControl = new GnssVisibilityControl(mContext, mLooper);
            }

            // load default GPS configuration
            // load default GPS configuration
            // (this configuration might change in the future based on SIM changes)
            // (this configuration might change in the future based on SIM changes)
            reloadGpsProperties();
            reloadGpsProperties();
@@ -2114,6 +2145,8 @@ public class GnssLocationProvider extends AbstractLocationProvider implements


    private static native boolean native_is_supported();
    private static native boolean native_is_supported();


    private static native boolean native_is_gnss_visibility_control_supported();

    private static native void native_init_once();
    private static native void native_init_once();


    private native boolean native_init();
    private native boolean native_init();
+362 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2019 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.server.location;

import android.annotation.SuppressLint;
import android.app.AppOpsManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Handles GNSS non-framework location access user visibility and control.
 *
 * The state of the GnssVisibilityControl object must be accessed/modified through the Handler
 * thread only.
 */
class GnssVisibilityControl {
    private static final String TAG = "GnssVisibilityControl";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    // Constants related to non-framework (NFW) location access permission proxy apps.
    private static final String NFW_PROXY_APP_PKG_ACTIVITY_NAME_SUFFIX =
            ".NonFrameworkLocationAccessActivity";
    private static final String NFW_INTENT_ACTION_NFW_LOCATION_ACCESS_SUFFIX =
            ".intent.action.NON_FRAMEWORK_LOCATION_ACCESS";
    private static final String NFW_INTENT_TYPE = "text/plain";

    private static final String LOCATION_PERMISSION_NAME =
            "android.permission.ACCESS_FINE_LOCATION";

    // Wakelocks
    private static final String WAKELOCK_KEY = TAG;
    private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
    private final PowerManager.WakeLock mWakeLock;

    private final AppOpsManager mAppOps;
    private final PackageManager mPackageManager;

    private final Handler mHandler;
    private final Context mContext;

    // Number of non-framework location access proxy apps is expected to be small (< 5).
    private static final int HASH_MAP_INITIAL_CAPACITY_PROXY_APP_TO_LOCATION_PERMISSIONS = 7;
    private HashMap<String, Boolean> mProxyAppToLocationPermissions = new HashMap<>(
            HASH_MAP_INITIAL_CAPACITY_PROXY_APP_TO_LOCATION_PERMISSIONS);

    private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener =
            uid -> postEvent(() -> handlePermissionsChanged(uid));

    GnssVisibilityControl(Context context, Looper looper) {
        mContext = context;
        PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
        mHandler = new Handler(looper);
        mAppOps = mContext.getSystemService(AppOpsManager.class);
        mPackageManager = mContext.getPackageManager();
        // TODO(b/122855984): Handle global location settings on/off.
        // TODO(b/122856189): Handle roaming case.
    }

    void updateProxyApps(List<String> nfwLocationAccessProxyApps) {
        // NOTE: This class doesn't explicitly register and listen for SIM_STATE_CHANGED event
        //       but rather piggy backs on the GnssLocationProvider SIM_STATE_CHANGED handling
        //       so that the order of processing is preserved. GnssLocationProvider should
        //       first load the new config parameters for the new SIM and then call this method.
        postEvent(() -> handleSubscriptionOrSimChanged(nfwLocationAccessProxyApps));
    }

    void reportNfwNotification(String proxyAppPackageName, byte protocolStack,
            String otherProtocolStackName, byte requestor, String requestorId, byte responseType,
            boolean inEmergencyMode, boolean isCachedLocation) {
        postEvent(() -> handleNfwNotification(
                new NfwNotification(proxyAppPackageName, protocolStack, otherProtocolStackName,
                        requestor, requestorId, responseType, inEmergencyMode, isCachedLocation)));
    }

    private void handleSubscriptionOrSimChanged(List<String> nfwLocationAccessProxyApps) {
        if (nfwLocationAccessProxyApps.isEmpty()) {
            // Stop listening for app permission changes. Clear the app list in GNSS HAL.
            if (!mProxyAppToLocationPermissions.isEmpty()) {
                mPackageManager.removeOnPermissionsChangeListener(mOnPermissionsChangedListener);
                mProxyAppToLocationPermissions.clear();
                updateNfwLocationAccessProxyAppsInGnssHal();
            }
            return;
        }

        if (mProxyAppToLocationPermissions.isEmpty()) {
            mPackageManager.addOnPermissionsChangeListener(mOnPermissionsChangedListener);
        } else {
            mProxyAppToLocationPermissions.clear();
        }

        for (String proxApp : nfwLocationAccessProxyApps) {
            mProxyAppToLocationPermissions.put(proxApp, hasLocationPermission(proxApp));
        }

        updateNfwLocationAccessProxyAppsInGnssHal();
    }

    // Represents NfwNotification structure in IGnssVisibilityControlCallback.hal
    private static class NfwNotification {
        private static final String KEY_PROTOCOL_STACK = "ProtocolStack";
        private static final String KEY_OTHER_PROTOCOL_STACK_NAME = "OtherProtocolStackName";
        private static final String KEY_REQUESTOR = "Requestor";
        private static final String KEY_REQUESTOR_ID = "RequestorId";
        private static final String KEY_RESPONSE_TYPE = "ResponseType";
        private static final String KEY_IN_EMERGENCY_MODE = "InEmergencyMode";
        private static final String KEY_IS_CACHED_LOCATION = "IsCachedLocation";

        // This must match with NfwResponseType enum in IGnssVisibilityControlCallback.hal.
        private static final byte NFW_RESPONSE_TYPE_REJECTED = 0;
        private static final byte NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED = 1;
        private static final byte NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED = 2;

        private final String mProxyAppPackageName;
        private final byte mProtocolStack;
        private final String mOtherProtocolStackName;
        private final byte mRequestor;
        private final String mRequestorId;
        private final byte mResponseType;
        private final boolean mInEmergencyMode;
        private final boolean mIsCachedLocation;

        NfwNotification(String proxyAppPackageName, byte protocolStack,
                String otherProtocolStackName, byte requestor, String requestorId,
                byte responseType, boolean inEmergencyMode, boolean isCachedLocation) {
            mProxyAppPackageName = proxyAppPackageName;
            mProtocolStack = protocolStack;
            mOtherProtocolStackName = otherProtocolStackName;
            mRequestor = requestor;
            mRequestorId = requestorId;
            mResponseType = responseType;
            mInEmergencyMode = inEmergencyMode;
            mIsCachedLocation = isCachedLocation;
        }

        void copyFieldsToIntent(Intent intent) {
            intent.putExtra(KEY_PROTOCOL_STACK, mProtocolStack);
            if (!TextUtils.isEmpty(mOtherProtocolStackName)) {
                intent.putExtra(KEY_OTHER_PROTOCOL_STACK_NAME, mOtherProtocolStackName);
            }
            intent.putExtra(KEY_REQUESTOR, mRequestor);
            if (!TextUtils.isEmpty(mRequestorId)) {
                intent.putExtra(KEY_REQUESTOR_ID, mRequestorId);
            }
            intent.putExtra(KEY_RESPONSE_TYPE, mResponseType);
            intent.putExtra(KEY_IN_EMERGENCY_MODE, mInEmergencyMode);
            if (mResponseType == NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED) {
                intent.putExtra(KEY_IS_CACHED_LOCATION, mIsCachedLocation);
            }
        }

        @SuppressLint("DefaultLocale")
        public String toString() {
            return String.format(
                    "[Notification] proxyAppPackageName: %s, protocolStack: %d"
                            + ", otherProtocolStackName: %s, requestor: %d, requestorId: %s"
                            + ", responseType: %d, inEmergencyMode: %b, isCachedLocation: %b",
                    mProxyAppPackageName, mProtocolStack, mOtherProtocolStackName,
                    mRequestor, mRequestorId, mResponseType, mInEmergencyMode, mIsCachedLocation);
        }

        String getResponseTypeAsString() {
            switch (mResponseType) {
                case NFW_RESPONSE_TYPE_REJECTED:
                    return "REJECTED";
                case NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED:
                    return "ACCEPTED_NO_LOCATION_PROVIDED";
                case NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED:
                    return "ACCEPTED_LOCATION_PROVIDED";
                default:
                    return "<Unknown>";
            }
        }
    }

    private void handlePermissionsChanged(int uid) {
        if (mProxyAppToLocationPermissions.isEmpty()) {
            return;
        }

        for (Map.Entry<String, Boolean> entry : mProxyAppToLocationPermissions.entrySet()) {
            // Cannot cache uid since the application could be uninstalled and reinstalled.
            final String proxyApp = entry.getKey();
            final Integer nfwProxyAppUid = getApplicationUid(proxyApp);
            if (nfwProxyAppUid == null || nfwProxyAppUid != uid) {
                continue;
            }

            final boolean isLocationPermissionEnabled = hasLocationPermission(proxyApp);
            if (isLocationPermissionEnabled != entry.getValue()) {
                Log.i(TAG, "Location permission setting is changed to "
                        + (isLocationPermissionEnabled ? "enabled" : "disabled")
                        + " for non-framework location access proxy app "
                        + proxyApp);
                entry.setValue(isLocationPermissionEnabled);
                updateNfwLocationAccessProxyAppsInGnssHal();
                return;
            }
        }
    }

    private Integer getApplicationUid(String pkgName) {
        try {
            return mPackageManager.getApplicationInfo(pkgName, 0).uid;
        } catch (PackageManager.NameNotFoundException e) {
            if (DEBUG) {
                Log.d(TAG, "Non-framework location access proxy app "
                        + pkgName + " is not found.");
            }
            return null;
        }
    }

    private boolean hasLocationPermission(String pkgName) {
        return mPackageManager.checkPermission(LOCATION_PERMISSION_NAME, pkgName)
                == PackageManager.PERMISSION_GRANTED;
    }

    private void updateNfwLocationAccessProxyAppsInGnssHal() {
        // Get a count of proxy apps with location permission enabled to array creation size.
        int countLocationPermissionEnabledProxyApps = 0;
        for (Boolean hasLocationPermissionEnabled : mProxyAppToLocationPermissions.values()) {
            if (hasLocationPermissionEnabled) {
                ++countLocationPermissionEnabledProxyApps;
            }
        }

        int i = 0;
        String[] locationPermissionEnabledProxyApps =
                new String[countLocationPermissionEnabledProxyApps];
        for (Map.Entry<String, Boolean> entry : mProxyAppToLocationPermissions.entrySet()) {
            final String proxyApp = entry.getKey();
            final boolean hasLocationPermissionEnabled = entry.getValue();
            if (hasLocationPermissionEnabled) {
                locationPermissionEnabledProxyApps[i++] = proxyApp;
            }
        }

        String proxyAppsStr = Arrays.toString(locationPermissionEnabledProxyApps);
        Log.i(TAG, "Updating non-framework location access proxy apps in the GNSS HAL to: "
                + proxyAppsStr);
        boolean result = native_enable_nfw_location_access(locationPermissionEnabledProxyApps);
        if (!result) {
            Log.e(TAG, "Failed to update non-framework location access proxy apps in the"
                    + " GNSS HAL to: " + proxyAppsStr);
        }
    }

    private void handleNfwNotification(NfwNotification nfwNotification) {
        if (DEBUG) Log.d(TAG, nfwNotification.toString());

        final String proxyAppPackageName = nfwNotification.mProxyAppPackageName;
        if (TextUtils.isEmpty(proxyAppPackageName)) {
            Log.e(TAG, "ProxyAppPackageName field is not set. Not sending intent to proxy app for "
                    + nfwNotification);
            return;
        }

        Boolean isLocationPermissionEnabled = mProxyAppToLocationPermissions.get(
                proxyAppPackageName);
        if (isLocationPermissionEnabled == null) {
            // App is not in the configured list.
            Log.e(TAG, "Could not find proxy app with name: " + proxyAppPackageName + " in the "
                    + "value specified for config parameter: "
                    + GnssConfiguration.CONFIG_NFW_PROXY_APPS + ". Not sending intent to proxy app"
                    + " for " + nfwNotification);
            return;
        }

        // Send intent to non-framework location proxy app with notification information.
        final Intent intent = new Intent(
                proxyAppPackageName + NFW_INTENT_ACTION_NFW_LOCATION_ACCESS_SUFFIX);
        final String proxAppActivityName =
                proxyAppPackageName + NFW_PROXY_APP_PKG_ACTIVITY_NAME_SUFFIX;
        intent.setClassName(proxyAppPackageName, proxAppActivityName);
        intent.setType(NFW_INTENT_TYPE);
        nfwNotification.copyFieldsToIntent(intent);

        // Check if the proxy app is still installed.
        final Integer clsAppUid = getApplicationUid(proxyAppPackageName);
        if (clsAppUid == null || intent.resolveActivity(mPackageManager) == null) {
            Log.i(TAG, "Proxy application " + proxyAppPackageName + " and/or activity "
                    + proxAppActivityName + " is not found. Not sending"
                    + " intent to proxy app for " + nfwNotification);
            return;
        }

        // Display location icon attributed to this proxy app.
        mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, clsAppUid, proxyAppPackageName);

        // Log proxy app permission mismatch between framework and GNSS HAL.
        boolean isLocationRequestAccepted =
                nfwNotification.mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED;
        if (isLocationPermissionEnabled != isLocationRequestAccepted) {
            Log.w(TAG, "Permission mismatch. Framework proxy app " + proxyAppPackageName
                    + " location permission is set to " + isLocationPermissionEnabled
                    + " but GNSS non-framework location access response type is "
                    + nfwNotification.getResponseTypeAsString() + " for " + nfwNotification);
        }

        // Notify proxy app.
        try {
            if (DEBUG) {
                Log.d(TAG, "Sending non-framework location access notification intent: " + intent);
            }
            mContext.startActivityAsUser(intent, UserHandle.getUserHandleForUid(clsAppUid));
        } catch (ActivityNotFoundException e) {
            Log.w(TAG, "Activity not found. Failed to send non-framework location access"
                    + " notification intent to proxy app activity: " + proxAppActivityName);
        }
    }

    private void postEvent(Runnable event) {
        // Hold a wake lock until this message is delivered.
        // Note that this assumes the message will not be removed from the queue before
        // it is handled (otherwise the wake lock would be leaked).
        mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
        if (!mHandler.post(runEventAndReleaseWakeLock(event))) {
            mWakeLock.release();
        }
    }

    private Runnable runEventAndReleaseWakeLock(Runnable event) {
        return () -> {
            try {
                event.run();
            } finally {
                mWakeLock.release();
            }
        };
    }

    private native boolean native_enable_nfw_location_access(String[] proxyApps);
}
+1 −0
Original line number Original line Diff line number Diff line
@@ -107,6 +107,7 @@ cc_defaults {
        "android.hardware.gnss@1.0",
        "android.hardware.gnss@1.0",
        "android.hardware.gnss@1.1",
        "android.hardware.gnss@1.1",
        "android.hardware.gnss@2.0",
        "android.hardware.gnss@2.0",
        "android.hardware.gnss.visibility_control@1.0",
        "android.hardware.input.classifier@1.0",
        "android.hardware.input.classifier@1.0",
        "android.hardware.ir@1.0",
        "android.hardware.ir@1.0",
        "android.hardware.light@2.0",
        "android.hardware.light@2.0",
+123 −8

File changed.

Preview size limit exceeded, changes collapsed.