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

Commit af1ec68e authored by youngtaecha's avatar youngtaecha
Browse files

Support config update for satellite

Bug: 319609096
Test: Build
Test: Manual test with sample data files(v4, v5) and no file case
Test: atest com.android.internal.telephony.satellite.SatelliteConfigParserTest

Change-Id: I1ce08d0a8fd03fe42e321976f8c473a43faf6d21
parent fd8f4cc3
Loading
Loading
Loading
Loading
+46 −0
Original line number Diff line number Diff line

// Copyright 2024 Google LLC
//
// 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.

syntax = "proto2";
package com.android.internal.telephony.satellite;

option java_package = "com.android.internal.telephony.satellite";
option java_outer_classname = "SatelliteConfigData";

message TelephonyConfigProto {
  optional SatelliteConfigProto satellite = 1;
}

message SatelliteConfigProto {
  optional int32 version = 1;
  repeated CarrierSupportedSatelliteServicesProto carrier_supported_satellite_services = 2;
  optional SatelliteRegionProto device_satellite_region = 3;
}

message CarrierSupportedSatelliteServicesProto {
  optional int32 carrier_id = 1;
  repeated SatelliteProviderCapabilityProto supported_satellite_provider_capabilities = 2;
}

message SatelliteProviderCapabilityProto{
  optional string carrier_plmn = 1;
  repeated int32 allowed_services = 2;
}

message SatelliteRegionProto {
  optional bytes s2_cell_file = 1;
  repeated string country_codes = 2;
  optional bool is_allowed = 3;
}
 No newline at end of file
+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.telephony.configupdate;

import android.annotation.NonNull;
import android.annotation.Nullable;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public abstract class ConfigParser<T> {

    public static final int VERSION_UNKNOWN = -1;

    protected int mVersion = VERSION_UNKNOWN;

    protected T mConfig;

    /**
     * Constructs a parser from the raw data
     *
     * @param data the config data
     */
    public ConfigParser(@Nullable byte[] data) {
        parseData(data);
    }

    /**
     * Constructs a parser from the input stream
     *
     * @param input the input stream of the config
     * @return the instance of the ConfigParser
     */
    public ConfigParser(@NonNull InputStream input) throws IOException {
        parseData(input.readAllBytes());
    }

    /**
     * Constructs a parser from the input stream
     *
     * @param file the input file of the config
     * @return the instance of the ConfigParser
     */
    public ConfigParser(@NonNull File file) throws IOException {
        try (FileInputStream fis = new FileInputStream(file)) {
            parseData(fis.readAllBytes());
        }
    }

    /**
     * Get the version of the config
     *
     * @return the version of the config if it is defined, or VERSION_UNKNOWN
     */
    public int getVersion() {
        return mVersion;
    }

    /**
     * Get the config
     *
     * @return the config
     */
    public @Nullable T getConfig() {
        return mConfig;
    }

    /**
     * Get the sub config raw data by id
     *
     * @param id the identifier of the sub config
     * @return the raw data of the sub config
     */
    public @Nullable byte[] getData(String id) {
        return null;
    }

    /**
     * Parse the config data
     *
     * @param data the config data
     */
    protected abstract void parseData(@Nullable byte[] data);
}
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.telephony.configupdate;

import android.annotation.NonNull;
import android.annotation.Nullable;

import java.util.concurrent.Executor;

public interface ConfigProviderAdaptor {

    String DOMAIN_SATELLITE = "satellite";

    /**
     * Get the config from the provider
     */
    @Nullable ConfigParser getConfigParser(String domain);

    class Callback {
        /**
         * The callback when the config is changed
         * @param config the config is changed
         */
        public void onChanged(@Nullable ConfigParser config) {}
    }

    /**
     * Register the callback to monitor the config change
     * @param executor The executor to execute the callback.
     * @param callback the callback to monitor the config change
     */
    void registerCallback(@NonNull Executor executor, @NonNull Callback callback);

    /**
     * Unregister the callback
     * @param callback the callback to be unregistered
     */
    void unregisterCallback(@NonNull Callback callback);
}
+200 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.telephony.configupdate;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.satellite.SatelliteConfigParser;
import com.android.server.updates.ConfigUpdateInstallReceiver;

import libcore.io.IoUtils;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;

public class TelephonyConfigUpdateInstallReceiver extends ConfigUpdateInstallReceiver implements
        ConfigProviderAdaptor {

    private static final String TAG = "TelephonyConfigUpdateInstallReceiver";
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    protected static final String UPDATE_DIR = "/data/misc/telephonyconfig";
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    protected static final String UPDATE_CONTENT_PATH = "telephony_config.pb";
    protected static final String UPDATE_METADATA_PATH = "metadata/";
    public static final String VERSION = "version";

    private ConcurrentHashMap<Executor, Callback> mCallbackHashMap = new ConcurrentHashMap<>();
    @NonNull
    private final Object mConfigParserLock = new Object();
    @GuardedBy("mConfigParserLock")
    private ConfigParser mConfigParser;


    public static TelephonyConfigUpdateInstallReceiver sReceiverAdaptorInstance =
            new TelephonyConfigUpdateInstallReceiver();

    /**
     * @return The singleton instance of TelephonyConfigUpdateInstallReceiver
     */
    @NonNull
    public static TelephonyConfigUpdateInstallReceiver getInstance() {
        return sReceiverAdaptorInstance;
    }

    public TelephonyConfigUpdateInstallReceiver() {
        super(UPDATE_DIR, UPDATE_CONTENT_PATH, UPDATE_METADATA_PATH, VERSION);
    }

    /**
     * @return byte array type of config data protobuffer file
     */
    @Nullable
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public byte[] getCurrentContent() {
        try {
            return IoUtils.readFileAsByteArray(updateContent.getCanonicalPath());
        } catch (IOException e) {
            Slog.i(TAG, "Failed to read current content, assuming first update!");
            return null;
        }
    }

    @Override
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
    public void postInstall(Context context, Intent intent) {
        Log.d(TAG, "Telephony config is updated in file partition");
        ConfigParser updatedConfigParser = getNewConfigParser(DOMAIN_SATELLITE,
                getCurrentContent());

        if (updatedConfigParser == null) {
            Log.d(TAG, "updatedConfigParser is null");
            return;
        }

        boolean isParserChanged = false;

        synchronized (getInstance().mConfigParserLock) {
            if (getInstance().mConfigParser == null) {
                getInstance().mConfigParser = updatedConfigParser;
                isParserChanged = true;
            } else {
                int updatedVersion = updatedConfigParser.mVersion;
                int previousVersion = getInstance().mConfigParser.mVersion;
                Log.d(TAG, "previous version is " + previousVersion + " | updated version is "
                        + updatedVersion);
                if (updatedVersion > previousVersion) {
                    getInstance().mConfigParser = updatedConfigParser;
                    isParserChanged = true;
                }
            }
        }

        if (isParserChanged) {
            if (getInstance().mCallbackHashMap.keySet().isEmpty()) {
                Log.d(TAG, "mCallbackHashMap.keySet().isEmpty");
                return;
            }
            Iterator<Executor> iterator =
                    getInstance().mCallbackHashMap.keySet().iterator();
            while (iterator.hasNext()) {
                Executor executor = iterator.next();
                getInstance().mCallbackHashMap.get(executor).onChanged(
                        updatedConfigParser);
            }
        }
    }

    @Nullable
    @Override
    public ConfigParser getConfigParser(String domain) {
        Log.d(TAG, "getConfigParser");
        synchronized (getInstance().mConfigParserLock) {
            if (getInstance().mConfigParser == null) {
                Log.d(TAG, "CreateNewConfigParser with domain " + domain);
                getInstance().mConfigParser = getNewConfigParser(domain, getCurrentContent());
            }
            return getInstance().mConfigParser;
        }
    }

    @Override
    public void registerCallback(@NonNull Executor executor, @NonNull Callback callback) {
        mCallbackHashMap.put(executor, callback);
    }

    @Override
    public void unregisterCallback(@NonNull Callback callback) {
        Iterator<Executor> iterator = mCallbackHashMap.keySet().iterator();
        while (iterator.hasNext()) {
            Executor executor = iterator.next();
            if (mCallbackHashMap.get(executor) == callback) {
                mCallbackHashMap.remove(executor);
                break;
            }
        }
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public File getUpdateDir() {
        return getInstance().updateDir;
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public File getUpdateContent() {
        return getInstance().updateContent;
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public ConcurrentHashMap<Executor, Callback> getCallbackMap() {
        return getInstance().mCallbackHashMap;
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public void setCallbackMap(ConcurrentHashMap<Executor, Callback> map) {
        getInstance().mCallbackHashMap = map;
    }

    /**
     * @param data byte array type of config data
     * @return when data is null, return null otherwise return ConfigParser
     */
    @Nullable
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public ConfigParser getNewConfigParser(String domain, @Nullable byte[] data) {
        if (data == null) {
            Log.d(TAG, "content data is null");
            return null;
        }
        switch (domain) {
            case DOMAIN_SATELLITE:
                return new SatelliteConfigParser(data);
            default:
                Log.e(TAG, "DOMAIN should be specified");
                return null;
        }
    }
}
+233 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.telephony.satellite;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.satellite.nano.SatelliteConfigData;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * SatelliteConfig is utility class for satellite.
 * It is obtained through the getConfig() at the SatelliteConfigParser.
 */
public class SatelliteConfig {

    private static final String TAG = "SatelliteConfig";
    private static final String SATELLITE_DIR_NAME = "satellite";
    private static final String S2_CELL_FILE_NAME = "s2_cell_file";
    private int mVersion;
    private Map<Integer, Map<String, Set<Integer>>> mSupportedServicesPerCarrier;
    private List<String> mSatelliteRegionCountryCodes;
    private Boolean mIsSatelliteRegionAllowed;
    private Path mSatS2FilePath;
    private SatelliteConfigData.SatelliteConfigProto mConfigData;

    public SatelliteConfig(SatelliteConfigData.SatelliteConfigProto configData) {
        mConfigData = configData;
        mVersion = mConfigData.version;
        mSupportedServicesPerCarrier = getCarrierSupportedSatelliteServices();
        mSatelliteRegionCountryCodes = List.of(
                mConfigData.deviceSatelliteRegion.countryCodes);
        mIsSatelliteRegionAllowed = mConfigData.deviceSatelliteRegion.isAllowed;
        mSatS2FilePath = null;

        Log.d(TAG, "mVersion:" + mVersion + " | "
                + "mSupportedServicesPerCarrier:" + mSupportedServicesPerCarrier + " | "
                + "mSatelliteRegionCountryCodes:" + mSatelliteRegionCountryCodes + " | "
                + "mIsSatelliteRegionAllowed:" + mIsSatelliteRegionAllowed + " | "
                + "s2CellFile size:" + mConfigData.deviceSatelliteRegion.s2CellFile.length);
    }

    /**
     * @return a Map data with carrier_id, plmns and allowed_services.
     */
    private Map<Integer, Map<String, Set<Integer>>> getCarrierSupportedSatelliteServices() {
        SatelliteConfigData.CarrierSupportedSatelliteServicesProto[] satelliteServices =
                mConfigData.carrierSupportedSatelliteServices;
        Map<Integer, Map<String, Set<Integer>>> carrierToServicesMap = new HashMap<>();
        for (SatelliteConfigData.CarrierSupportedSatelliteServicesProto carrierProto :
                satelliteServices) {
            SatelliteConfigData.SatelliteProviderCapabilityProto[] satelliteCapabilities =
                    carrierProto.supportedSatelliteProviderCapabilities;
            Map<String, Set<Integer>> satelliteCapabilityMap = new HashMap<>();
            for (SatelliteConfigData.SatelliteProviderCapabilityProto capabilityProto :
                    satelliteCapabilities) {
                String carrierPlmn = capabilityProto.carrierPlmn;
                Set<Integer> allowedServices = new HashSet<>();
                for (int service : capabilityProto.allowedServices) {
                    allowedServices.add(service);
                }
                satelliteCapabilityMap.put(carrierPlmn, allowedServices);
            }
            carrierToServicesMap.put(carrierProto.carrierId, satelliteCapabilityMap);
        }
        return carrierToServicesMap;
    }

    /**
     * Get satellite plmns for carrier
     *
     * @param carrierId the carrier identifier.
     * @return Plmns corresponding to carrier identifier.
     */
    @NonNull
    public List<String> getAllSatellitePlmnsForCarrier(int carrierId) {
        if (mSupportedServicesPerCarrier != null) {
            Map<String, Set<Integer>> satelliteCapabilitiesMap = mSupportedServicesPerCarrier.get(
                    carrierId);
            if (satelliteCapabilitiesMap != null) {
                return new ArrayList<>(satelliteCapabilitiesMap.keySet());
            }
        }
        Log.d(TAG, "getAllSatellitePlmnsForCarrier : mConfigData is null or no config data");
        return new ArrayList<>();
    }

    /**
     * Get supported satellite services of all providers for a carrier.
     * The format of the return value - Key: PLMN, Value: Set of supported satellite services.
     *
     * @param carrierId the carrier identifier.
     * @return all supported satellite services for a carrier
     */
    @NonNull
    public Map<String, Set<Integer>> getSupportedSatelliteServices(int carrierId) {
        if (mSupportedServicesPerCarrier != null) {
            Map<String, Set<Integer>> satelliteCapaMap =
                    mSupportedServicesPerCarrier.get(carrierId);
            if (satelliteCapaMap != null) {
                return satelliteCapaMap;
            } else {
                Log.d(TAG, "No supported services found for carrier=" + carrierId);
            }
        } else {
            Log.d(TAG, "mSupportedServicesPerCarrier is null");
        }
        return new HashMap<>();
    }

    /**
     * @return satellite region country codes
     */
    @NonNull
    public List<String> getDeviceSatelliteCountryCodes() {
        if (mSatelliteRegionCountryCodes != null) {
            return mSatelliteRegionCountryCodes;
        }
        Log.d(TAG, "getDeviceSatelliteCountryCodes : mConfigData is null or no config data");
        return new ArrayList<>();
    }

    /**
     * @return satellite access allow value, if there is no config data then it returns null.
     */
    @Nullable
    public Boolean isSatelliteDataForAllowedRegion() {
        if (mIsSatelliteRegionAllowed == null) {
            Log.d(TAG, "getIsSatelliteRegionAllowed : mConfigData is null or no config data");
        }
        return mIsSatelliteRegionAllowed;
    }


    /**
     * @param context the Context
     * @return satellite s2_cell_file path
     */
    @Nullable
    public Path getSatelliteS2CellFile(@Nullable Context context) {
        if (context == null) {
            Log.d(TAG, "getSatelliteS2CellFile : context is null");
            return null;
        }

        if (isFileExist(mSatS2FilePath)) {
            Log.d(TAG, "File mSatS2FilePath is already exist");
            return mSatS2FilePath;
        }

        if (mConfigData != null && mConfigData.deviceSatelliteRegion != null) {
            mSatS2FilePath = copySatS2FileToPhoneDirectory(context,
                    mConfigData.deviceSatelliteRegion.s2CellFile);
            return mSatS2FilePath;
        }
        Log.d(TAG, "getSatelliteS2CellFile :"
                + "mConfigData is null or mConfigData.deviceSatelliteRegion is null");
        return null;
    }

    /**
     * @param context       the Context
     * @param byteArrayFile byte array type of protobuffer config data
     * @return the satellite_cell_file path
     */
    @Nullable
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public Path copySatS2FileToPhoneDirectory(@Nullable Context context,
            @Nullable byte[] byteArrayFile) {

        if (context == null || byteArrayFile == null) {
            Log.d(TAG, "copySatS2FileToPhoneDirectory : context or byteArrayFile are null");
            return null;
        }

        File satS2FileDir = context.getDir(SATELLITE_DIR_NAME, Context.MODE_PRIVATE);
        if (!satS2FileDir.exists()) {
            satS2FileDir.mkdirs();
        }

        Path targetSatS2FilePath = satS2FileDir.toPath().resolve(S2_CELL_FILE_NAME);
        try {
            InputStream inputStream = new ByteArrayInputStream(byteArrayFile);
            if (inputStream == null) {
                Log.d(TAG, "copySatS2FileToPhoneDirectory: Resource=" + S2_CELL_FILE_NAME
                        + " not found");
            } else {
                Files.copy(inputStream, targetSatS2FilePath, StandardCopyOption.REPLACE_EXISTING);
            }
        } catch (IOException ex) {
            Log.e(TAG, "copySatS2FileToPhoneDirectory: ex=" + ex);
        }
        return targetSatS2FilePath;
    }

    /**
     * @return {@code true} if the SatS2File is already existed and {@code false} otherwise.
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public boolean isFileExist(Path filePath) {
        return Files.exists(filePath);
    }
}
Loading