Implements satellite state change listener APIs
This change implemements the new satellite state change listener APIs by
the help of TelephonyRegistryManager.
Bug: 357638490
Test: atest FrameworkTelephonyTests SatelliteManagerTest
Flag: com.android.internal.telephony.flags.satellite_state_change_listener
Change-Id: I5b76aa8756bc35ccd94b332de708601f11ea6b7f
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 4d50a45..1dab2cf 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -47,6 +47,8 @@
import android.telephony.ims.ImsCallSession;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.MediaQualityStatus;
+import android.telephony.satellite.SatelliteStateChangeListener;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -55,12 +57,14 @@
import com.android.internal.telephony.ICarrierConfigChangeListener;
import com.android.internal.telephony.ICarrierPrivilegesCallback;
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
+import com.android.internal.telephony.ISatelliteStateChangeListener;
import com.android.internal.telephony.ITelephonyRegistry;
import com.android.server.telecom.flags.Flags;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
@@ -1482,6 +1486,111 @@
pkgName, attributionTag, callback, new int[0], notifyNow);
}
+ @NonNull
+ @GuardedBy("sSatelliteStateChangeListeners")
+ private static final Map<SatelliteStateChangeListener,
+ WeakReference<SatelliteStateChangeListenerWrapper>>
+ sSatelliteStateChangeListeners = new ArrayMap<>();
+
+ /**
+ * Register a {@link SatelliteStateChangeListener} to receive notification when Satellite state
+ * has changed.
+ *
+ * @param executor The {@link Executor} where the {@code listener} will be invoked
+ * @param listener The listener to monitor the satellite state change
+ * @hide
+ */
+ public void addSatelliteStateChangeListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull SatelliteStateChangeListener listener) {
+ if (listener == null || executor == null) {
+ throw new IllegalArgumentException("Listener and executor must be non-null");
+ }
+
+ synchronized (sSatelliteStateChangeListeners) {
+ WeakReference<SatelliteStateChangeListenerWrapper> existing =
+ sSatelliteStateChangeListeners.get(listener);
+ if (existing != null && existing.get() != null) {
+ Log.d(TAG, "addSatelliteStateChangeListener: listener already registered");
+ return;
+ }
+ SatelliteStateChangeListenerWrapper wrapper =
+ new SatelliteStateChangeListenerWrapper(executor, listener);
+ try {
+ sRegistry.addSatelliteStateChangeListener(
+ wrapper,
+ mContext.getOpPackageName(),
+ mContext.getAttributionTag());
+ sSatelliteStateChangeListeners.put(listener, new WeakReference<>(wrapper));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregister a {@link SatelliteStateChangeListener} to stop receiving notification when
+ * satellite state has changed.
+ *
+ * @param listener The listener previously registered with addSatelliteStateChangeListener.
+ * @hide
+ */
+ public void removeSatelliteStateChangeListener(@NonNull SatelliteStateChangeListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must be non-null");
+ }
+
+ synchronized (sSatelliteStateChangeListeners) {
+ WeakReference<SatelliteStateChangeListenerWrapper> ref =
+ sSatelliteStateChangeListeners.get(listener);
+ if (ref == null) return;
+ SatelliteStateChangeListenerWrapper wrapper = ref.get();
+ if (wrapper == null) return;
+ try {
+ sRegistry.removeSatelliteStateChangeListener(wrapper, mContext.getOpPackageName());
+ sSatelliteStateChangeListeners.remove(listener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Notify the registrants that the satellite state has changed.
+ *
+ * @param isEnabled True if the satellite modem is enabled, false otherwise
+ * @hide
+ */
+ public void notifySatelliteStateChanged(boolean isEnabled) {
+ try {
+ sRegistry.notifySatelliteStateChanged(isEnabled);
+ } catch (RemoteException ex) {
+ // system process is dead
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ private static class SatelliteStateChangeListenerWrapper extends
+ ISatelliteStateChangeListener.Stub implements ListenerExecutor {
+ @NonNull private final WeakReference<SatelliteStateChangeListener> mListener;
+ @NonNull private final Executor mExecutor;
+
+ SatelliteStateChangeListenerWrapper(@NonNull Executor executor,
+ @NonNull SatelliteStateChangeListener listener) {
+ mExecutor = executor;
+ mListener = new WeakReference<>(listener);
+ }
+
+ @Override
+ public void onSatelliteEnabledStateChanged(boolean isEnabled) {
+ Binder.withCleanCallingIdentity(
+ () ->
+ executeSafely(
+ mExecutor,
+ mListener::get,
+ sscl -> sscl.onEnabledStateChanged(isEnabled)));
+ }
+ }
+
private static class CarrierPrivilegesCallbackWrapper extends ICarrierPrivilegesCallback.Stub
implements ListenerExecutor {
@NonNull private final WeakReference<CarrierPrivilegesCallback> mCallback;
diff --git a/core/java/com/android/internal/telephony/ISatelliteStateChangeListener.aidl b/core/java/com/android/internal/telephony/ISatelliteStateChangeListener.aidl
new file mode 100644
index 0000000..4d195c2
--- /dev/null
+++ b/core/java/com/android/internal/telephony/ISatelliteStateChangeListener.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+oneway interface ISatelliteStateChangeListener {
+ void onSatelliteEnabledStateChanged(boolean isEnabled);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index ca75abd..1c76a6c 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -38,6 +38,7 @@
import com.android.internal.telephony.ICarrierPrivilegesCallback;
import com.android.internal.telephony.IPhoneStateListener;
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
+import com.android.internal.telephony.ISatelliteStateChangeListener;
interface ITelephonyRegistry {
void addOnSubscriptionsChangedListener(String pkg, String featureId,
@@ -124,4 +125,8 @@
void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active);
void notifyCarrierRoamingNtnEligibleStateChanged(int subId, in boolean eligible);
void notifyCarrierRoamingNtnAvailableServicesChanged(int subId, in int[] availableServices);
+
+ void addSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg, String featureId);
+ void removeSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg);
+ void notifySatelliteStateChanged(boolean isEnabled);
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 363807d..72a9a2d 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -102,6 +102,7 @@
import com.android.internal.telephony.ICarrierPrivilegesCallback;
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
import com.android.internal.telephony.IPhoneStateListener;
+import com.android.internal.telephony.ISatelliteStateChangeListener;
import com.android.internal.telephony.ITelephonyRegistry;
import com.android.internal.telephony.TelephonyPermissions;
import com.android.internal.telephony.flags.Flags;
@@ -126,6 +127,7 @@
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/**
@@ -164,6 +166,7 @@
IOnSubscriptionsChangedListener onOpportunisticSubscriptionsChangedListenerCallback;
ICarrierPrivilegesCallback carrierPrivilegesCallback;
ICarrierConfigChangeListener carrierConfigChangeListener;
+ ISatelliteStateChangeListener satelliteStateChangeListener;
int callerUid;
int callerPid;
@@ -196,6 +199,10 @@
return carrierConfigChangeListener != null;
}
+ boolean matchSatelliteStateChangeListener() {
+ return satelliteStateChangeListener != null;
+ }
+
boolean canReadCallLog() {
try {
return TelephonyPermissions.checkReadCallLog(
@@ -215,6 +222,7 @@
+ onOpportunisticSubscriptionsChangedListenerCallback
+ " carrierPrivilegesCallback=" + carrierPrivilegesCallback
+ " carrierConfigChangeListener=" + carrierConfigChangeListener
+ + " satelliteStateChangeListener=" + satelliteStateChangeListener
+ " subId=" + subId + " phoneId=" + phoneId + " events=" + eventList + "}";
}
}
@@ -433,6 +441,10 @@
private List<IntArray> mCarrierRoamingNtnAvailableServices;
+ // Local cache to check if Satellite Modem is enabled
+ private AtomicBoolean mIsSatelliteEnabled;
+ private AtomicBoolean mWasSatelliteEnabledNotified;
+
/**
* Per-phone map of precise data connection state. The key of the map is the pair of transport
* type and APN setting. This is the cache to prevent redundant callbacks to the listeners.
@@ -871,6 +883,9 @@
mCarrierRoamingNtnMode = new boolean[numPhones];
mCarrierRoamingNtnEligible = new boolean[numPhones];
mCarrierRoamingNtnAvailableServices = new ArrayList<>();
+ mIsSatelliteEnabled = new AtomicBoolean();
+ mWasSatelliteEnabledNotified = new AtomicBoolean();
+
for (int i = 0; i < numPhones; i++) {
mCallState[i] = TelephonyManager.CALL_STATE_IDLE;
@@ -3425,6 +3440,94 @@
}
@Override
+ public void addSatelliteStateChangeListener(@NonNull ISatelliteStateChangeListener listener,
+ @NonNull String pkg, @Nullable String featureId) {
+ final int callerUserId = UserHandle.getCallingUserId();
+ mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+ enforceCallingOrSelfAtLeastReadBasicPhoneStatePermission(pkg, featureId,
+ "addSatelliteStateChangeListener");
+ if (VDBG) {
+ log("addSatelliteStateChangeListener pkg=" + pii(pkg)
+ + " uid=" + Binder.getCallingUid()
+ + " myUserId=" + UserHandle.myUserId() + " callerUerId" + callerUserId
+ + " listener=" + listener + " listener.asBinder=" + listener.asBinder());
+ }
+
+ synchronized (mRecords) {
+ final IBinder b = listener.asBinder();
+ boolean doesLimitApply = doesLimitApplyForListeners(Binder.getCallingUid(),
+ Process.myUid());
+ Record r = add(b, Binder.getCallingUid(), Binder.getCallingPid(), doesLimitApply);
+
+ if (r == null) {
+ loge("addSatelliteStateChangeListener: can not create Record instance!");
+ return;
+ }
+
+ r.context = mContext;
+ r.satelliteStateChangeListener = listener;
+ r.callingPackage = pkg;
+ r.callingFeatureId = featureId;
+ r.callerUid = Binder.getCallingUid();
+ r.callerPid = Binder.getCallingPid();
+ r.eventList = new ArraySet<>();
+ if (DBG) {
+ log("addSatelliteStateChangeListener: Register r=" + r);
+ }
+
+ // Always notify registrants on registration if it has been notified before
+ if (mWasSatelliteEnabledNotified.get() && r.matchSatelliteStateChangeListener()) {
+ try {
+ r.satelliteStateChangeListener.onSatelliteEnabledStateChanged(
+ mIsSatelliteEnabled.get());
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void removeSatelliteStateChangeListener(@NonNull ISatelliteStateChangeListener listener,
+ @NonNull String pkg) {
+ if (DBG) log("removeSatelliteStateChangeListener listener=" + listener + ", pkg=" + pkg);
+ mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+ enforceCallingOrSelfAtLeastReadBasicPhoneStatePermission(pkg, null,
+ "removeSatelliteStateChangeListener");
+ remove(listener.asBinder());
+ }
+
+ @Override
+ public void notifySatelliteStateChanged(boolean isEnabled) {
+ if (!checkNotifyPermission("notifySatelliteStateChanged")) {
+ loge("notifySatelliteStateChanged: Caller has no notify permission!");
+ return;
+ }
+ if (VDBG) {
+ log("notifySatelliteStateChanged: isEnabled=" + isEnabled);
+ }
+
+ mWasSatelliteEnabledNotified.set(true);
+ mIsSatelliteEnabled.set(isEnabled);
+
+ synchronized (mRecords) {
+ mRemoveList.clear();
+ for (Record r : mRecords) {
+ // Listeners are "global", neither per-slot nor per-sub, so no idMatch check here
+ if (!r.matchSatelliteStateChangeListener()) {
+ continue;
+ }
+ try {
+ r.satelliteStateChangeListener.onSatelliteEnabledStateChanged(isEnabled);
+ } catch (RemoteException re) {
+ mRemoveList.add(r.binder);
+ }
+ }
+ handleRemoveListLocked();
+ }
+ }
+
+ @Override
public void notifyMediaQualityStatusChanged(int phoneId, int subId, MediaQualityStatus status) {
if (!checkNotifyPermission("notifyMediaQualityStatusChanged()")) {
return;
@@ -4622,4 +4725,32 @@
if (packageNames.isEmpty() || Build.IS_DEBUGGABLE) return packageNames.toString();
return "[***, size=" + packageNames.size() + "]";
}
+
+ /**
+ * The method enforces the calling package at least has READ_BASIC_PHONE_STATE permission.
+ * That is, calling package either has READ_PRIVILEGED_PHONE_STATE, READ_PHONE_STATE or Carrier
+ * Privileges on ANY active subscription, or has READ_BASIC_PHONE_STATE permission.
+ */
+ private void enforceCallingOrSelfAtLeastReadBasicPhoneStatePermission(String pkgName,
+ String featureId, String message) {
+ // Check if calling app has READ_PHONE_STATE on ANY active subscription
+ boolean hasReadPhoneState = false;
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ if (sm != null) {
+ for (int subId : sm.getActiveSubscriptionIdList()) {
+ if (TelephonyPermissions.checkCallingOrSelfReadPhoneStateNoThrow(mContext, subId,
+ pkgName, featureId, message)) {
+ hasReadPhoneState = true;
+ break;
+ }
+ }
+ }
+
+ // If yes, pass. If not, then enforce READ_BASIC_PHONE_STATE permission
+ if (!hasReadPhoneState) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.READ_BASIC_PHONE_STATE,
+ message);
+ }
+ }
}