[Satellite][WIP] Bind PSS with SatelliteTestApp

Earlier this test app is used to test SatelliteService. With this new
activity, PixelSatelliteService (Child class) can also be tested.

CL includes:
- SatelliteTestApp binds PixelSatelliteService.
- APIs of PSS can be triggered from UI.
- Multiple iterations can be triggered.
- Calculate time taken by the request.


Bug: 395782973
Doc: go/satellite-test-app
Test: manual
Flag: TEST_ONLY
Change-Id: I51395a53be5c10391c5c8452739414bb9bde397b
diff --git a/testapps/TestSatelliteApp/Android.bp b/testapps/TestSatelliteApp/Android.bp
index 78d125d..fa95de6 100644
--- a/testapps/TestSatelliteApp/Android.bp
+++ b/testapps/TestSatelliteApp/Android.bp
@@ -13,6 +13,7 @@
     ],
     static_libs: [
         "SatelliteClient",
+        "google-satellite",
     ],
     owner: "google",
     privileged: true,
diff --git a/testapps/TestSatelliteApp/AndroidManifest.xml b/testapps/TestSatelliteApp/AndroidManifest.xml
index de455f2..814e958 100644
--- a/testapps/TestSatelliteApp/AndroidManifest.xml
+++ b/testapps/TestSatelliteApp/AndroidManifest.xml
@@ -38,6 +38,7 @@
         <activity android:name=".SendReceive" />
         <activity android:name=".NbIotSatellite" />
         <activity android:name=".TestSatelliteWrapper" />
+        <activity android:name=".PssActivity" />
 
         <receiver
             android:name=".SatelliteTestAppReceiver"
diff --git a/testapps/TestSatelliteApp/res/layout/activity_Pss.xml b/testapps/TestSatelliteApp/res/layout/activity_Pss.xml
new file mode 100644
index 0000000..7f8ac44
--- /dev/null
+++ b/testapps/TestSatelliteApp/res/layout/activity_Pss.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 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
+  -->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true"
+    android:orientation="vertical"
+    android:gravity="center"
+    android:padding="20dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/holo_blue_light"
+            android:text="@string/SelectCarrier"
+            android:padding="16dp"/>
+
+        <Spinner
+            android:id="@+id/spinner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:minHeight="48dp" />
+
+        <Switch
+            android:id="@+id/DemoModeSwitch"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:padding="16dp"
+            android:text="@string/DemoMode" />
+
+        <Switch
+            android:id="@+id/EnableSwitch"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:padding="16dp"
+            android:text="@string/Enable" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/holo_blue_light"
+            android:text="@string/SelectIterations"
+            android:padding="16dp"/>
+
+        <NumberPicker
+            android:id="@+id/numberPicker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"  />
+
+        <Button
+            android:id="@+id/requestSatelliteEnable"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:padding="16dp"
+            android:text="@string/requestSatelliteEnable"/>
+
+        <TextView
+            android:id="@+id/logView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/holo_blue_light"
+            android:padding="16dp"/>
+
+    </LinearLayout>
+</ScrollView>
diff --git a/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml b/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
index 43cce9b..c85594d 100644
--- a/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
+++ b/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
@@ -17,6 +17,7 @@
 
 <ScrollView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fitsSystemWindows="true"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical"
@@ -88,5 +89,12 @@
             android:paddingStart="4dp"
             android:paddingEnd="4dp"
             android:text="@string/TestSatelliteConstrainConnection"/>
+        <Button
+            android:id="@+id/PssActivity"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingStart="4dp"
+            android:paddingEnd="4dp"
+            android:text="@string/PssActivity"/>
     </LinearLayout>
 </ScrollView>
diff --git a/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml b/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
index f48c022..ae76a56 100644
--- a/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
+++ b/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
@@ -113,4 +113,14 @@
     <string name="unregisterForModemStateChanged">unregisterForModemStateChanged</string>
 
     <string name="requestSatelliteAccessConfigurationForCurrentLocation">requestSatelliteAccessConfigurationForCurrentLocation</string>
+
+    <string name="requestSatelliteEnable">requestSatelliteEnable</string>
+    <string name="requestSatelliteModemState">requestSatelliteModemState</string>
+    <string name="startSendingNtnSignalStrength">startSendingSignalStrength</string>
+    <string name="stopSendingNtnSignalStrength">stopSendingSignalStrength</string>
+    <string name="PssActivity">Pss Activity</string>
+    <string name="DemoMode">Demo Mode</string>
+    <string name="Enable">Enable</string>
+    <string name="SelectIterations">Select number of iterations</string>
+    <string name="SelectCarrier">Select a carrier</string>
 </resources>
diff --git a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PssActivity.java b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PssActivity.java
new file mode 100644
index 0000000..84b49bd
--- /dev/null
+++ b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PssActivity.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2025 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.phone.testapps.satellitetestapp;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.telephony.IIntegerConsumer;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.satellite.stub.ISatellite;
+import android.telephony.satellite.stub.ISatelliteListener;
+import android.telephony.satellite.stub.NtnSignalStrength;
+import android.telephony.satellite.stub.PointingInfo;
+import android.telephony.satellite.stub.SatelliteCapabilities;
+import android.telephony.satellite.stub.SatelliteDatagram;
+import android.telephony.satellite.stub.SatelliteModemEnableRequestAttributes;
+import android.telephony.satellite.stub.SatelliteSubscriptionInfo;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.NumberPicker;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Activity to bind PSS. */
+public class PssActivity extends Activity implements AdapterView.OnItemSelectedListener {
+
+    private static final String TAG = "PssActivity";
+    private static final String SATELLITE_ACTION = "android.telephony.satellite.SatelliteService";
+    private static final String PSS_PACKAGE = "com.google.android.satellite";
+
+    private ISatellite mSatelliteService;
+    private SubscriptionManager mSubscriptionManager;
+    private boolean mIsDemoModeSwitchEnabled = false;
+    private boolean mIsEnabledSwitchEnabled = false;
+    private int mIterations = 1;
+    private int mSuccessCount = 0;
+    private int mCurrentIteration = 0;
+    private long mTotalTime = 0;
+    private long mStartTime;
+    private String mIccId = "";
+    private Runnable mTask;
+    private TextView mLogTextView;
+    private final ArrayDeque<String> mLogQueue = new ArrayDeque();
+    private static final int QUEUE_SIZE = 10;
+
+    private List<Carrier> mSubList;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_Pss);
+        setTitle(R.string.PssActivity);
+
+        initViews();
+        getSubscriptionInfo();
+        bindPss();
+    }
+
+    private void getSubscriptionInfo() {
+        if (mSubscriptionManager == null) {
+            mSubscriptionManager =
+                    (SubscriptionManager)
+                            getBaseContext()
+                                    .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        }
+        mSubList = new ArrayList();
+
+        for (SubscriptionInfo subInfo : mSubscriptionManager.getAllSubscriptionInfoList()) {
+            logi("iccId: " + subInfo.getIccId() + " carrier name " + subInfo.getCarrierName());
+            boolean isNtn = subInfo.isOnlyNonTerrestrialNetwork();
+            mSubList.add(
+                    new Carrier(subInfo.getIccId(), subInfo.getCarrierName().toString(), isNtn));
+
+            if (isNtn) {
+                mIccId = subInfo.getIccId();
+                logi("NTN iccId: " + mIccId);
+            }
+        }
+        Spinner mSpinner = findViewById(R.id.spinner);
+        ArrayAdapter<Carrier> adapter =
+                new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, mSubList);
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        mSpinner.setAdapter(adapter);
+        mSpinner.setOnItemSelectedListener(this);
+    }
+
+    private void initViews() {
+        findViewById(R.id.requestSatelliteEnable).setOnClickListener(this::requestSatelliteEnable);
+
+        mLogTextView = findViewById(R.id.logView);
+        Switch demoSwitch = findViewById(R.id.DemoModeSwitch);
+        demoSwitch.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> mIsDemoModeSwitchEnabled = isChecked);
+
+        Switch enableSwitch = findViewById(R.id.EnableSwitch);
+        enableSwitch.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> mIsEnabledSwitchEnabled = isChecked);
+
+        NumberPicker numberPicker = findViewById(R.id.numberPicker);
+        numberPicker.setMinValue(1);
+        numberPicker.setMaxValue(100);
+        numberPicker.setValue(1);
+        numberPicker.setOnValueChangedListener((picker, oldVal, newVal) -> mIterations = newVal);
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        Carrier selectedItem = mSubList.get(position);
+        mIccId = selectedItem.getIccId();
+        Toast.makeText(this, "Selected: " + selectedItem.getName(), Toast.LENGTH_SHORT).show();
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        // Optional: Handle the case where nothing is selected
+    }
+
+    private void requestSatelliteEnable(View v) {
+        mCurrentIteration = 1;
+        mTotalTime = 0;
+        mSuccessCount = 0;
+        mStartTime = System.currentTimeMillis();
+        boolean isSwitchEnabled = mIsEnabledSwitchEnabled;
+        mTask =
+                () -> {
+                    IIntegerConsumer callback =
+                            new IIntegerConsumer.Stub() {
+                                @Override
+                                public void accept(int result) {
+                                    long duration = System.currentTimeMillis() - mStartTime;
+                                    mStartTime = System.currentTimeMillis();
+                                    logi(
+                                            "Request type: "
+                                                    + (mIsEnabledSwitchEnabled
+                                                            ? "Enable"
+                                                            : "Disable")
+                                                    + " mCurrentIteration: "
+                                                    + mCurrentIteration
+                                                    + " requestSatelliteEnable error: "
+                                                    + result
+                                                    + " duration: "
+                                                    + duration);
+                                    mIsEnabledSwitchEnabled = !mIsEnabledSwitchEnabled;
+                                    if (result == 0) {
+                                        mTotalTime += duration;
+                                        mSuccessCount += 1;
+                                    }
+                                    if (mCurrentIteration < mIterations) {
+                                        mCurrentIteration++;
+                                        mTask.run();
+                                    } else {
+                                        logi(
+                                                "Pass: "
+                                                        + mSuccessCount
+                                                        + "/"
+                                                        + mIterations
+                                                        + " Average duration in ms : "
+                                                        + mTotalTime / mSuccessCount);
+                                        mIsEnabledSwitchEnabled = isSwitchEnabled;
+                                    }
+                                }
+                            };
+                    try {
+                        mSatelliteService.requestSatelliteEnabled(
+                                createModemEnableRequest(), callback);
+                    } catch (Exception e) {
+                        logi("requestSatelliteEnable: " + e);
+                    }
+                };
+        mTask.run();
+    }
+
+    private SatelliteModemEnableRequestAttributes createModemEnableRequest() {
+        String apn = "pixel.ntn";
+        SatelliteModemEnableRequestAttributes attributes =
+                new SatelliteModemEnableRequestAttributes();
+        attributes.isEnabled = mIsEnabledSwitchEnabled;
+        attributes.isDemoMode = mIsDemoModeSwitchEnabled;
+        SatelliteSubscriptionInfo info = new SatelliteSubscriptionInfo();
+        info.iccId = mIccId;
+        info.niddApn = apn;
+        attributes.satelliteSubscriptionInfo = info;
+        return attributes;
+    }
+
+    private final class PssConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            logi("onServiceConnected Service: " + name);
+            mSatelliteService = ISatellite.Stub.asInterface(service);
+            try {
+                mSatelliteService.setSatelliteListener(mSatelliteListener);
+            } catch (Exception e) {
+                Log.e(TAG, "onServiceConnected: setListener error: ", e);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            logi("onServiceDisconnected Service: " + name);
+        }
+    }
+
+    private void bindPss() {
+        logi("Binding PSS...");
+        Intent intent = new Intent(SATELLITE_ACTION);
+        intent.setPackage(PSS_PACKAGE);
+        PssConnection pssConnection = new PssConnection();
+
+        try {
+            getBaseContext()
+                    .bindService(
+                            intent,
+                            pssConnection,
+                            Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE);
+        } catch (SecurityException exception) {
+            logi("BindService failed " + exception);
+        }
+    }
+
+    private final ISatelliteListener mSatelliteListener =
+            new ISatelliteListener.Stub() {
+                @Override
+                public void onSatelliteDatagramReceived(
+                        SatelliteDatagram datagram, int pendingCount) {
+                    logi("onSatelliteDatagramReceived: " + Arrays.toString(datagram.data));
+                }
+
+                @Override
+                public void onPendingDatagrams() {
+                    logi("onPendingDatagrams: ");
+                }
+
+                @Override
+                public void onSatellitePositionChanged(PointingInfo pointingInfo) {
+                    logi(
+                            "onSatellitePositionChanged: Azimuth= "
+                                    + pointingInfo.satelliteAzimuth
+                                    + " Elevation "
+                                    + pointingInfo.satelliteElevation);
+                }
+
+                @Override
+                public void onSatelliteModemStateChanged(int state) {
+                    logi("onSatelliteModemStateChanged: " + state);
+                }
+
+                @Override
+                public void onNtnSignalStrengthChanged(NtnSignalStrength ntnSignalStrength) {
+                    logi("onNtnSignalStrengthChanged: " + ntnSignalStrength.signalStrengthLevel);
+                }
+
+                @Override
+                public void onSatelliteCapabilitiesChanged(SatelliteCapabilities capabilities) {
+                    logi(
+                            "onSatelliteCapabilitiesChanged: "
+                                    + Arrays.toString(capabilities.supportedRadioTechnologies));
+                }
+
+                @Override
+                public void onSatelliteSupportedStateChanged(boolean supported) {
+                    logi("onSatelliteSupportedStateChanged: " + supported);
+                }
+
+                @Override
+                public void onRegistrationFailure(int causeCode) {
+                    logi("onRegistrationFailure: " + causeCode);
+                }
+
+                @Override
+                public void onTerrestrialNetworkAvailableChanged(boolean isAvailable) {
+                    logi("onTerrestrialNetworkAvailableChanged: " + isAvailable);
+                }
+            };
+
+    private void logi(String str) {
+        Log.i(TAG, str);
+        if (mLogQueue.size() >= QUEUE_SIZE) {
+            mLogQueue.pollLast();
+        }
+        mLogQueue.offerFirst(str);
+        updateTextView();
+    }
+
+    private void updateTextView() {
+        StringBuilder sb = new StringBuilder();
+        for (String s : mLogQueue) {
+            sb.append(s).append("\n");
+        }
+        runOnUiThread(() -> mLogTextView.setText(sb.toString()));
+    }
+
+    private static class Carrier {
+        private final String mIccId;
+        private final String mName;
+        private final boolean mIsNtn;
+
+        Carrier(String iccId, String name, boolean isNtn) {
+            this.mIccId = iccId;
+            this.mName = name;
+            this.mIsNtn = isNtn;
+        }
+
+        public boolean isNtn() {
+            return mIsNtn;
+        }
+
+        public String getIccId() {
+            return mIccId;
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        @Override
+        public String toString() {
+            return mName + (isNtn() ? " (NTN)" : "");
+        }
+    }
+}
diff --git a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
index 911e179..e205f6e 100644
--- a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
+++ b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
@@ -139,6 +139,13 @@
           }
         });
       });
+        findViewById(R.id.PssActivity).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                Intent intent = new Intent(SatelliteTestApp.this, PssActivity.class);
+                startActivity(intent);
+            }
+        });
     }
 
     @Override