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.telephony.satellite.SatelliteManager.DATAGRAM_TYPE_UNKNOWN;
20 import static android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED;
21 import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_MODEM_TIMEOUT;
22 import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_NOT_REACHABLE;
23 import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS;
24 
25 import static com.android.internal.telephony.satellite.DatagramController.ROUNDING_UNIT;
26 
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.content.Context;
30 import android.content.res.Resources;
31 import android.os.AsyncResult;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.telephony.DropBoxManagerLoggerBackend;
36 import android.telephony.PersistentLogger;
37 import android.telephony.Rlog;
38 import android.telephony.SubscriptionManager;
39 import android.telephony.satellite.SatelliteDatagram;
40 import android.telephony.satellite.SatelliteManager;
41 
42 import com.android.internal.R;
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.telephony.Phone;
46 import com.android.internal.telephony.flags.FeatureFlags;
47 import com.android.internal.telephony.metrics.SatelliteStats;
48 import com.android.internal.telephony.satellite.metrics.ControllerMetricsStats;
49 import com.android.internal.telephony.satellite.metrics.SessionMetricsStats;
50 
51 import java.util.LinkedHashMap;
52 import java.util.Map.Entry;
53 import java.util.Set;
54 import java.util.concurrent.TimeUnit;
55 import java.util.concurrent.atomic.AtomicBoolean;
56 import java.util.concurrent.atomic.AtomicLong;
57 import java.util.function.Consumer;
58 
59 /**
60  * Datagram dispatcher used to send satellite datagrams.
61  */
62 public class DatagramDispatcher extends Handler {
63     private static final String TAG = "DatagramDispatcher";
64 
65     private static final int CMD_SEND_SATELLITE_DATAGRAM = 1;
66     private static final int EVENT_SEND_SATELLITE_DATAGRAM_DONE = 2;
67     private static final int EVENT_WAIT_FOR_DEVICE_ALIGNMENT_IN_DEMO_MODE_TIMED_OUT = 3;
68     private static final int EVENT_DATAGRAM_WAIT_FOR_CONNECTED_STATE_TIMED_OUT = 4;
69     private static final int EVENT_WAIT_FOR_DATAGRAM_SENDING_RESPONSE_TIMED_OUT = 5;
70     private static final int EVENT_ABORT_SENDING_SATELLITE_DATAGRAMS_DONE = 6;
71     private static final int EVENT_WAIT_FOR_SIMULATED_POLL_DATAGRAMS_DELAY_TIMED_OUT = 7;
72     private static final Long TIMEOUT_DATAGRAM_DELAY_IN_DEMO_MODE = TimeUnit.SECONDS.toMillis(10);
73     @NonNull private static DatagramDispatcher sInstance;
74     @NonNull private final Context mContext;
75     @NonNull private final DatagramController mDatagramController;
76     @NonNull private final ControllerMetricsStats mControllerMetricsStats;
77     @NonNull private final SessionMetricsStats mSessionMetricsStats;
78     @NonNull private final FeatureFlags mFeatureFlags;
79 
80     private boolean mIsDemoMode = false;
81     private boolean mIsAligned = false;
82     private DatagramDispatcherHandlerRequest mSendSatelliteDatagramRequest = null;
83 
84     private static AtomicLong mNextDatagramId = new AtomicLong(0);
85 
86     private AtomicBoolean mShouldSendDatagramToModemInDemoMode = null;
87 
88     private final Object mLock = new Object();
89     private long mDemoTimeoutDuration = TIMEOUT_DATAGRAM_DELAY_IN_DEMO_MODE;
90 
91     @GuardedBy("mLock")
92     private boolean mSendingDatagramInProgress;
93 
94     /**
95      * Map key: datagramId, value: SendSatelliteDatagramArgument to retry sending emergency
96      * datagrams.
97      */
98     @GuardedBy("mLock")
99     private final LinkedHashMap<Long, SendSatelliteDatagramArgument>
100             mPendingEmergencyDatagramsMap = new LinkedHashMap<>();
101 
102     /**
103      * Map key: datagramId, value: SendSatelliteDatagramArgument to retry sending non-emergency
104      * datagrams.
105      */
106     @GuardedBy("mLock")
107     private final LinkedHashMap<Long, SendSatelliteDatagramArgument>
108             mPendingNonEmergencyDatagramsMap = new LinkedHashMap<>();
109 
110     private long mWaitTimeForDatagramSendingResponse;
111     private long mWaitTimeForDatagramSendingForLastMessageResponse;
112     @SatelliteManager.DatagramType
113     private int mLastSendRequestDatagramType = DATAGRAM_TYPE_UNKNOWN;
114     @Nullable private PersistentLogger mPersistentLogger = null;
115 
116     /**
117      * Create the DatagramDispatcher singleton instance.
118      * @param context The Context to use to create the DatagramDispatcher.
119      * @param looper The looper for the handler.
120      * @param featureFlags The telephony feature flags.
121      * @param datagramController DatagramController which is used to update datagram transfer state.
122      * @return The singleton instance of DatagramDispatcher.
123      */
make(@onNull Context context, @NonNull Looper looper, @NonNull FeatureFlags featureFlags, @NonNull DatagramController datagramController)124     public static DatagramDispatcher make(@NonNull Context context, @NonNull Looper looper,
125             @NonNull FeatureFlags featureFlags,
126             @NonNull DatagramController datagramController) {
127         if (sInstance == null) {
128             sInstance = new DatagramDispatcher(context, looper, featureFlags, datagramController);
129         }
130         return sInstance;
131     }
132 
133     /**
134      * @return The singleton instance of DatagramDispatcher.
135      */
getInstance()136     public static DatagramDispatcher getInstance() {
137         if (sInstance == null) {
138             loge("DatagramDispatcher was not yet initialized.");
139         }
140         return sInstance;
141     }
142 
143     /**
144      * Create a DatagramDispatcher to send satellite datagrams.
145      *
146      * @param context The Context for the DatagramDispatcher.
147      * @param looper The looper for the handler.
148      * @param featureFlags The telephony feature flags.
149      * @param datagramController DatagramController which is used to update datagram transfer state.
150      */
151     @VisibleForTesting
DatagramDispatcher(@onNull Context context, @NonNull Looper looper, @NonNull FeatureFlags featureFlags, @NonNull DatagramController datagramController)152     protected DatagramDispatcher(@NonNull Context context, @NonNull Looper looper,
153             @NonNull FeatureFlags featureFlags,
154             @NonNull DatagramController datagramController) {
155         super(looper);
156         mContext = context;
157         mFeatureFlags = featureFlags;
158         mDatagramController = datagramController;
159         mControllerMetricsStats = ControllerMetricsStats.getInstance();
160         mSessionMetricsStats = SessionMetricsStats.getInstance();
161         if (isSatellitePersistentLoggingEnabled(context, featureFlags)) {
162             mPersistentLogger = new PersistentLogger(
163                     DropBoxManagerLoggerBackend.getInstance(context));
164         }
165 
166         synchronized (mLock) {
167             mSendingDatagramInProgress = false;
168         }
169         mWaitTimeForDatagramSendingResponse = getWaitForDatagramSendingResponseTimeoutMillis();
170         mWaitTimeForDatagramSendingForLastMessageResponse =
171                 getWaitForDatagramSendingResponseForLastMessageTimeoutMillis();
172     }
173 
174     private static final class DatagramDispatcherHandlerRequest {
175         /** The argument to use for the request */
176         public @NonNull Object argument;
177         /** The caller needs to specify the phone to be used for the request */
178         public @NonNull Phone phone;
179         /** The result of the request that is run on the main thread */
180         public @Nullable Object result;
181 
DatagramDispatcherHandlerRequest(Object argument, Phone phone)182         DatagramDispatcherHandlerRequest(Object argument, Phone phone) {
183             this.argument = argument;
184             this.phone = phone;
185         }
186     }
187 
188     private static final class SendSatelliteDatagramArgument {
189         public int subId;
190         public long datagramId;
191         public @SatelliteManager.DatagramType int datagramType;
192         public @NonNull SatelliteDatagram datagram;
193         public boolean needFullScreenPointingUI;
194         public @NonNull Consumer<Integer> callback;
195         public long datagramStartTime;
196         public boolean skipCheckingSatelliteAligned = false;
197 
SendSatelliteDatagramArgument(int subId, long datagramId, @SatelliteManager.DatagramType int datagramType, @NonNull SatelliteDatagram datagram, boolean needFullScreenPointingUI, @NonNull Consumer<Integer> callback)198         SendSatelliteDatagramArgument(int subId, long datagramId,
199                 @SatelliteManager.DatagramType int datagramType,
200                 @NonNull SatelliteDatagram datagram, boolean needFullScreenPointingUI,
201                 @NonNull Consumer<Integer> callback) {
202             this.subId = subId;
203             this.datagramId = datagramId;
204             this.datagramType = datagramType;
205             this.datagram = datagram;
206             this.needFullScreenPointingUI = needFullScreenPointingUI;
207             this.callback = callback;
208         }
209 
210         /** returns the size of outgoing SMS, rounded by 10 bytes */
getDatagramRoundedSizeBytes()211         public int getDatagramRoundedSizeBytes() {
212             if (datagram.getSatelliteDatagram() != null) {
213                 int sizeBytes = datagram.getSatelliteDatagram().length;
214                 // rounded by ROUNDING_UNIT
215                 return (int) (Math.round((double) sizeBytes / ROUNDING_UNIT) * ROUNDING_UNIT);
216             } else {
217                 return 0;
218             }
219         }
220 
221         /** sets the start time at datagram is sent out */
setDatagramStartTime()222         public void setDatagramStartTime() {
223             datagramStartTime =
224                     datagramStartTime == 0 ? System.currentTimeMillis() : datagramStartTime;
225         }
226     }
227 
228     @Override
handleMessage(Message msg)229     public void handleMessage(Message msg) {
230         DatagramDispatcherHandlerRequest request;
231         Message onCompleted;
232         AsyncResult ar;
233 
234         switch(msg.what) {
235             case CMD_SEND_SATELLITE_DATAGRAM: {
236                 plogd("CMD_SEND_SATELLITE_DATAGRAM mIsDemoMode=" + mIsDemoMode
237                         + ", shouldSendDatagramToModemInDemoMode="
238                         + shouldSendDatagramToModemInDemoMode());
239                 request = (DatagramDispatcherHandlerRequest) msg.obj;
240                 SendSatelliteDatagramArgument argument =
241                         (SendSatelliteDatagramArgument) request.argument;
242                 onCompleted = obtainMessage(EVENT_SEND_SATELLITE_DATAGRAM_DONE, request);
243 
244                 synchronized (mLock) {
245                     if (mIsDemoMode && !shouldSendDatagramToModemInDemoMode()) {
246                         AsyncResult.forMessage(onCompleted, SATELLITE_RESULT_SUCCESS, null);
247                         sendMessageDelayed(onCompleted, getDemoTimeoutDuration());
248                     } else {
249                         SatelliteModemInterface.getInstance().sendSatelliteDatagram(
250                                 argument.datagram,
251                                 SatelliteServiceUtils.isSosMessage(argument.datagramType),
252                                 argument.needFullScreenPointingUI, onCompleted);
253                         startWaitForDatagramSendingResponseTimer(argument);
254                     }
255                 }
256                 break;
257             }
258             case EVENT_SEND_SATELLITE_DATAGRAM_DONE: {
259                 ar = (AsyncResult) msg.obj;
260                 request = (DatagramDispatcherHandlerRequest) ar.userObj;
261                 int error = SatelliteServiceUtils.getSatelliteError(ar, "sendDatagram");
262                 SendSatelliteDatagramArgument argument =
263                         (SendSatelliteDatagramArgument) request.argument;
264 
265                 synchronized (mLock) {
266                     if (mIsDemoMode && (error == SatelliteManager.SATELLITE_RESULT_SUCCESS)) {
267                         if (argument.skipCheckingSatelliteAligned) {
268                             plogd("Satellite was already aligned. "
269                                 + "No need to check alignment again");
270                         } else if (mDatagramController.waitForAligningToSatellite(mIsAligned)) {
271                             plogd("Satellite is not aligned in demo mode, wait for the alignment.");
272                             startSatelliteAlignedTimer(request);
273                             break;
274                         }
275                     }
276                     plogd("EVENT_SEND_SATELLITE_DATAGRAM_DONE error: " + error
277                             + ", mIsDemoMode=" + mIsDemoMode);
278 
279                     /*
280                      * The response should be ignored if either of the following hold
281                      * 1) Framework has already received this response from the vendor service.
282                      * 2) Framework has timed out to wait for the response from vendor service for
283                      *    the send request.
284                      * 3) All pending send requests have been aborted due to some error.
285                      */
286                     if (!shouldProcessEventSendSatelliteDatagramDone(argument)) {
287                         plogw("The message " + argument.datagramId + " was already processed");
288                         break;
289                     }
290 
291                     stopWaitForDatagramSendingResponseTimer();
292                     mSendingDatagramInProgress = false;
293 
294                     // Log metrics about the outgoing datagram
295                     reportSendDatagramCompleted(argument, error);
296                     // Remove current datagram from pending map.
297                     if (SatelliteServiceUtils.isSosMessage(argument.datagramType)) {
298                         mPendingEmergencyDatagramsMap.remove(argument.datagramId);
299                     } else {
300                         mPendingNonEmergencyDatagramsMap.remove(argument.datagramId);
301                     }
302 
303                     if (error == SATELLITE_RESULT_SUCCESS) {
304                         // Update send status for current datagram
305                         mDatagramController.updateSendStatus(argument.subId, argument.datagramType,
306                                 SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_SUCCESS,
307                                 getPendingDatagramCount(), error);
308                         startWaitForSimulatedPollDatagramsDelayTimer(request);
309                     } else {
310                         // Update send status
311                         mDatagramController.updateSendStatus(argument.subId, argument.datagramType,
312                                 SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED,
313                                 getPendingDatagramCount(), error);
314                     }
315 
316                     if (getPendingDatagramCount() > 0) {
317                         // Send response for current datagram
318                         argument.callback.accept(error);
319                         // Send pending datagrams
320                         sendPendingDatagrams();
321                     } else {
322                         mDatagramController.updateSendStatus(argument.subId,
323                                 argument.datagramType,
324                                 SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE, 0,
325                                 SatelliteManager.SATELLITE_RESULT_SUCCESS);
326                         // Send response for current datagram
327                         argument.callback.accept(error);
328                     }
329                 }
330                 break;
331             }
332 
333             case EVENT_WAIT_FOR_DATAGRAM_SENDING_RESPONSE_TIMED_OUT:
334                 handleEventWaitForDatagramSendingResponseTimedOut(
335                         (SendSatelliteDatagramArgument) msg.obj);
336                 break;
337 
338             case EVENT_WAIT_FOR_DEVICE_ALIGNMENT_IN_DEMO_MODE_TIMED_OUT: {
339                 handleEventSatelliteAlignedTimeout((DatagramDispatcherHandlerRequest) msg.obj);
340                 break;
341             }
342 
343             case EVENT_DATAGRAM_WAIT_FOR_CONNECTED_STATE_TIMED_OUT:
344                 handleEventDatagramWaitForConnectedStateTimedOut(
345                         (SendSatelliteDatagramArgument) msg.obj);
346                 break;
347 
348             case EVENT_WAIT_FOR_SIMULATED_POLL_DATAGRAMS_DELAY_TIMED_OUT:
349                 request = (DatagramDispatcherHandlerRequest) msg.obj;
350                 handleEventWaitForSimulatedPollDatagramsDelayTimedOut(
351                         (SendSatelliteDatagramArgument) request.argument);
352                 break;
353 
354             default:
355                 plogw("DatagramDispatcherHandler: unexpected message code: " + msg.what);
356                 break;
357         }
358     }
359 
360     /**
361      * Send datagram over satellite.
362      *
363      * Gateway encodes SOS message or location sharing message into a datagram and passes it as
364      * input to this method. Datagram received here will be passed down to modem without any
365      * encoding or encryption.
366      *
367      * @param subId The subId of the subscription to send satellite datagrams for.
368      * @param datagramType datagram type indicating whether the datagram is of type
369      *                     SOS_SMS or LOCATION_SHARING.
370      * @param datagram encoded gateway datagram which is encrypted by the caller.
371      *                 Datagram will be passed down to modem without any encoding or encryption.
372      * @param needFullScreenPointingUI this is used to indicate pointingUI app to open in
373      *                                 full screen mode.
374      * @param callback The callback to get {@link SatelliteManager.SatelliteResult} of the request.
375      */
sendSatelliteDatagram(int subId, @SatelliteManager.DatagramType int datagramType, @NonNull SatelliteDatagram datagram, boolean needFullScreenPointingUI, @NonNull Consumer<Integer> callback)376     public void sendSatelliteDatagram(int subId, @SatelliteManager.DatagramType int datagramType,
377             @NonNull SatelliteDatagram datagram, boolean needFullScreenPointingUI,
378             @NonNull Consumer<Integer> callback) {
379         Phone phone = SatelliteServiceUtils.getPhone();
380 
381         long datagramId = mNextDatagramId.getAndUpdate(
382                 n -> ((n + 1) % DatagramController.MAX_DATAGRAM_ID));
383         SendSatelliteDatagramArgument datagramArgs =
384                 new SendSatelliteDatagramArgument(subId, datagramId, datagramType, datagram,
385                         needFullScreenPointingUI, callback);
386         mLastSendRequestDatagramType = datagramType;
387 
388         synchronized (mLock) {
389             // Add datagram to pending datagram map
390             if (SatelliteServiceUtils.isSosMessage(datagramType)) {
391                 mPendingEmergencyDatagramsMap.put(datagramId, datagramArgs);
392             } else {
393                 mPendingNonEmergencyDatagramsMap.put(datagramId, datagramArgs);
394             }
395 
396             if (mDatagramController.needsWaitingForSatelliteConnected(datagramType)) {
397                 plogd("sendDatagram: wait for satellite connected");
398                 mDatagramController.updateSendStatus(subId, datagramType,
399                         SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_WAITING_TO_CONNECT,
400                         getPendingDatagramCount(), SatelliteManager.SATELLITE_RESULT_SUCCESS);
401                 startDatagramWaitForConnectedStateTimer(datagramArgs);
402             } else if (!mSendingDatagramInProgress && mDatagramController.isPollingInIdleState()) {
403                 // Modem can be busy receiving datagrams, so send datagram only when modem is
404                 // not busy.
405                 mSendingDatagramInProgress = true;
406                 datagramArgs.setDatagramStartTime();
407                 mDatagramController.updateSendStatus(subId, datagramType,
408                         SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_SENDING,
409                         getPendingDatagramCount(), SatelliteManager.SATELLITE_RESULT_SUCCESS);
410                 sendRequestAsync(CMD_SEND_SATELLITE_DATAGRAM, datagramArgs, phone);
411             } else {
412                 plogd("sendDatagram: mSendingDatagramInProgress="
413                         + mSendingDatagramInProgress + ", isPollingInIdleState="
414                         + mDatagramController.isPollingInIdleState());
415             }
416         }
417     }
418 
retrySendingDatagrams()419     public void retrySendingDatagrams() {
420         synchronized (mLock) {
421             sendPendingDatagrams();
422         }
423     }
424 
425     /** Set demo mode
426      *
427      * @param isDemoMode {@code true} means demo mode is on, {@code false} otherwise.
428      */
429     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
setDemoMode(boolean isDemoMode)430     protected void setDemoMode(boolean isDemoMode) {
431         mIsDemoMode = isDemoMode;
432         plogd("setDemoMode: mIsDemoMode=" + mIsDemoMode);
433     }
434 
435     /**
436      * Set whether the device is aligned with the satellite.
437      */
438     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
setDeviceAlignedWithSatellite(boolean isAligned)439     public void setDeviceAlignedWithSatellite(boolean isAligned) {
440         synchronized (mLock) {
441             mIsAligned = isAligned;
442             plogd("setDeviceAlignedWithSatellite: " + mIsAligned);
443             if (isAligned && mIsDemoMode) handleEventSatelliteAligned();
444         }
445     }
446 
startSatelliteAlignedTimer(@onNull DatagramDispatcherHandlerRequest request)447     private void startSatelliteAlignedTimer(@NonNull DatagramDispatcherHandlerRequest request) {
448         if (isSatelliteAlignedTimerStarted()) {
449             plogd("Satellite aligned timer was already started");
450             return;
451         }
452         mSendSatelliteDatagramRequest = request;
453         sendMessageDelayed(
454                 obtainMessage(EVENT_WAIT_FOR_DEVICE_ALIGNMENT_IN_DEMO_MODE_TIMED_OUT, request),
455                 getSatelliteAlignedTimeoutDuration());
456     }
457 
458     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
getSatelliteAlignedTimeoutDuration()459     protected long getSatelliteAlignedTimeoutDuration() {
460         return mDatagramController.getSatelliteAlignedTimeoutDuration();
461     }
462 
handleEventSatelliteAligned()463     private void handleEventSatelliteAligned() {
464         if (isSatelliteAlignedTimerStarted()) {
465             stopSatelliteAlignedTimer();
466 
467             if (mSendSatelliteDatagramRequest == null) {
468                 ploge("handleEventSatelliteAligned: mSendSatelliteDatagramRequest is null");
469             } else {
470                 SendSatelliteDatagramArgument argument =
471                         (SendSatelliteDatagramArgument) mSendSatelliteDatagramRequest.argument;
472                 argument.skipCheckingSatelliteAligned = true;
473                 Message message = obtainMessage(
474                         EVENT_SEND_SATELLITE_DATAGRAM_DONE, mSendSatelliteDatagramRequest);
475                 mSendSatelliteDatagramRequest = null;
476                 AsyncResult.forMessage(message, null, null);
477                 message.sendToTarget();
478                 plogd("handleEventSatelliteAligned: EVENT_SEND_SATELLITE_DATAGRAM_DONE");
479             }
480         }
481     }
482 
handleEventSatelliteAlignedTimeout( @onNull DatagramDispatcherHandlerRequest request)483     private void handleEventSatelliteAlignedTimeout(
484             @NonNull DatagramDispatcherHandlerRequest request) {
485         plogd("handleEventSatelliteAlignedTimeout");
486         mSendSatelliteDatagramRequest = null;
487         SatelliteManager.SatelliteException exception =
488                 new SatelliteManager.SatelliteException(
489                         SATELLITE_RESULT_NOT_REACHABLE);
490         Message message = obtainMessage(EVENT_SEND_SATELLITE_DATAGRAM_DONE, request);
491         AsyncResult.forMessage(message, null, exception);
492         message.sendToTarget();
493     }
494 
isSatelliteAlignedTimerStarted()495     private boolean isSatelliteAlignedTimerStarted() {
496         return hasMessages(EVENT_WAIT_FOR_DEVICE_ALIGNMENT_IN_DEMO_MODE_TIMED_OUT);
497     }
498 
stopSatelliteAlignedTimer()499     private void stopSatelliteAlignedTimer() {
500         removeMessages(EVENT_WAIT_FOR_DEVICE_ALIGNMENT_IN_DEMO_MODE_TIMED_OUT);
501     }
502 
503     /**
504      * Send pending satellite datagrams. Emergency datagrams are given priority over
505      * non-emergency datagrams.
506      */
507     @GuardedBy("mLock")
sendPendingDatagrams()508     private void sendPendingDatagrams() {
509         plogd("sendPendingDatagrams()");
510         if (!mDatagramController.isPollingInIdleState()) {
511             // Datagram should be sent to satellite modem when modem is free.
512             plogd("sendPendingDatagrams: modem is receiving datagrams");
513             return;
514         }
515 
516         if (getPendingDatagramCount() <= 0) {
517             plogd("sendPendingDatagrams: no pending datagrams to send");
518             return;
519         }
520 
521         Phone phone = SatelliteServiceUtils.getPhone();
522         Set<Entry<Long, SendSatelliteDatagramArgument>> pendingDatagram = null;
523         if (!mSendingDatagramInProgress && !mPendingEmergencyDatagramsMap.isEmpty()) {
524             pendingDatagram = mPendingEmergencyDatagramsMap.entrySet();
525         } else if (!mSendingDatagramInProgress && !mPendingNonEmergencyDatagramsMap.isEmpty()) {
526             pendingDatagram = mPendingNonEmergencyDatagramsMap.entrySet();
527         }
528 
529         if ((pendingDatagram != null) && pendingDatagram.iterator().hasNext()) {
530             SendSatelliteDatagramArgument datagramArg =
531                     pendingDatagram.iterator().next().getValue();
532             if (mDatagramController.needsWaitingForSatelliteConnected(datagramArg.datagramType)) {
533                 plogd("sendPendingDatagrams: wait for satellite connected");
534                 return;
535             }
536 
537             mSendingDatagramInProgress = true;
538             // Sets the trigger time for getting pending datagrams
539             datagramArg.setDatagramStartTime();
540             mDatagramController.updateSendStatus(datagramArg.subId, datagramArg.datagramType,
541                     SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_SENDING,
542                     getPendingDatagramCount(), SatelliteManager.SATELLITE_RESULT_SUCCESS);
543             sendRequestAsync(CMD_SEND_SATELLITE_DATAGRAM, datagramArg, phone);
544         }
545     }
546 
547     /**
548      * Send error code to all the pending datagrams
549      *
550      * @param pendingDatagramsMap The pending datagrams map to be cleaned up.
551      * @param errorCode error code to be returned.
552      */
553     @GuardedBy("mLock")
sendErrorCodeAndCleanupPendingDatagrams( LinkedHashMap<Long, SendSatelliteDatagramArgument> pendingDatagramsMap, @SatelliteManager.SatelliteResult int errorCode)554     private void sendErrorCodeAndCleanupPendingDatagrams(
555             LinkedHashMap<Long, SendSatelliteDatagramArgument> pendingDatagramsMap,
556             @SatelliteManager.SatelliteResult int errorCode) {
557         if (pendingDatagramsMap.size() == 0) {
558             return;
559         }
560         ploge("sendErrorCodeAndCleanupPendingDatagrams: cleaning up resources");
561 
562         // Send error code to all the pending datagrams
563         for (Entry<Long, SendSatelliteDatagramArgument> entry :
564                 pendingDatagramsMap.entrySet()) {
565             SendSatelliteDatagramArgument argument = entry.getValue();
566             reportSendDatagramCompleted(argument, errorCode);
567             argument.callback.accept(errorCode);
568         }
569 
570         // Clear pending datagram maps
571         pendingDatagramsMap.clear();
572     }
573 
574     /**
575      * Abort sending all the pending datagrams.
576      *
577      * @param subId The subId of the subscription used to send datagram
578      * @param errorCode The error code that resulted in abort.
579      */
580     @GuardedBy("mLock")
abortSendingPendingDatagrams(int subId, @SatelliteManager.SatelliteResult int errorCode)581     private void abortSendingPendingDatagrams(int subId,
582             @SatelliteManager.SatelliteResult int errorCode) {
583         plogd("abortSendingPendingDatagrams()");
584         sendErrorCodeAndCleanupPendingDatagrams(mPendingEmergencyDatagramsMap, errorCode);
585         sendErrorCodeAndCleanupPendingDatagrams(mPendingNonEmergencyDatagramsMap, errorCode);
586     }
587 
588     /**
589      * Return pending datagram count
590      * @return pending datagram count
591      */
getPendingDatagramCount()592     public int getPendingDatagramCount() {
593         synchronized (mLock) {
594             return mPendingEmergencyDatagramsMap.size() + mPendingNonEmergencyDatagramsMap.size();
595         }
596     }
597 
598     /** Return pending user messages count */
getPendingUserMessagesCount()599     public int getPendingUserMessagesCount() {
600         synchronized (mLock) {
601             int pendingUserMessagesCount = 0;
602             for (Entry<Long, SendSatelliteDatagramArgument> entry :
603                     mPendingNonEmergencyDatagramsMap.entrySet()) {
604                 SendSatelliteDatagramArgument argument = entry.getValue();
605                 if (argument.datagramType != SatelliteManager.DATAGRAM_TYPE_KEEP_ALIVE) {
606                     pendingUserMessagesCount += 1;
607                 }
608             }
609             pendingUserMessagesCount += mPendingEmergencyDatagramsMap.size();
610             return pendingUserMessagesCount;
611         }
612     }
613 
614     /**
615      * Posts the specified command to be executed on the main thread and returns immediately.
616      *
617      * @param command command to be executed on the main thread
618      * @param argument additional parameters required to perform of the operation
619      * @param phone phone object used to perform the operation.
620      */
sendRequestAsync(int command, @NonNull Object argument, @Nullable Phone phone)621     private void sendRequestAsync(int command, @NonNull Object argument, @Nullable Phone phone) {
622         DatagramDispatcherHandlerRequest request = new DatagramDispatcherHandlerRequest(
623                 argument, phone);
624         Message msg = this.obtainMessage(command, request);
625         msg.sendToTarget();
626     }
627 
reportSendDatagramCompleted(@onNull SendSatelliteDatagramArgument argument, @NonNull @SatelliteManager.SatelliteResult int resultCode)628     private void reportSendDatagramCompleted(@NonNull SendSatelliteDatagramArgument argument,
629             @NonNull @SatelliteManager.SatelliteResult int resultCode) {
630         SatelliteStats.getInstance().onSatelliteOutgoingDatagramMetrics(
631                 new SatelliteStats.SatelliteOutgoingDatagramParams.Builder()
632                         .setDatagramType(argument.datagramType)
633                         .setResultCode(resultCode)
634                         .setDatagramSizeBytes(argument.getDatagramRoundedSizeBytes())
635                         /* In case pending datagram has not been attempted to send to modem
636                         interface. transfer time will be 0. */
637                         .setDatagramTransferTimeMillis(argument.datagramStartTime > 0
638                                 ? (System.currentTimeMillis() - argument.datagramStartTime) : 0)
639                         .setIsDemoMode(mIsDemoMode)
640                         .build());
641         if (resultCode == SatelliteManager.SATELLITE_RESULT_SUCCESS) {
642             mControllerMetricsStats.reportOutgoingDatagramSuccessCount(argument.datagramType,
643                     mIsDemoMode);
644             mSessionMetricsStats.addCountOfSuccessfulOutgoingDatagram(argument.datagramType);
645         } else {
646             mControllerMetricsStats.reportOutgoingDatagramFailCount(argument.datagramType,
647                     mIsDemoMode);
648             mSessionMetricsStats.addCountOfFailedOutgoingDatagram(argument.datagramType,
649                     resultCode);
650         }
651     }
652 
653     /**
654      * Destroys this DatagramDispatcher. Used for tearing down static resources during testing.
655      */
656     @VisibleForTesting
destroy()657     public void destroy() {
658         sInstance = null;
659     }
660 
661     /**
662      * This function is used by {@link DatagramController} to notify {@link DatagramDispatcher}
663      * that satellite modem state has changed.
664      *
665      * @param state Current satellite modem state.
666      */
onSatelliteModemStateChanged(@atelliteManager.SatelliteModemState int state)667     public void onSatelliteModemStateChanged(@SatelliteManager.SatelliteModemState int state) {
668         synchronized (mLock) {
669             if (state == SatelliteManager.SATELLITE_MODEM_STATE_OFF
670                     || state == SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE) {
671                 plogd("onSatelliteModemStateChanged: cleaning up resources");
672                 cleanUpResources();
673             } else if (state == SatelliteManager.SATELLITE_MODEM_STATE_IDLE) {
674                 sendPendingDatagrams();
675             }
676 
677             if (state == SATELLITE_MODEM_STATE_CONNECTED
678                     && isDatagramWaitForConnectedStateTimerStarted()) {
679                 stopDatagramWaitForConnectedStateTimer();
680                 sendPendingDatagrams();
681             }
682         }
683     }
684 
685     @GuardedBy("mLock")
cleanUpResources()686     private void cleanUpResources() {
687         plogd("cleanUpResources");
688         mSendingDatagramInProgress = false;
689         if (getPendingDatagramCount() > 0) {
690             mDatagramController.updateSendStatus(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
691                     mLastSendRequestDatagramType,
692                     SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED,
693                     getPendingDatagramCount(), SatelliteManager.SATELLITE_RESULT_REQUEST_ABORTED);
694         }
695         mDatagramController.updateSendStatus(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
696                 mLastSendRequestDatagramType,
697                 SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE,
698                 0, SatelliteManager.SATELLITE_RESULT_SUCCESS);
699         abortSendingPendingDatagrams(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
700                 SatelliteManager.SATELLITE_RESULT_REQUEST_ABORTED);
701 
702         stopSatelliteAlignedTimer();
703         stopDatagramWaitForConnectedStateTimer();
704         stopWaitForDatagramSendingResponseTimer();
705         stopWaitForSimulatedPollDatagramsDelayTimer();
706         mIsDemoMode = false;
707         mSendSatelliteDatagramRequest = null;
708         mIsAligned = false;
709         mLastSendRequestDatagramType = DATAGRAM_TYPE_UNKNOWN;
710     }
711 
startDatagramWaitForConnectedStateTimer( @onNull SendSatelliteDatagramArgument datagramArgs)712     private void startDatagramWaitForConnectedStateTimer(
713             @NonNull SendSatelliteDatagramArgument datagramArgs) {
714         if (isDatagramWaitForConnectedStateTimerStarted()) {
715             plogd("DatagramWaitForConnectedStateTimer is already started");
716             return;
717         }
718         sendMessageDelayed(obtainMessage(
719                         EVENT_DATAGRAM_WAIT_FOR_CONNECTED_STATE_TIMED_OUT, datagramArgs),
720                 mDatagramController.getDatagramWaitTimeForConnectedState(
721                         SatelliteServiceUtils.isLastSosMessage(datagramArgs.datagramType)));
722     }
723 
stopDatagramWaitForConnectedStateTimer()724     private void stopDatagramWaitForConnectedStateTimer() {
725         removeMessages(EVENT_DATAGRAM_WAIT_FOR_CONNECTED_STATE_TIMED_OUT);
726     }
727 
728     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
isDatagramWaitForConnectedStateTimerStarted()729     public boolean isDatagramWaitForConnectedStateTimerStarted() {
730         return hasMessages(EVENT_DATAGRAM_WAIT_FOR_CONNECTED_STATE_TIMED_OUT);
731     }
732 
733     /**
734      * This API is used by CTS tests to override the mWaitTimeForDatagramSendingResponse.
735      */
setWaitTimeForDatagramSendingResponse(boolean reset, long timeoutMillis)736     void setWaitTimeForDatagramSendingResponse(boolean reset, long timeoutMillis) {
737         if (reset) {
738             mWaitTimeForDatagramSendingResponse = getWaitForDatagramSendingResponseTimeoutMillis();
739         } else {
740             mWaitTimeForDatagramSendingResponse = timeoutMillis;
741         }
742     }
743 
startWaitForDatagramSendingResponseTimer( @onNull SendSatelliteDatagramArgument argument)744     private void startWaitForDatagramSendingResponseTimer(
745             @NonNull SendSatelliteDatagramArgument argument) {
746         if (hasMessages(EVENT_WAIT_FOR_DATAGRAM_SENDING_RESPONSE_TIMED_OUT)) {
747             plogd("WaitForDatagramSendingResponseTimer was already started");
748             return;
749         }
750         long waitTime = SatelliteServiceUtils.isLastSosMessage(argument.datagramType)
751                 ? mWaitTimeForDatagramSendingForLastMessageResponse
752                 : mWaitTimeForDatagramSendingResponse;
753         logd("startWaitForDatagramSendingResponseTimer: datagramType=" + argument.datagramType
754                 + ", waitTime=" + waitTime);
755         sendMessageDelayed(obtainMessage(
756                 EVENT_WAIT_FOR_DATAGRAM_SENDING_RESPONSE_TIMED_OUT, argument), waitTime);
757     }
758 
stopWaitForDatagramSendingResponseTimer()759     private void stopWaitForDatagramSendingResponseTimer() {
760         removeMessages(EVENT_WAIT_FOR_DATAGRAM_SENDING_RESPONSE_TIMED_OUT);
761     }
762 
handleEventDatagramWaitForConnectedStateTimedOut( @onNull SendSatelliteDatagramArgument argument)763     private void handleEventDatagramWaitForConnectedStateTimedOut(
764             @NonNull SendSatelliteDatagramArgument argument) {
765         plogw("Timed out to wait for satellite connected before sending datagrams");
766         synchronized (mLock) {
767             // Update send status
768             mDatagramController.updateSendStatus(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
769                     argument.datagramType,
770                     SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED,
771                     getPendingDatagramCount(),
772                     SATELLITE_RESULT_NOT_REACHABLE);
773             mDatagramController.updateSendStatus(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
774                     argument.datagramType,
775                     SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE,
776                     0, SatelliteManager.SATELLITE_RESULT_SUCCESS);
777             abortSendingPendingDatagrams(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
778                     SATELLITE_RESULT_NOT_REACHABLE);
779         }
780     }
781 
shouldSendDatagramToModemInDemoMode()782     private boolean shouldSendDatagramToModemInDemoMode() {
783         if (mShouldSendDatagramToModemInDemoMode != null) {
784             return mShouldSendDatagramToModemInDemoMode.get();
785         }
786 
787         try {
788             mShouldSendDatagramToModemInDemoMode = new AtomicBoolean(
789                     mContext.getResources().getBoolean(
790                             R.bool.config_send_satellite_datagram_to_modem_in_demo_mode));
791             return mShouldSendDatagramToModemInDemoMode.get();
792 
793         } catch (Resources.NotFoundException ex) {
794             ploge("shouldSendDatagramToModemInDemoMode: id= "
795                     + R.bool.config_send_satellite_datagram_to_modem_in_demo_mode + ", ex=" + ex);
796             return false;
797         }
798     }
799 
getWaitForDatagramSendingResponseTimeoutMillis()800     private long getWaitForDatagramSendingResponseTimeoutMillis() {
801         return mContext.getResources().getInteger(
802                 R.integer.config_wait_for_datagram_sending_response_timeout_millis);
803     }
804 
getWaitForDatagramSendingResponseForLastMessageTimeoutMillis()805     private long getWaitForDatagramSendingResponseForLastMessageTimeoutMillis() {
806         return mContext.getResources().getInteger(R.integer
807                 .config_wait_for_datagram_sending_response_for_last_message_timeout_millis);
808     }
809 
shouldProcessEventSendSatelliteDatagramDone( @onNull SendSatelliteDatagramArgument argument)810     private boolean shouldProcessEventSendSatelliteDatagramDone(
811             @NonNull SendSatelliteDatagramArgument argument) {
812         synchronized (mLock) {
813             if (SatelliteServiceUtils.isSosMessage(argument.datagramType)) {
814                 return mPendingEmergencyDatagramsMap.containsKey(argument.datagramId);
815             } else {
816                 return mPendingNonEmergencyDatagramsMap.containsKey(argument.datagramId);
817             }
818         }
819     }
820 
handleEventWaitForDatagramSendingResponseTimedOut( @onNull SendSatelliteDatagramArgument argument)821     private void handleEventWaitForDatagramSendingResponseTimedOut(
822             @NonNull SendSatelliteDatagramArgument argument) {
823         synchronized (mLock) {
824             plogw("Timed out to wait for the response of the request to send the datagram "
825                     + argument.datagramId);
826 
827             // Ask vendor service to abort all datagram-sending requests
828             SatelliteModemInterface.getInstance().abortSendingSatelliteDatagrams(
829                     obtainMessage(EVENT_ABORT_SENDING_SATELLITE_DATAGRAMS_DONE, argument));
830             mSendingDatagramInProgress = false;
831 
832             // Update send status
833             mDatagramController.updateSendStatus(argument.subId, argument.datagramType,
834                     SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED,
835                     getPendingDatagramCount(), SATELLITE_RESULT_MODEM_TIMEOUT);
836             mDatagramController.updateSendStatus(argument.subId, argument.datagramType,
837                     SatelliteManager.SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE,
838                     0, SatelliteManager.SATELLITE_RESULT_SUCCESS);
839 
840             // Send response for current datagram after updating datagram transfer state
841             // internally.
842             argument.callback.accept(SATELLITE_RESULT_MODEM_TIMEOUT);
843 
844             // Log metrics about the outgoing datagram
845             reportSendDatagramCompleted(argument, SATELLITE_RESULT_MODEM_TIMEOUT);
846             // Remove current datagram from pending map.
847             if (SatelliteServiceUtils.isSosMessage(argument.datagramType)) {
848                 mPendingEmergencyDatagramsMap.remove(argument.datagramId);
849             } else {
850                 mPendingNonEmergencyDatagramsMap.remove(argument.datagramId);
851             }
852 
853             // Abort sending all the pending datagrams
854             abortSendingPendingDatagrams(argument.subId, SATELLITE_RESULT_MODEM_TIMEOUT);
855         }
856     }
857 
858     /**
859      * This API can be used by only CTS to override the cached value for the device overlay config
860      * value : config_send_satellite_datagram_to_modem_in_demo_mode, which determines whether
861      * outgoing satellite datagrams should be sent to modem in demo mode.
862      *
863      * @param shouldSendToModemInDemoMode Whether send datagram in demo mode should be sent to
864      * satellite modem or not. If it is null, the cache will be cleared.
865      */
866     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
setShouldSendDatagramToModemInDemoMode( @ullable Boolean shouldSendToModemInDemoMode)867     protected void setShouldSendDatagramToModemInDemoMode(
868             @Nullable Boolean shouldSendToModemInDemoMode) {
869         plogd("setShouldSendDatagramToModemInDemoMode(" + (shouldSendToModemInDemoMode == null
870                 ? "null" : shouldSendToModemInDemoMode) + ")");
871 
872         if (shouldSendToModemInDemoMode == null) {
873             mShouldSendDatagramToModemInDemoMode = null;
874         } else {
875             if (mShouldSendDatagramToModemInDemoMode == null) {
876                 mShouldSendDatagramToModemInDemoMode = new AtomicBoolean(
877                         shouldSendToModemInDemoMode);
878             } else {
879                 mShouldSendDatagramToModemInDemoMode.set(shouldSendToModemInDemoMode);
880             }
881         }
882     }
883 
startWaitForSimulatedPollDatagramsDelayTimer( @onNull DatagramDispatcherHandlerRequest request)884     private void startWaitForSimulatedPollDatagramsDelayTimer(
885             @NonNull DatagramDispatcherHandlerRequest request) {
886         if (mIsDemoMode) {
887             plogd("startWaitForSimulatedPollDatagramsDelayTimer");
888             sendMessageDelayed(
889                     obtainMessage(EVENT_WAIT_FOR_SIMULATED_POLL_DATAGRAMS_DELAY_TIMED_OUT, request),
890                     getDemoTimeoutDuration());
891         } else {
892             plogd("Should not start WaitForSimulatedPollDatagramsDelayTimer in non-demo mode");
893         }
894     }
895 
stopWaitForSimulatedPollDatagramsDelayTimer()896     private void stopWaitForSimulatedPollDatagramsDelayTimer() {
897         removeMessages(EVENT_WAIT_FOR_SIMULATED_POLL_DATAGRAMS_DELAY_TIMED_OUT);
898     }
899 
handleEventWaitForSimulatedPollDatagramsDelayTimedOut( @onNull SendSatelliteDatagramArgument argument)900     private void handleEventWaitForSimulatedPollDatagramsDelayTimedOut(
901             @NonNull SendSatelliteDatagramArgument argument) {
902         if (mIsDemoMode) {
903             plogd("handleEventWaitForSimulatedPollDatagramsDelayTimedOut");
904             mDatagramController.pushDemoModeDatagram(argument.datagramType, argument.datagram);
905             Consumer<Integer> internalCallback = new Consumer<Integer>() {
906                 @Override
907                 public void accept(Integer result) {
908                     plogd("pollPendingSatelliteDatagrams result: " + result);
909                 }
910             };
911             mDatagramController.pollPendingSatelliteDatagrams(argument.subId, internalCallback);
912         } else {
913             plogd("Unexpected EVENT_WAIT_FOR_SIMULATED_POLL_DATAGRAMS_DELAY_TIMED_OUT in "
914                     + "non-demo mode");
915         }
916     }
917 
getDemoTimeoutDuration()918     long getDemoTimeoutDuration() {
919         return mDemoTimeoutDuration;
920     }
921 
922     /**
923      * This API is used by CTS tests to override the mDemoTimeoutDuration.
924      */
setTimeoutDatagramDelayInDemoMode(boolean reset, long timeoutMillis)925     void setTimeoutDatagramDelayInDemoMode(boolean reset, long timeoutMillis) {
926         if (!mIsDemoMode) {
927             return;
928         }
929         if (reset) {
930             mDemoTimeoutDuration = TIMEOUT_DATAGRAM_DELAY_IN_DEMO_MODE;
931         } else {
932             mDemoTimeoutDuration = timeoutMillis;
933         }
934         plogd("setTimeoutDatagramDelayInDemoMode " + mDemoTimeoutDuration + " reset=" + reset);
935     }
936 
logd(@onNull String log)937     private static void logd(@NonNull String log) {
938         Rlog.d(TAG, log);
939     }
940 
loge(@onNull String log)941     private static void loge(@NonNull String log) {
942         Rlog.e(TAG, log);
943     }
944 
logw(@onNull String log)945     private static void logw(@NonNull String log) { Rlog.w(TAG, log); }
946 
isSatellitePersistentLoggingEnabled( @onNull Context context, @NonNull FeatureFlags featureFlags)947     private boolean isSatellitePersistentLoggingEnabled(
948             @NonNull Context context, @NonNull FeatureFlags featureFlags) {
949         if (featureFlags.satellitePersistentLogging()) {
950             return true;
951         }
952         try {
953             return context.getResources().getBoolean(
954                     R.bool.config_dropboxmanager_persistent_logging_enabled);
955         } catch (RuntimeException e) {
956             return false;
957         }
958     }
959 
plogd(@onNull String log)960     private void plogd(@NonNull String log) {
961         Rlog.d(TAG, log);
962         if (mPersistentLogger != null) {
963             mPersistentLogger.debug(TAG, log);
964         }
965     }
966 
plogw(@onNull String log)967     private void plogw(@NonNull String log) {
968         Rlog.w(TAG, log);
969         if (mPersistentLogger != null) {
970             mPersistentLogger.warn(TAG, log);
971         }
972     }
973 
ploge(@onNull String log)974     private void ploge(@NonNull String log) {
975         Rlog.e(TAG, log);
976         if (mPersistentLogger != null) {
977             mPersistentLogger.error(TAG, log);
978         }
979     }
980 }
981