1 /*
2  * Copyright (C) 2019 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 package com.android.car.am;
17 
18 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
19 import static android.os.Process.INVALID_UID;
20 
21 import static com.android.car.CarLog.TAG_AM;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.UserIdInt;
26 import android.app.ActivityManager;
27 import android.app.ActivityManager.StackInfo;
28 import android.app.ActivityOptions;
29 import android.app.IActivityManager;
30 import android.app.IProcessObserver;
31 import android.app.Presentation;
32 import android.app.TaskStackListener;
33 import android.car.hardware.power.CarPowerManager;
34 import android.content.BroadcastReceiver;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.content.pm.ActivityInfo;
40 import android.content.pm.PackageInfo;
41 import android.content.pm.PackageManager;
42 import android.hardware.display.DisplayManager;
43 import android.net.Uri;
44 import android.os.HandlerThread;
45 import android.os.RemoteException;
46 import android.os.SystemClock;
47 import android.os.UserHandle;
48 import android.os.UserManager;
49 import android.util.Log;
50 import android.util.SparseArray;
51 import android.view.Display;
52 
53 import com.android.car.CarLocalServices;
54 import com.android.car.CarServiceBase;
55 import com.android.car.CarServiceUtils;
56 import com.android.car.R;
57 import com.android.car.user.CarUserService;
58 import com.android.internal.annotations.GuardedBy;
59 
60 import java.io.PrintWriter;
61 import java.util.List;
62 
63 /**
64  * Monitors top activity for a display and guarantee activity in fixed mode is re-launched if it has
65  * crashed or gone to background for whatever reason.
66  *
67  * <p>This component also monitors the upddate of the target package and re-launch it once
68  * update is complete.</p>
69  */
70 public final class FixedActivityService implements CarServiceBase {
71 
72     private static final boolean DBG = false;
73 
74     private static final long RECHECK_INTERVAL_MS = 500;
75     private static final int MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY = 5;
76     // If process keep running without crashing, will reset consecutive crash counts.
77     private static final long CRASH_FORGET_INTERVAL_MS = 2 * 60 * 1000; // 2 mins
78 
79     private static class RunningActivityInfo {
80         @NonNull
81         public final Intent intent;
82 
83         @NonNull
84         public final ActivityOptions activityOptions;
85 
86         @UserIdInt
87         public final int userId;
88 
89         @GuardedBy("mLock")
90         public boolean isVisible;
91         @GuardedBy("mLock")
92         public long lastLaunchTimeMs = 0;
93         @GuardedBy("mLock")
94         public int consecutiveRetries = 0;
95         @GuardedBy("mLock")
96         public int taskId = INVALID_TASK_ID;
97         @GuardedBy("mLock")
98         public int previousTaskId = INVALID_TASK_ID;
99         @GuardedBy("mLock")
100         public boolean inBackground;
101         @GuardedBy("mLock")
102         public boolean failureLogged;
103 
RunningActivityInfo(@onNull Intent intent, @NonNull ActivityOptions activityOptions, @UserIdInt int userId)104         RunningActivityInfo(@NonNull Intent intent, @NonNull ActivityOptions activityOptions,
105                 @UserIdInt int userId) {
106             this.intent = intent;
107             this.activityOptions = activityOptions;
108             this.userId = userId;
109         }
110 
resetCrashCounterLocked()111         private void resetCrashCounterLocked() {
112             consecutiveRetries = 0;
113             failureLogged = false;
114         }
115 
116         @Override
toString()117         public String toString() {
118             return "RunningActivityInfo{intent:" + intent + ",activityOptions:" + activityOptions
119                     + ",userId:" + userId + ",isVisible:" + isVisible
120                     + ",lastLaunchTimeMs:" + lastLaunchTimeMs
121                     + ",consecutiveRetries:" + consecutiveRetries + ",taskId:" + taskId + "}";
122         }
123     }
124 
125     private final Context mContext;
126 
127     private final IActivityManager mAm;
128 
129     private final DisplayManager mDm;
130 
131     private final UserManager mUm;
132 
133     private final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
134         @Override
135         public void onUserLockChanged(@UserIdInt int userId, boolean unlocked) {
136             // Nothing to do
137         }
138 
139         @Override
140         public void onSwitchUser(@UserIdInt int userId) {
141             synchronized (mLock) {
142                 mRunningActivities.clear();
143             }
144         }
145     };
146 
147     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
148         @Override
149         public void onReceive(Context context, Intent intent) {
150             String action = intent.getAction();
151             if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
152                     || Intent.ACTION_PACKAGE_REPLACED.equals(
153                     action)) {
154                 Uri packageData = intent.getData();
155                 if (packageData == null) {
156                     Log.w(TAG_AM, "null packageData");
157                     return;
158                 }
159                 String packageName = packageData.getSchemeSpecificPart();
160                 if (packageName == null) {
161                     Log.w(TAG_AM, "null packageName");
162                     return;
163                 }
164                 int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
165                 int userId = UserHandle.getUserId(uid);
166                 boolean tryLaunch = false;
167                 synchronized (mLock) {
168                     for (int i = 0; i < mRunningActivities.size(); i++) {
169                         RunningActivityInfo info = mRunningActivities.valueAt(i);
170                         ComponentName component = info.intent.getComponent();
171                         // should do this for all activities as the same package can cover multiple
172                         // displays.
173                         if (packageName.equals(component.getPackageName())
174                                 && info.userId == userId) {
175                             Log.i(TAG_AM, "Package updated:" + packageName
176                                     + ",user:" + userId);
177                             info.resetCrashCounterLocked();
178                             tryLaunch = true;
179                         }
180                     }
181                 }
182                 if (tryLaunch) {
183                     launchIfNecessary();
184                 }
185             }
186         }
187     };
188 
189     // It says listener but is actually callback.
190     private final TaskStackListener mTaskStackListener = new TaskStackListener() {
191         @Override
192         public void onTaskStackChanged() {
193             launchIfNecessary();
194         }
195 
196         @Override
197         public void onTaskCreated(int taskId, ComponentName componentName) {
198             launchIfNecessary();
199         }
200 
201         @Override
202         public void onTaskRemoved(int taskId) {
203             launchIfNecessary();
204         }
205 
206         @Override
207         public void onTaskMovedToFront(int taskId) {
208             launchIfNecessary();
209         }
210 
211         @Override
212         public void onTaskRemovalStarted(int taskId) {
213             launchIfNecessary();
214         }
215     };
216 
217     private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
218         @Override
219         public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
220             launchIfNecessary();
221         }
222 
223         @Override
224         public void onForegroundServicesChanged(int pid, int uid, int fgServiceTypes) {
225           // ignore
226         }
227 
228         @Override
229         public void onProcessDied(int pid, int uid) {
230             launchIfNecessary();
231         }
232     };
233 
234     private final HandlerThread mHandlerThread = new HandlerThread(
235             FixedActivityService.class.getSimpleName());
236 
237     private final Runnable mActivityCheckRunnable = () -> {
238         launchIfNecessary();
239     };
240 
241     private final Object mLock = new Object();
242 
243     // key: displayId
244     @GuardedBy("mLock")
245     private final SparseArray<RunningActivityInfo> mRunningActivities =
246             new SparseArray<>(/* capacity= */ 1); // default to one cluster only case
247 
248     @GuardedBy("mLock")
249     private final SparseArray<Presentation> mBlockingPresentations = new SparseArray<>(1);
250 
251     @GuardedBy("mLock")
252     private boolean mEventMonitoringActive;
253 
254     @GuardedBy("mLock")
255     private CarPowerManager mCarPowerManager;
256 
257     private final CarPowerManager.CarPowerStateListener mCarPowerStateListener = (state) -> {
258         if (state != CarPowerManager.CarPowerStateListener.ON) {
259             return;
260         }
261         synchronized (mLock) {
262             for (int i = 0; i < mRunningActivities.size(); i++) {
263                 RunningActivityInfo info = mRunningActivities.valueAt(i);
264                 info.resetCrashCounterLocked();
265             }
266         }
267         launchIfNecessary();
268     };
269 
FixedActivityService(Context context)270     public FixedActivityService(Context context) {
271         mContext = context;
272         mAm = ActivityManager.getService();
273         mUm = context.getSystemService(UserManager.class);
274         mDm = context.getSystemService(DisplayManager.class);
275         mHandlerThread.start();
276     }
277 
278     @Override
init()279     public void init() {
280         // nothing to do
281     }
282 
283     @Override
release()284     public void release() {
285         stopMonitoringEvents();
286     }
287 
288     @Override
dump(PrintWriter writer)289     public void dump(PrintWriter writer) {
290         writer.println("*FixedActivityService*");
291         synchronized (mLock) {
292             writer.println("mRunningActivities:" + mRunningActivities
293                     + " ,mEventMonitoringActive:" + mEventMonitoringActive);
294         }
295     }
296 
postRecheck(long delayMs)297     private void postRecheck(long delayMs) {
298         mHandlerThread.getThreadHandler().postDelayed(mActivityCheckRunnable, delayMs);
299     }
300 
startMonitoringEvents()301     private void startMonitoringEvents() {
302         CarPowerManager carPowerManager;
303         synchronized (mLock) {
304             if (mEventMonitoringActive) {
305                 return;
306             }
307             mEventMonitoringActive = true;
308             carPowerManager = CarLocalServices.createCarPowerManager(mContext);
309             mCarPowerManager = carPowerManager;
310         }
311         CarUserService userService = CarLocalServices.getService(CarUserService.class);
312         userService.addUserCallback(mUserCallback);
313         IntentFilter filter = new IntentFilter();
314         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
315         filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
316         filter.addDataScheme("package");
317         mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
318                 /* broadcastPermission= */ null, /* scheduler= */ null);
319         try {
320             mAm.registerTaskStackListener(mTaskStackListener);
321             mAm.registerProcessObserver(mProcessObserver);
322         } catch (RemoteException e) {
323             Log.e(TAG_AM, "remote exception from AM", e);
324         }
325         try {
326             carPowerManager.setListener(mCarPowerStateListener);
327         } catch (Exception e) {
328             // should not happen
329             Log.e(TAG_AM, "Got exception from CarPowerManager", e);
330         }
331     }
332 
stopMonitoringEvents()333     private void stopMonitoringEvents() {
334         CarPowerManager carPowerManager;
335         synchronized (mLock) {
336             if (!mEventMonitoringActive) {
337                 return;
338             }
339             mEventMonitoringActive = false;
340             carPowerManager = mCarPowerManager;
341             mCarPowerManager = null;
342         }
343         if (carPowerManager != null) {
344             carPowerManager.clearListener();
345         }
346         mHandlerThread.getThreadHandler().removeCallbacks(mActivityCheckRunnable);
347         CarUserService userService = CarLocalServices.getService(CarUserService.class);
348         userService.removeUserCallback(mUserCallback);
349         try {
350             mAm.unregisterTaskStackListener(mTaskStackListener);
351             mAm.unregisterProcessObserver(mProcessObserver);
352         } catch (RemoteException e) {
353             Log.e(TAG_AM, "remote exception from AM", e);
354         }
355         mContext.unregisterReceiver(mBroadcastReceiver);
356     }
357 
358     @Nullable
getStackInfos()359     private List<StackInfo> getStackInfos() {
360         try {
361             return mAm.getAllStackInfos();
362         } catch (RemoteException e) {
363             Log.e(TAG_AM, "remote exception from AM", e);
364         }
365         return null;
366     }
367 
368     /**
369      * Launches all stored fixed mode activities if necessary.
370      * @param displayId Display id to check if it is visible. If check is not necessary, should pass
371      *        {@link Display#INVALID_DISPLAY}.
372      * @return true if fixed Activity for given {@code displayId} is visible / successfully
373      *         launched. It will return false for {@link Display#INVALID_DISPLAY} {@code displayId}.
374      */
launchIfNecessary(int displayId)375     private boolean launchIfNecessary(int displayId) {
376         List<StackInfo> infos = getStackInfos();
377         if (infos == null) {
378             Log.e(TAG_AM, "cannot get StackInfo from AM");
379             return false;
380         }
381         long now = SystemClock.elapsedRealtime();
382         synchronized (mLock) {
383             if (mRunningActivities.size() == 0) {
384                 // it must have been stopped.
385                 if (DBG) {
386                     Log.i(TAG_AM, "empty activity list", new RuntimeException());
387                 }
388                 return false;
389             }
390             for (int i = mRunningActivities.size() - 1; i >= 0; i--) {
391                 RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
392                 activityInfo.isVisible = false;
393                 if (isUserAllowedToLaunchActivity(activityInfo.userId)) {
394                     continue;
395                 }
396                 final int displayIdForActivity = mRunningActivities.keyAt(i);
397                 if (activityInfo.taskId != INVALID_TASK_ID) {
398                     Log.i(TAG_AM, "Finishing fixed activity on user switching:"
399                             + activityInfo);
400                     try {
401                         mAm.removeTask(activityInfo.taskId);
402                     } catch (RemoteException e) {
403                         Log.e(TAG_AM, "remote exception from AM", e);
404                     }
405                     CarServiceUtils.runOnMain(() -> {
406                         Display display = mDm.getDisplay(displayIdForActivity);
407                         if (display == null) {
408                             Log.e(TAG_AM, "Display not available, cannot launnch window:"
409                                     + displayIdForActivity);
410                             return;
411                         }
412                         Presentation p = new Presentation(mContext, display,
413                                 android.R.style.Theme_Black_NoTitleBar_Fullscreen);
414                         p.setContentView(R.layout.activity_continuous_blank);
415                         p.show();
416                         synchronized (mLock) {
417                             mBlockingPresentations.append(displayIdForActivity, p);
418                         }
419                     });
420                 }
421                 mRunningActivities.removeAt(i);
422             }
423             for (StackInfo stackInfo : infos) {
424                 RunningActivityInfo activityInfo = mRunningActivities.get(stackInfo.displayId);
425                 if (activityInfo == null) {
426                     continue;
427                 }
428                 int topUserId = stackInfo.taskUserIds[stackInfo.taskUserIds.length - 1];
429                 if (activityInfo.intent.getComponent().equals(stackInfo.topActivity)
430                         && activityInfo.userId == topUserId && stackInfo.visible) {
431                     // top one is matching.
432                     activityInfo.isVisible = true;
433                     activityInfo.taskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
434                     continue;
435                 }
436                 activityInfo.previousTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
437                 Log.i(TAG_AM, "Unmatched top activity will be removed:"
438                         + stackInfo.topActivity + " top task id:" + activityInfo.previousTaskId
439                         + " user:" + topUserId + " display:" + stackInfo.displayId);
440                 activityInfo.inBackground = false;
441                 for (int i = 0; i < stackInfo.taskIds.length - 1; i++) {
442                     if (activityInfo.taskId == stackInfo.taskIds[i]) {
443                         activityInfo.inBackground = true;
444                     }
445                 }
446                 if (!activityInfo.inBackground) {
447                     activityInfo.taskId = INVALID_TASK_ID;
448                 }
449             }
450 
451             for (int i = 0; i < mRunningActivities.size(); i++) {
452                 RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
453                 long timeSinceLastLaunchMs = now - activityInfo.lastLaunchTimeMs;
454                 if (activityInfo.isVisible) {
455                     if (timeSinceLastLaunchMs >= CRASH_FORGET_INTERVAL_MS) {
456                         activityInfo.consecutiveRetries = 0;
457                     }
458                     continue;
459                 }
460                 if (!isComponentAvailable(activityInfo.intent.getComponent(),
461                         activityInfo.userId)) {
462                     continue;
463                 }
464                 // For 1st call (consecutiveRetries == 0), do not wait as there can be no posting
465                 // for recheck.
466                 if (activityInfo.consecutiveRetries > 0 && (timeSinceLastLaunchMs
467                         < RECHECK_INTERVAL_MS)) {
468                     // wait until next check interval comes.
469                     continue;
470                 }
471                 if (activityInfo.consecutiveRetries >= MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY) {
472                     // re-tried too many times, give up for now.
473                     if (!activityInfo.failureLogged) {
474                         activityInfo.failureLogged = true;
475                         Log.w(TAG_AM, "Too many relaunch failure of fixed activity:"
476                                 + activityInfo);
477                     }
478                     continue;
479                 }
480 
481                 Log.i(TAG_AM, "Launching Activity for fixed mode. Intent:" + activityInfo.intent
482                         + ",userId:" + UserHandle.of(activityInfo.userId) + ",displayId:"
483                         + mRunningActivities.keyAt(i));
484                 // Increase retry count if task is not in background. In case like other app is
485                 // launched and the target activity is still in background, do not consider it
486                 // as retry.
487                 if (!activityInfo.inBackground) {
488                     activityInfo.consecutiveRetries++;
489                 }
490                 try {
491                     postRecheck(RECHECK_INTERVAL_MS);
492                     postRecheck(CRASH_FORGET_INTERVAL_MS);
493                     mContext.startActivityAsUser(activityInfo.intent,
494                             activityInfo.activityOptions.toBundle(),
495                             UserHandle.of(activityInfo.userId));
496                     activityInfo.isVisible = true;
497                     activityInfo.lastLaunchTimeMs = SystemClock.elapsedRealtime();
498                 } catch (Exception e) { // Catch all for any app related issues.
499                     Log.w(TAG_AM, "Cannot start activity:" + activityInfo.intent, e);
500                 }
501             }
502             RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
503             if (activityInfo == null) {
504                 return false;
505             }
506             return activityInfo.isVisible;
507         }
508     }
509 
launchIfNecessary()510     private void launchIfNecessary() {
511         launchIfNecessary(Display.INVALID_DISPLAY);
512     }
513 
logComponentNotFound(ComponentName component, @UserIdInt int userId, Exception e)514     private void logComponentNotFound(ComponentName component, @UserIdInt  int userId,
515             Exception e) {
516         Log.e(TAG_AM, "Specified Component not found:" + component
517                 + " for userid:" + userId, e);
518     }
519 
isComponentAvailable(ComponentName component, @UserIdInt int userId)520     private boolean isComponentAvailable(ComponentName component, @UserIdInt int userId) {
521         PackageInfo packageInfo;
522         try {
523             packageInfo = mContext.getPackageManager().getPackageInfoAsUser(
524                     component.getPackageName(), PackageManager.GET_ACTIVITIES, userId);
525         } catch (PackageManager.NameNotFoundException e) {
526             logComponentNotFound(component, userId, e);
527             return false;
528         }
529         if (packageInfo == null || packageInfo.activities == null) {
530             // may not be necessary but additional safety check
531             logComponentNotFound(component, userId, new RuntimeException());
532             return false;
533         }
534         String fullName = component.getClassName();
535         String shortName = component.getShortClassName();
536         for (ActivityInfo info : packageInfo.activities) {
537             if (info.name.equals(fullName) || info.name.equals(shortName)) {
538                 return true;
539             }
540         }
541         logComponentNotFound(component, userId, new RuntimeException());
542         return false;
543     }
544 
isUserAllowedToLaunchActivity(@serIdInt int userId)545     private boolean isUserAllowedToLaunchActivity(@UserIdInt int userId) {
546         int currentUser = ActivityManager.getCurrentUser();
547         if (userId == currentUser) {
548             return true;
549         }
550         int[] profileIds = mUm.getEnabledProfileIds(currentUser);
551         for (int id : profileIds) {
552             if (id == userId) {
553                 return true;
554             }
555         }
556         return false;
557     }
558 
isDisplayAllowedForFixedMode(int displayId)559     private boolean isDisplayAllowedForFixedMode(int displayId) {
560         if (displayId == Display.DEFAULT_DISPLAY || displayId == Display.INVALID_DISPLAY) {
561             Log.w(TAG_AM, "Target display cannot be used for fixed mode, displayId:" + displayId,
562                     new RuntimeException());
563             return false;
564         }
565         return true;
566     }
567 
568     /**
569      * Checks {@link InstrumentClusterRenderingService#startFixedActivityModeForDisplayAndUser(
570      * Intent, ActivityOptions, int)}
571      */
startFixedActivityModeForDisplayAndUser(@onNull Intent intent, @NonNull ActivityOptions options, int displayId, @UserIdInt int userId)572     public boolean startFixedActivityModeForDisplayAndUser(@NonNull Intent intent,
573             @NonNull ActivityOptions options, int displayId, @UserIdInt int userId) {
574         if (!isDisplayAllowedForFixedMode(displayId)) {
575             return false;
576         }
577         if (options == null) {
578             Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, null options");
579             return false;
580         }
581         if (!isUserAllowedToLaunchActivity(userId)) {
582             Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, requested user:" + userId
583                     + " cannot launch activity, Intent:" + intent);
584             return false;
585         }
586         ComponentName component = intent.getComponent();
587         if (component == null) {
588             Log.e(TAG_AM,
589                     "startFixedActivityModeForDisplayAndUser: No component specified for "
590                             + "requested Intent"
591                             + intent);
592             return false;
593         }
594         if (!isComponentAvailable(component, userId)) {
595             return false;
596         }
597         boolean startMonitoringEvents = false;
598         synchronized (mLock) {
599             Presentation p = mBlockingPresentations.removeReturnOld(displayId);
600             if (p != null) {
601                 p.dismiss();
602             }
603             if (mRunningActivities.size() == 0) {
604                 startMonitoringEvents = true;
605             }
606             RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
607             boolean replaceEntry = true;
608             if (activityInfo != null && activityInfo.intent.equals(intent)
609                     && options.equals(activityInfo.activityOptions)
610                     && userId == activityInfo.userId) {
611                 replaceEntry = false;
612                 if (activityInfo.isVisible) { // already shown.
613                     return true;
614                 }
615             }
616             if (replaceEntry) {
617                 activityInfo = new RunningActivityInfo(intent, options, userId);
618                 mRunningActivities.put(displayId, activityInfo);
619             }
620         }
621         boolean launched = launchIfNecessary(displayId);
622         if (!launched) {
623             synchronized (mLock) {
624                 mRunningActivities.remove(displayId);
625             }
626         }
627         // If first trial fails, let client know and do not retry as it can be wrong setting.
628         if (startMonitoringEvents && launched) {
629             startMonitoringEvents();
630         }
631         return launched;
632     }
633 
634     /** Check {@link InstrumentClusterRenderingService#stopFixedActivityMode(int)} */
stopFixedActivityMode(int displayId)635     public void stopFixedActivityMode(int displayId) {
636         if (!isDisplayAllowedForFixedMode(displayId)) {
637             return;
638         }
639         boolean stopMonitoringEvents = false;
640         synchronized (mLock) {
641             mRunningActivities.remove(displayId);
642             if (mRunningActivities.size() == 0) {
643                 stopMonitoringEvents = true;
644             }
645         }
646         if (stopMonitoringEvents) {
647             stopMonitoringEvents();
648         }
649     }
650 }
651