1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony.satellite;
18 
19 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.ActivityManager;
24 import android.content.ActivityNotFoundException;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.os.AsyncResult;
30 import android.os.Build;
31 import android.os.Handler;
32 import android.os.IBinder;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.os.RemoteException;
36 import android.os.SystemProperties;
37 import android.telephony.DropBoxManagerLoggerBackend;
38 import android.telephony.PersistentLogger;
39 import android.telephony.Rlog;
40 import android.telephony.satellite.ISatelliteTransmissionUpdateCallback;
41 import android.telephony.satellite.PointingInfo;
42 import android.telephony.satellite.SatelliteManager;
43 import android.text.TextUtils;
44 
45 import com.android.internal.R;
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.telephony.flags.FeatureFlags;
48 
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.List;
52 import java.util.concurrent.ConcurrentHashMap;
53 import java.util.function.Consumer;
54 
55 /**
56  * PointingApp controller to manage interactions with PointingUI app.
57  */
58 public class PointingAppController {
59     private static final String TAG = "PointingAppController";
60     private static final String ALLOW_MOCK_MODEM_PROPERTY = "persist.radio.allow_mock_modem";
61     private static final boolean DEBUG = !"user".equals(Build.TYPE);
62 
63     @NonNull
64     private static PointingAppController sInstance;
65     @NonNull private final Context mContext;
66     @NonNull private final FeatureFlags mFeatureFlags;
67     private boolean mStartedSatelliteTransmissionUpdates;
68     private boolean mLastNeedFullScreenPointingUI;
69     private boolean mLastIsDemoMode;
70     private boolean mLastIsEmergency;
71     private boolean mListenerForPointingUIRegistered;
72     @NonNull private String mPointingUiPackageName = "";
73     @NonNull private String mPointingUiClassName = "";
74     @NonNull private ActivityManager mActivityManager;
75     @NonNull public UidImportanceListener mUidImportanceListener = new UidImportanceListener();
76     /**
77      * Map key: subId, value: SatelliteTransmissionUpdateHandler to notify registrants.
78      */
79     private final ConcurrentHashMap<Integer, SatelliteTransmissionUpdateHandler>
80             mSatelliteTransmissionUpdateHandlers = new ConcurrentHashMap<>();
81     @Nullable private PersistentLogger mPersistentLogger = null;
82 
83     /**
84      * @return The singleton instance of PointingAppController.
85      */
getInstance()86     public static PointingAppController getInstance() {
87         if (sInstance == null) {
88             loge("PointingAppController was not yet initialized.");
89         }
90         return sInstance;
91     }
92 
93     /**
94      * Create the PointingAppController singleton instance.
95      * @param context The Context to use to create the PointingAppController.
96      * @param featureFlags The telephony feature flags.
97      * @return The singleton instance of PointingAppController.
98      */
make(@onNull Context context, @NonNull FeatureFlags featureFlags)99     public static PointingAppController make(@NonNull Context context,
100             @NonNull FeatureFlags featureFlags) {
101         if (sInstance == null) {
102             sInstance = new PointingAppController(context, featureFlags);
103         }
104         return sInstance;
105     }
106 
107     /**
108      * Create a PointingAppController to manage interactions with PointingUI app.
109      *
110      * @param context The Context for the PointingUIController.
111      */
112     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
PointingAppController(@onNull Context context, @NonNull FeatureFlags featureFlags)113     public PointingAppController(@NonNull Context context,
114             @NonNull FeatureFlags featureFlags) {
115         mContext = context;
116         mFeatureFlags = featureFlags;
117         mStartedSatelliteTransmissionUpdates = false;
118         mLastNeedFullScreenPointingUI = false;
119         mLastIsDemoMode = false;
120         mLastIsEmergency = false;
121         mListenerForPointingUIRegistered = false;
122         mActivityManager = mContext.getSystemService(ActivityManager.class);
123         if (isSatellitePersistentLoggingEnabled(context, featureFlags)) {
124             mPersistentLogger = new PersistentLogger(
125                     DropBoxManagerLoggerBackend.getInstance(context));
126         }
127     }
128 
129     /**
130      * Set the flag mStartedSatelliteTransmissionUpdates to true or false based on the state of
131      * transmission updates
132      * @param startedSatelliteTransmissionUpdates boolean to set the flag
133      */
134     @VisibleForTesting
setStartedSatelliteTransmissionUpdates( boolean startedSatelliteTransmissionUpdates)135     public void setStartedSatelliteTransmissionUpdates(
136             boolean startedSatelliteTransmissionUpdates) {
137         mStartedSatelliteTransmissionUpdates = startedSatelliteTransmissionUpdates;
138     }
139 
140     /**
141      * Get the flag mStartedSatelliteTransmissionUpdates
142      * @return returns mStartedSatelliteTransmissionUpdates
143      */
144     @VisibleForTesting
getStartedSatelliteTransmissionUpdates()145     public boolean getStartedSatelliteTransmissionUpdates() {
146         return mStartedSatelliteTransmissionUpdates;
147     }
148 
149     /**
150      * Listener for handling pointing UI App in the event of crash
151      */
152     @VisibleForTesting
153     public class UidImportanceListener implements ActivityManager.OnUidImportanceListener {
154         @Override
onUidImportance(int uid, int importance)155         public void onUidImportance(int uid, int importance) {
156             if (importance != IMPORTANCE_GONE) return;
157             final PackageManager pm = mContext.getPackageManager();
158             final String[] callerPackages = pm.getPackagesForUid(uid);
159             String pointingUiPackage = getPointingUiPackageName();
160 
161             if (callerPackages != null) {
162                 if (Arrays.stream(callerPackages).anyMatch(pointingUiPackage::contains)) {
163                     plogd("Restarting pointingUI");
164                     startPointingUI(mLastNeedFullScreenPointingUI, mLastIsDemoMode,
165                             mLastIsEmergency);
166                 }
167             }
168         }
169     }
170 
171     private static final class DatagramTransferStateHandlerRequest {
172         public int datagramType;
173         public int datagramTransferState;
174         public int pendingCount;
175         public int errorCode;
176 
DatagramTransferStateHandlerRequest(int datagramType, int datagramTransferState, int pendingCount, int errorCode)177         DatagramTransferStateHandlerRequest(int datagramType, int datagramTransferState,
178                 int pendingCount, int errorCode) {
179             this.datagramType = datagramType;
180             this.datagramTransferState = datagramTransferState;
181             this.pendingCount = pendingCount;
182             this.errorCode = errorCode;
183         }
184     }
185 
186 
187     private static final class SatelliteTransmissionUpdateHandler extends Handler {
188         public static final int EVENT_POSITION_INFO_CHANGED = 1;
189         public static final int EVENT_SEND_DATAGRAM_STATE_CHANGED = 2;
190         public static final int EVENT_RECEIVE_DATAGRAM_STATE_CHANGED = 3;
191         public static final int EVENT_DATAGRAM_TRANSFER_STATE_CHANGED = 4;
192 
193         private final ConcurrentHashMap<IBinder, ISatelliteTransmissionUpdateCallback> mListeners;
194 
SatelliteTransmissionUpdateHandler(Looper looper)195         SatelliteTransmissionUpdateHandler(Looper looper) {
196             super(looper);
197             mListeners = new ConcurrentHashMap<>();
198         }
199 
addListener(ISatelliteTransmissionUpdateCallback listener)200         public void addListener(ISatelliteTransmissionUpdateCallback listener) {
201             mListeners.put(listener.asBinder(), listener);
202         }
203 
removeListener(ISatelliteTransmissionUpdateCallback listener)204         public void removeListener(ISatelliteTransmissionUpdateCallback listener) {
205             mListeners.remove(listener.asBinder());
206         }
207 
hasListeners()208         public boolean hasListeners() {
209             return !mListeners.isEmpty();
210         }
211 
212         @Override
handleMessage(@onNull Message msg)213         public void handleMessage(@NonNull Message msg) {
214             switch (msg.what) {
215                 case EVENT_POSITION_INFO_CHANGED: {
216                     AsyncResult ar = (AsyncResult) msg.obj;
217                     PointingInfo pointingInfo = (PointingInfo) ar.result;
218                     List<IBinder> toBeRemoved = new ArrayList<>();
219                     mListeners.values().forEach(listener -> {
220                         try {
221                             listener.onSatellitePositionChanged(pointingInfo);
222                         } catch (RemoteException e) {
223                             logd("EVENT_POSITION_INFO_CHANGED RemoteException: " + e);
224                             toBeRemoved.add(listener.asBinder());
225                         }
226                     });
227                     toBeRemoved.forEach(listener -> {
228                         mListeners.remove(listener);
229                     });
230                     break;
231                 }
232 
233                 case EVENT_DATAGRAM_TRANSFER_STATE_CHANGED: {
234                     AsyncResult ar = (AsyncResult) msg.obj;
235                     logd("Receive EVENT_DATAGRAM_TRANSFER_STATE_CHANGED state=" + (int) ar.result);
236                     break;
237                 }
238 
239                 case EVENT_SEND_DATAGRAM_STATE_CHANGED: {
240                     logd("Received EVENT_SEND_DATAGRAM_STATE_CHANGED");
241                     DatagramTransferStateHandlerRequest request =
242                             (DatagramTransferStateHandlerRequest) msg.obj;
243                     List<IBinder> toBeRemoved = new ArrayList<>();
244                     mListeners.values().forEach(listener -> {
245                         try {
246                             listener.onSendDatagramStateChanged(request.datagramType,
247                                     request.datagramTransferState, request.pendingCount,
248                                     request.errorCode);
249                         } catch (RemoteException e) {
250                             logd("EVENT_SEND_DATAGRAM_STATE_CHANGED RemoteException: " + e);
251                             toBeRemoved.add(listener.asBinder());
252                         }
253                     });
254                     toBeRemoved.forEach(listener -> {
255                         mListeners.remove(listener);
256                     });
257                     break;
258                 }
259 
260                 case EVENT_RECEIVE_DATAGRAM_STATE_CHANGED: {
261                     logd("Received EVENT_RECEIVE_DATAGRAM_STATE_CHANGED");
262                     DatagramTransferStateHandlerRequest request =
263                             (DatagramTransferStateHandlerRequest) msg.obj;
264                     List<IBinder> toBeRemoved = new ArrayList<>();
265                     mListeners.values().forEach(listener -> {
266                         try {
267                             listener.onReceiveDatagramStateChanged(request.datagramTransferState,
268                                     request.pendingCount, request.errorCode);
269                         } catch (RemoteException e) {
270                             logd("EVENT_RECEIVE_DATAGRAM_STATE_CHANGED RemoteException: " + e);
271                             toBeRemoved.add(listener.asBinder());
272                         }
273                     });
274                     toBeRemoved.forEach(listener -> {
275                         mListeners.remove(listener);
276                     });
277                     break;
278                 }
279 
280                 default:
281                     loge("SatelliteTransmissionUpdateHandler unknown event: " + msg.what);
282             }
283         }
284     }
285 
286     /**
287      * Register to start receiving updates for satellite position and datagram transfer state
288      * @param subId The subId of the subscription to register for receiving the updates.
289      * @param callback The callback to notify of satellite transmission updates.
290      */
registerForSatelliteTransmissionUpdates(int subId, ISatelliteTransmissionUpdateCallback callback)291     public void registerForSatelliteTransmissionUpdates(int subId,
292             ISatelliteTransmissionUpdateCallback callback) {
293         SatelliteTransmissionUpdateHandler handler =
294                 mSatelliteTransmissionUpdateHandlers.get(subId);
295         if (handler != null) {
296             handler.addListener(callback);
297         } else {
298             handler = new SatelliteTransmissionUpdateHandler(Looper.getMainLooper());
299             handler.addListener(callback);
300             mSatelliteTransmissionUpdateHandlers.put(subId, handler);
301             SatelliteModemInterface.getInstance().registerForSatellitePositionInfoChanged(
302                     handler, SatelliteTransmissionUpdateHandler.EVENT_POSITION_INFO_CHANGED,
303                     null);
304             SatelliteModemInterface.getInstance().registerForDatagramTransferStateChanged(
305                     handler,
306                     SatelliteTransmissionUpdateHandler.EVENT_DATAGRAM_TRANSFER_STATE_CHANGED,
307                     null);
308         }
309     }
310 
311     /**
312      * Unregister to stop receiving updates on satellite position and datagram transfer state
313      * If the callback was not registered before, it is ignored
314      * @param subId The subId of the subscription to unregister for receiving the updates.
315      * @param result The callback to get the error code in case of failure
316      * @param callback The callback that was passed to {@link
317      * #registerForSatelliteTransmissionUpdates(int, ISatelliteTransmissionUpdateCallback)}.
318      */
unregisterForSatelliteTransmissionUpdates(int subId, Consumer<Integer> result, ISatelliteTransmissionUpdateCallback callback)319     public void unregisterForSatelliteTransmissionUpdates(int subId, Consumer<Integer> result,
320             ISatelliteTransmissionUpdateCallback callback) {
321         SatelliteTransmissionUpdateHandler handler =
322                 mSatelliteTransmissionUpdateHandlers.get(subId);
323         if (handler != null) {
324             handler.removeListener(callback);
325             if (handler.hasListeners()) {
326                 result.accept(SatelliteManager.SATELLITE_RESULT_SUCCESS);
327                 return;
328             }
329             mSatelliteTransmissionUpdateHandlers.remove(subId);
330 
331             SatelliteModemInterface satelliteModemInterface = SatelliteModemInterface.getInstance();
332             satelliteModemInterface.unregisterForSatellitePositionInfoChanged(handler);
333             satelliteModemInterface.unregisterForDatagramTransferStateChanged(handler);
334         }
335     }
336 
337     /**
338      * Start receiving satellite trasmission updates.
339      * This can be called by the pointing UI when the user starts pointing to the satellite.
340      * Modem should continue to report the pointing input as the device or satellite moves.
341      * The transmission updates will be received via
342      * {@link android.telephony.satellite.SatelliteTransmissionUpdateCallback
343      * #onSatellitePositionChanged(pointingInfo)}.
344      */
startSatelliteTransmissionUpdates(@onNull Message message)345     public void startSatelliteTransmissionUpdates(@NonNull Message message) {
346         if (mStartedSatelliteTransmissionUpdates) {
347             plogd("startSatelliteTransmissionUpdates: already started");
348             AsyncResult.forMessage(message, null, new SatelliteManager.SatelliteException(
349                     SatelliteManager.SATELLITE_RESULT_SUCCESS));
350             message.sendToTarget();
351             return;
352         }
353         SatelliteModemInterface.getInstance().startSendingSatellitePointingInfo(message);
354         mStartedSatelliteTransmissionUpdates = true;
355     }
356 
357     /**
358      * Stop receiving satellite transmission updates.
359      * Reset the flag mStartedSatelliteTransmissionUpdates
360      * This can be called by the pointing UI when the user stops pointing to the satellite.
361      */
stopSatelliteTransmissionUpdates(@onNull Message message)362     public void stopSatelliteTransmissionUpdates(@NonNull Message message) {
363         setStartedSatelliteTransmissionUpdates(false);
364         SatelliteModemInterface.getInstance().stopSendingSatellitePointingInfo(message);
365     }
366 
367     /**
368      * Check if Pointing is needed and Launch Pointing UI
369      * @param needFullScreenPointingUI if pointing UI has to be launchd with Full screen
370      */
startPointingUI(boolean needFullScreenPointingUI, boolean isDemoMode, boolean isEmergency)371     public void startPointingUI(boolean needFullScreenPointingUI, boolean isDemoMode,
372             boolean isEmergency) {
373         String packageName = getPointingUiPackageName();
374         if (TextUtils.isEmpty(packageName)) {
375             plogd("startPointingUI: config_pointing_ui_package is not set. Ignore the request");
376             return;
377         }
378 
379         Intent launchIntent;
380         String className = getPointingUiClassName();
381         if (!TextUtils.isEmpty(className)) {
382             launchIntent = new Intent()
383                     .setComponent(new ComponentName(packageName, className))
384                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
385         } else {
386             launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(packageName);
387         }
388         if (launchIntent == null) {
389             ploge("startPointingUI: launchIntent is null");
390             return;
391         }
392         plogd("startPointingUI: needFullScreenPointingUI: " + needFullScreenPointingUI
393                 + ", isDemoMode: " + isDemoMode + ", isEmergency: " + isEmergency);
394         launchIntent.putExtra("needFullScreen", needFullScreenPointingUI);
395         launchIntent.putExtra("isDemoMode", isDemoMode);
396         launchIntent.putExtra("isEmergency", isEmergency);
397 
398         try {
399             if (!mListenerForPointingUIRegistered) {
400                 mActivityManager.addOnUidImportanceListener(mUidImportanceListener,
401                         IMPORTANCE_GONE);
402                 mListenerForPointingUIRegistered = true;
403             }
404             mLastNeedFullScreenPointingUI = needFullScreenPointingUI;
405             mLastIsDemoMode = isDemoMode;
406             mLastIsEmergency = isEmergency;
407             mContext.startActivity(launchIntent);
408         } catch (ActivityNotFoundException ex) {
409             ploge("startPointingUI: Pointing UI app activity is not found, ex=" + ex);
410         }
411     }
412 
413     /**
414      * Remove the Importance Listener For Pointing UI App once the satellite is disabled
415      */
removeListenerForPointingUI()416     public void removeListenerForPointingUI() {
417         if (mListenerForPointingUIRegistered) {
418             mActivityManager.removeOnUidImportanceListener(mUidImportanceListener);
419             mListenerForPointingUIRegistered = false;
420         }
421     }
422 
updateSendDatagramTransferState(int subId, @SatelliteManager.DatagramType int datagramType, @SatelliteManager.SatelliteDatagramTransferState int datagramTransferState, int sendPendingCount, int errorCode)423     public void updateSendDatagramTransferState(int subId,
424             @SatelliteManager.DatagramType int datagramType,
425             @SatelliteManager.SatelliteDatagramTransferState int datagramTransferState,
426             int sendPendingCount, int errorCode) {
427         DatagramTransferStateHandlerRequest request = new DatagramTransferStateHandlerRequest(
428                 datagramType, datagramTransferState, sendPendingCount, errorCode);
429         SatelliteTransmissionUpdateHandler handler =
430                 mSatelliteTransmissionUpdateHandlers.get(subId);
431 
432         if (handler != null) {
433             Message msg = handler.obtainMessage(
434                     SatelliteTransmissionUpdateHandler.EVENT_SEND_DATAGRAM_STATE_CHANGED,
435                     request);
436             msg.sendToTarget();
437         } else {
438             ploge("SatelliteTransmissionUpdateHandler not found for subId: " + subId);
439         }
440     }
441 
updateReceiveDatagramTransferState(int subId, @SatelliteManager.SatelliteDatagramTransferState int datagramTransferState, int receivePendingCount, int errorCode)442     public void updateReceiveDatagramTransferState(int subId,
443             @SatelliteManager.SatelliteDatagramTransferState int datagramTransferState,
444             int receivePendingCount, int errorCode) {
445         DatagramTransferStateHandlerRequest request = new DatagramTransferStateHandlerRequest(
446                 SatelliteManager.DATAGRAM_TYPE_UNKNOWN, datagramTransferState, receivePendingCount,
447                 errorCode);
448         SatelliteTransmissionUpdateHandler handler =
449                 mSatelliteTransmissionUpdateHandlers.get(subId);
450 
451         if (handler != null) {
452             Message msg = handler.obtainMessage(
453                     SatelliteTransmissionUpdateHandler.EVENT_RECEIVE_DATAGRAM_STATE_CHANGED,
454                     request);
455             msg.sendToTarget();
456         } else {
457             ploge(" SatelliteTransmissionUpdateHandler not found for subId: " + subId);
458         }
459     }
460 
461     /**
462      * This API can be used by only CTS to update satellite pointing UI app package and class names.
463      *
464      * @param packageName The package name of the satellite pointing UI app.
465      * @param className The class name of the satellite pointing UI app.
466      * @return {@code true} if the satellite pointing UI app package and class is set successfully,
467      * {@code false} otherwise.
468      */
setSatellitePointingUiClassName( @ullable String packageName, @Nullable String className)469     boolean setSatellitePointingUiClassName(
470             @Nullable String packageName, @Nullable String className) {
471         if (!isMockModemAllowed()) {
472             ploge("setSatellitePointingUiClassName: modifying satellite pointing UI package and "
473                     + "class name is not allowed");
474             return false;
475         }
476 
477         plogd("setSatellitePointingUiClassName: config_pointing_ui_package is updated, new "
478                 + "packageName=" + packageName
479                 + ", config_pointing_ui_class new className=" + className);
480 
481         if (packageName == null || packageName.equals("null")) {
482             mPointingUiPackageName = "";
483             mPointingUiClassName = "";
484         } else {
485             mPointingUiPackageName = packageName;
486             if (className == null || className.equals("null")) {
487                 mPointingUiClassName = "";
488             } else {
489                 mPointingUiClassName = className;
490             }
491         }
492 
493         return true;
494     }
495 
getPointingUiPackageName()496     @NonNull private String getPointingUiPackageName() {
497         if (!TextUtils.isEmpty(mPointingUiPackageName)) {
498             return mPointingUiPackageName;
499         }
500         return TextUtils.emptyIfNull(mContext.getResources().getString(
501                 R.string.config_pointing_ui_package));
502     }
503 
getPointingUiClassName()504     @NonNull private String getPointingUiClassName() {
505         if (!TextUtils.isEmpty(mPointingUiClassName)) {
506             return mPointingUiClassName;
507         }
508         return TextUtils.emptyIfNull(mContext.getResources().getString(
509                 R.string.config_pointing_ui_class));
510     }
511 
isMockModemAllowed()512     private boolean isMockModemAllowed() {
513         return (DEBUG || SystemProperties.getBoolean(ALLOW_MOCK_MODEM_PROPERTY, false));
514     }
515 
logd(@onNull String log)516     private static void logd(@NonNull String log) {
517         Rlog.d(TAG, log);
518     }
519 
loge(@onNull String log)520     private static void loge(@NonNull String log) {
521         Rlog.e(TAG, log);
522     }
523 
isSatellitePersistentLoggingEnabled( @onNull Context context, @NonNull FeatureFlags featureFlags)524     private boolean isSatellitePersistentLoggingEnabled(
525             @NonNull Context context, @NonNull FeatureFlags featureFlags) {
526         if (featureFlags.satellitePersistentLogging()) {
527             return true;
528         }
529         try {
530             return context.getResources().getBoolean(
531                     R.bool.config_dropboxmanager_persistent_logging_enabled);
532         } catch (RuntimeException e) {
533             return false;
534         }
535     }
536 
plogd(@onNull String log)537     private void plogd(@NonNull String log) {
538         Rlog.d(TAG, log);
539         if (mPersistentLogger != null) {
540             mPersistentLogger.debug(TAG, log);
541         }
542     }
543 
ploge(@onNull String log)544     private void ploge(@NonNull String log) {
545         Rlog.e(TAG, log);
546         if (mPersistentLogger != null) {
547             mPersistentLogger.error(TAG, log);
548         }
549     }
550     /**
551      * TODO: The following needs to be added in this class:
552      * - check if pointingUI crashes - then restart it
553      */
554 }
555