• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.server.wm;
18 
19 import static android.car.PlatformVersion.VERSION_CODES;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.SystemApi;
24 import android.annotation.UserIdInt;
25 import android.car.PlatformVersionMismatchException;
26 import android.car.app.CarActivityManager;
27 import android.car.builtin.os.UserManagerHelper;
28 import android.car.builtin.util.Slogf;
29 import android.car.builtin.view.DisplayHelper;
30 import android.car.builtin.window.DisplayAreaOrganizerHelper;
31 import android.content.ComponentName;
32 import android.hardware.display.DisplayManager;
33 import android.os.ServiceSpecificException;
34 import android.util.ArrayMap;
35 import android.util.Log;
36 import android.util.Pair;
37 import android.util.SparseIntArray;
38 import android.view.Display;
39 
40 import com.android.car.internal.util.IndentingPrintWriter;
41 import com.android.car.internal.util.VersionUtils;
42 import com.android.internal.annotations.GuardedBy;
43 
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Collections;
47 import java.util.List;
48 
49 /**
50  * Implementation of {@link CarLaunchParamsModifierUpdatable}.
51  *
52  * @hide
53  */
54 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
55 public final class CarLaunchParamsModifierUpdatableImpl
56         implements CarLaunchParamsModifierUpdatable {
57     private static final String TAG = CarLaunchParamsModifierUpdatableImpl.class.getSimpleName();
58     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
59     // Comes from android.os.UserHandle.USER_NULL.
60     private static final int USER_NULL = -10000;
61 
62     private final CarLaunchParamsModifierInterface mBuiltin;
63     private final Object mLock = new Object();
64 
65     // Always start with USER_SYSTEM as the timing of handleCurrentUserSwitching(USER_SYSTEM) is not
66     // guaranteed to be earler than 1st Activity launch.
67     @GuardedBy("mLock")
68     private int mDriverUser = UserManagerHelper.USER_SYSTEM;
69 
70     // TODO: Switch from tracking displays to tracking display areas instead
71     /**
72      * This one is for holding all passenger (=profile user) displays which are mostly static unless
73      * displays are added / removed. Note that {@link #mDisplayToProfileUserMapping} can be empty
74      * while user is assigned and that cannot always tell if specific display is for driver or not.
75      */
76     @GuardedBy("mLock")
77     private final ArrayList<Integer> mPassengerDisplays = new ArrayList<>();
78 
79     /** key: display id, value: profile user id */
80     @GuardedBy("mLock")
81     private final SparseIntArray mDisplayToProfileUserMapping = new SparseIntArray();
82 
83     /** key: profile user id, value: display id */
84     @GuardedBy("mLock")
85     private final SparseIntArray mDefaultDisplayForProfileUser = new SparseIntArray();
86 
87     @GuardedBy("mLock")
88     private boolean mIsSourcePreferred;
89 
90     @GuardedBy("mLock")
91     private List<ComponentName> mSourcePreferredComponents;
92 
93     /** key: Activity, value: TaskDisplayAreaWrapper */
94     @GuardedBy("mLock")
95     private final ArrayMap<ComponentName, TaskDisplayAreaWrapper> mPersistentActivities =
96             new ArrayMap<>();
97 
CarLaunchParamsModifierUpdatableImpl(CarLaunchParamsModifierInterface builtin)98     public CarLaunchParamsModifierUpdatableImpl(CarLaunchParamsModifierInterface builtin) {
99         mBuiltin = builtin;
100     }
101 
getDisplayListener()102     public DisplayManager.DisplayListener getDisplayListener() {
103         return mDisplayListener;
104     }
105 
106     private final DisplayManager.DisplayListener mDisplayListener =
107             new DisplayManager.DisplayListener() {
108                 @Override
109                 public void onDisplayAdded(int displayId) {
110                     // ignore. car service should update whiltelist.
111                 }
112 
113                 @Override
114                 public void onDisplayRemoved(int displayId) {
115                     synchronized (mLock) {
116                         mPassengerDisplays.remove(Integer.valueOf(displayId));
117                         updateProfileUserConfigForDisplayRemovalLocked(displayId);
118                     }
119                 }
120 
121                 @Override
122                 public void onDisplayChanged(int displayId) {
123                     // ignore
124                 }
125             };
126 
127     @GuardedBy("mLock")
updateProfileUserConfigForDisplayRemovalLocked(int displayId)128     private void updateProfileUserConfigForDisplayRemovalLocked(int displayId) {
129         mDisplayToProfileUserMapping.delete(displayId);
130         int i = mDefaultDisplayForProfileUser.indexOfValue(displayId);
131         if (i >= 0) {
132             mDefaultDisplayForProfileUser.removeAt(i);
133         }
134     }
135 
136     /**
137      * Sets {@code sourcePreferred} configuration. When {@code sourcePreferred} is enabled and
138      * there is no pre-assigned display for the Activity, CarLauncherParamsModifier will launch
139      * the Activity in the display of the source. When {@code sourcePreferredComponents} isn't null
140      * the {@code sourcePreferred} is applied for the {@code sourcePreferredComponents} only.
141      *
142      * @param enableSourcePreferred whether to enable sourcePreferred mode
143      * @param sourcePreferredComponents null for all components, or the list of components to apply
144      */
setSourcePreferredComponents(boolean enableSourcePreferred, @Nullable List<ComponentName> sourcePreferredComponents)145     public void setSourcePreferredComponents(boolean enableSourcePreferred,
146             @Nullable List<ComponentName> sourcePreferredComponents) {
147         synchronized (mLock) {
148             mIsSourcePreferred = enableSourcePreferred;
149             mSourcePreferredComponents = sourcePreferredComponents;
150             if (mSourcePreferredComponents != null) {
151                 Collections.sort(mSourcePreferredComponents);
152             }
153         }
154     }
155 
156     @Override
handleUserVisibilityChanged(int userId, boolean visible)157     public void handleUserVisibilityChanged(int userId, boolean visible) {
158         synchronized (mLock) {
159             if (DBG) {
160                 Slogf.d(TAG, "handleUserVisibilityChanged user=%d, visible=%b",
161                         userId, visible);
162             }
163             if (userId != mDriverUser || visible) {
164                 return;
165             }
166             int currentOrTargetUserId = getCurrentOrTargetUserId();
167             maySwitchCurrentDriver(currentOrTargetUserId);
168         }
169     }
170 
getCurrentOrTargetUserId()171     private int getCurrentOrTargetUserId() {
172         if (!VersionUtils.isPlatformVersionAtLeastU()) {
173             throw new PlatformVersionMismatchException(VERSION_CODES.UPSIDE_DOWN_CAKE_0);
174         }
175         Pair<Integer, Integer> currentAndTargetUserIds = mBuiltin.getCurrentAndTargetUserIds();
176         int currentUserId = currentAndTargetUserIds.first;
177         int targetUserId = currentAndTargetUserIds.second;
178         int currentOrTargetUserId = targetUserId != USER_NULL ? targetUserId : currentUserId;
179         return currentOrTargetUserId;
180     }
181 
182     /** Notifies user switching. */
handleCurrentUserSwitching(@serIdInt int newUserId)183     public void handleCurrentUserSwitching(@UserIdInt int newUserId) {
184         if (DBG) Slogf.d(TAG, "handleCurrentUserSwitching user=%d", newUserId);
185         maySwitchCurrentDriver(newUserId);
186     }
187 
maySwitchCurrentDriver(int userId)188     private void maySwitchCurrentDriver(int userId) {
189         synchronized (mLock) {
190             if (DBG) {
191                 Slogf.d(TAG, "maySwitchCurrentDriver old=%d, new=%d", mDriverUser, userId);
192             }
193             if (mDriverUser == userId) {
194                 return;
195             }
196             mDriverUser = userId;
197             mDefaultDisplayForProfileUser.clear();
198             mDisplayToProfileUserMapping.clear();
199         }
200     }
201 
202     /** Notifies user starting. */
handleUserStarting(int startingUser)203     public void handleUserStarting(int startingUser) {
204         if (DBG) Slogf.d(TAG, "handleUserStarting user=%d", startingUser);
205         // Do nothing
206     }
207 
208     /** Notifies user stopped. */
handleUserStopped(@serIdInt int stoppedUser)209     public void handleUserStopped(@UserIdInt int stoppedUser) {
210         if (DBG) Slogf.d(TAG, "handleUserStopped user=%d", stoppedUser);
211         // Note that the current user is never stopped. It always takes switching into
212         // non-current user before stopping the user.
213         synchronized (mLock) {
214             removeUserFromAllowlistsLocked(stoppedUser);
215         }
216     }
217 
218     @GuardedBy("mLock")
removeUserFromAllowlistsLocked(int userId)219     private void removeUserFromAllowlistsLocked(int userId) {
220         for (int i = mDisplayToProfileUserMapping.size() - 1; i >= 0; i--) {
221             if (mDisplayToProfileUserMapping.valueAt(i) == userId) {
222                 mDisplayToProfileUserMapping.removeAt(i);
223             }
224         }
225         mDefaultDisplayForProfileUser.delete(userId);
226     }
227 
228     /**
229      * Sets display allowlist for the {@code userId}. For passenger user, activity will be always
230      * launched to a display in the allowlist. If requested display is not in the allowlist, the 1st
231      * display in the allowlist will be selected as target display.
232      *
233      * <p>The allowlist is kept only for profile user. Assigning the current user unassigns users
234      * for the given displays.
235      */
setDisplayAllowListForUser(@serIdInt int userId, int[] displayIds)236     public void setDisplayAllowListForUser(@UserIdInt int userId, int[] displayIds) {
237         if (DBG) {
238             Slogf.d(TAG, "setDisplayAllowlistForUser userId:%d displays:%s",
239                     userId, Arrays.toString(displayIds));
240         }
241         synchronized (mLock) {
242             for (int displayId : displayIds) {
243                 if (!mPassengerDisplays.contains(displayId)) {
244                     Slogf.w(TAG, "setDisplayAllowlistForUser called with display:%d"
245                             + " not in passenger display list:%s", displayId, mPassengerDisplays);
246                     continue;
247                 }
248                 if (userId == mDriverUser) {
249                     mDisplayToProfileUserMapping.delete(displayId);
250                 } else {
251                     mDisplayToProfileUserMapping.put(displayId, userId);
252                 }
253                 // now the display cannot be a default display for other user
254                 int i = mDefaultDisplayForProfileUser.indexOfValue(displayId);
255                 if (i >= 0) {
256                     mDefaultDisplayForProfileUser.removeAt(i);
257                 }
258             }
259             if (displayIds.length > 0) {
260                 mDefaultDisplayForProfileUser.put(userId, displayIds[0]);
261             } else {
262                 removeUserFromAllowlistsLocked(userId);
263             }
264         }
265     }
266 
267     /**
268      * Sets displays assigned to passenger. All other displays will be treated as assigned to
269      * driver.
270      *
271      * <p>The 1st display in the array will be considered as a default display to assign
272      * for any non-driver user if there is no display assigned for the user. </p>
273      */
setPassengerDisplays(int[] displayIdsForPassenger)274     public void setPassengerDisplays(int[] displayIdsForPassenger) {
275         if (DBG) {
276             Slogf.d(TAG, "setPassengerDisplays displays:%s",
277                     Arrays.toString(displayIdsForPassenger));
278         }
279         synchronized (mLock) {
280             for (int id : displayIdsForPassenger) {
281                 mPassengerDisplays.remove(Integer.valueOf(id));
282             }
283             // handle removed displays
284             for (int i = 0; i < mPassengerDisplays.size(); i++) {
285                 int displayId = mPassengerDisplays.get(i);
286                 updateProfileUserConfigForDisplayRemovalLocked(displayId);
287             }
288             mPassengerDisplays.clear();
289             mPassengerDisplays.ensureCapacity(displayIdsForPassenger.length);
290             for (int id : displayIdsForPassenger) {
291                 mPassengerDisplays.add(id);
292             }
293         }
294     }
295 
296     /**
297      * Calculates {@code outParams} based on the given arguments.
298      * See {@code LaunchParamsController.LaunchParamsModifier.onCalculate()} for the detail.
299      */
calculate(CalculateParams params)300     public int calculate(CalculateParams params) {
301         TaskWrapper task = params.getTask();
302         ActivityRecordWrapper activity = params.getActivity();
303         ActivityRecordWrapper source = params.getSource();
304         ActivityOptionsWrapper options = params.getOptions();
305         RequestWrapper request = params.getRequest();
306         LaunchParamsWrapper currentParams = params.getCurrentParams();
307         LaunchParamsWrapper outParams = params.getOutParams();
308 
309         int userId;
310         if (task != null) {
311             userId = task.getUserId();
312         } else if (activity != null) {
313             userId = activity.getUserId();
314         } else {
315             Slogf.w(TAG, "onCalculate, cannot decide user");
316             return LaunchParamsWrapper.RESULT_SKIP;
317         }
318         // DisplayArea where user wants to launch the Activity.
319         TaskDisplayAreaWrapper originalDisplayArea = currentParams.getPreferredTaskDisplayArea();
320         // DisplayArea where CarLaunchParamsModifier targets to launch the Activity.
321         TaskDisplayAreaWrapper targetDisplayArea = null;
322         ComponentName activityName = activity.getComponentName();
323         if (DBG) {
324             Slogf.d(TAG, "onCalculate, userId:%d original displayArea:%s actvity:%s options:%s",
325                     userId, originalDisplayArea, activityName, options);
326         }
327         decision:
328         synchronized (mLock) {
329             // If originalDisplayArea is set, respect that before ActivityOptions check.
330             if (originalDisplayArea == null) {
331                 if (options != null) {
332                     originalDisplayArea = options.getLaunchTaskDisplayArea();
333                     if (originalDisplayArea == null) {
334                         originalDisplayArea = mBuiltin.getDefaultTaskDisplayAreaOnDisplay(
335                                 options.getOptions().getLaunchDisplayId());
336                     }
337                 }
338             }
339             if (mPersistentActivities.containsKey(activityName)) {
340                 targetDisplayArea = mPersistentActivities.get(activityName);
341             } else if (originalDisplayArea == null  // No specified DA to launch the Activity
342                     && mIsSourcePreferred && source != null
343                     && (mSourcePreferredComponents == null || Collections.binarySearch(
344                     mSourcePreferredComponents, activityName) >= 0)) {
345                 targetDisplayArea = source.isNoDisplay() ? source.getHandoverTaskDisplayArea()
346                         : source.getDisplayArea();
347             } else if (originalDisplayArea == null
348                     && task == null  // launching as a new task
349                     && source != null && !source.isDisplayTrusted()
350                     && !source.allowingEmbedded()) {
351                 if (DBG) {
352                     Slogf.d(TAG, "Disallow launch on virtual display for not-embedded activity.");
353                 }
354                 targetDisplayArea = mBuiltin.getDefaultTaskDisplayAreaOnDisplay(
355                         Display.DEFAULT_DISPLAY);
356             }
357             if (userId == mDriverUser) {
358                 // Respect the existing DisplayArea.
359                 if (DBG) Slogf.d(TAG, "Skip the further check for Driver");
360                 break decision;
361             }
362             if (userId == UserManagerHelper.USER_SYSTEM) {
363                 // This will be only allowed if it has FLAG_SHOW_FOR_ALL_USERS.
364                 // The flag is not immediately accessible here so skip the check.
365                 // But other WM policy will enforce it.
366                 if (DBG) Slogf.d(TAG, "Skip the further check for SystemUser");
367                 break decision;
368             }
369             // Now user is a passenger.
370             if (mPassengerDisplays.isEmpty()) {
371                 // No displays for passengers. This could be old user and do not do anything.
372                 if (DBG) Slogf.d(TAG, "Skip the further check for no PassengerDisplays");
373                 break decision;
374             }
375             if (targetDisplayArea == null) {
376                 if (originalDisplayArea != null) {
377                     targetDisplayArea = originalDisplayArea;
378                 } else {
379                     targetDisplayArea = mBuiltin.getDefaultTaskDisplayAreaOnDisplay(
380                             Display.DEFAULT_DISPLAY);
381                 }
382             }
383             Display display = targetDisplayArea.getDisplay();
384             if ((display.getFlags() & Display.FLAG_PRIVATE) != 0) {
385                 // private display should follow its own restriction rule.
386                 if (DBG) Slogf.d(TAG, "Skip the further check for the private display");
387                 break decision;
388             }
389             if (DisplayHelper.getType(display) == DisplayHelper.TYPE_VIRTUAL) {
390                 // TODO(b/132903422) : We need to update this after the bug is resolved.
391                 // For now, don't change anything.
392                 if (DBG) Slogf.d(TAG, "Skip the further check for the virtual display");
393                 break decision;
394             }
395             int userForDisplay = getUserForDisplayLocked(display.getDisplayId());
396             if (userForDisplay == userId) {
397                 if (DBG) Slogf.d(TAG, "The display is assigned for the user");
398                 break decision;
399             }
400             targetDisplayArea = getAlternativeDisplayAreaForPassengerLocked(
401                     userId, activity, request);
402         }
403         if (targetDisplayArea != null && originalDisplayArea != targetDisplayArea) {
404             Slogf.i(TAG, "Changed launching display, user:%d requested display area:%s"
405                     + " target display area:%s", userId, originalDisplayArea, targetDisplayArea);
406             outParams.setPreferredTaskDisplayArea(targetDisplayArea);
407             if (VersionUtils.isPlatformVersionAtLeastU()
408                     && options != null
409                     && options.getLaunchWindowingMode()
410                     != ActivityOptionsWrapper.WINDOWING_MODE_UNDEFINED) {
411                 outParams.setWindowingMode(options.getLaunchWindowingMode());
412             }
413             return LaunchParamsWrapper.RESULT_DONE;
414         } else {
415             return LaunchParamsWrapper.RESULT_SKIP;
416         }
417     }
418 
419     @GuardedBy("mLock")
getUserForDisplayLocked(int displayId)420     private int getUserForDisplayLocked(int displayId) {
421         int userForDisplay = mDisplayToProfileUserMapping.get(displayId,
422                 UserManagerHelper.USER_NULL);
423         if (userForDisplay != UserManagerHelper.USER_NULL) {
424             return userForDisplay;
425         }
426         if (VersionUtils.isPlatformVersionAtLeastU()) {
427             userForDisplay = mBuiltin.getUserAssignedToDisplay(displayId);
428         }
429         return userForDisplay;
430     }
431 
432     @GuardedBy("mLock")
433     @Nullable
getAlternativeDisplayAreaForPassengerLocked(int userId, @NonNull ActivityRecordWrapper activtyRecord, @Nullable RequestWrapper request)434     private TaskDisplayAreaWrapper getAlternativeDisplayAreaForPassengerLocked(int userId,
435             @NonNull ActivityRecordWrapper activtyRecord, @Nullable RequestWrapper request) {
436         if (DBG) Slogf.d(TAG, "getAlternativeDisplayAreaForPassengerLocked:%d", userId);
437         List<TaskDisplayAreaWrapper> fallbacks = mBuiltin.getFallbackDisplayAreasForActivity(
438                 activtyRecord, request);
439         for (int i = 0, size = fallbacks.size(); i < size; ++i) {
440             TaskDisplayAreaWrapper fallbackTda = fallbacks.get(i);
441             int userForDisplay = getUserIdForDisplayLocked(fallbackTda.getDisplay().getDisplayId());
442             if (userForDisplay == userId) {
443                 return fallbackTda;
444             }
445         }
446         return fallbackDisplayAreaForUserLocked(userId);
447     }
448 
449     /**
450      * Returns {@code userId} who is allowed to use the given {@code displayId}, or
451      * {@code UserHandle.USER_NULL} if the display doesn't exist in the mapping.
452      */
453     @GuardedBy("mLock")
getUserIdForDisplayLocked(int displayId)454     private int getUserIdForDisplayLocked(int displayId) {
455         return mDisplayToProfileUserMapping.get(displayId, UserManagerHelper.USER_NULL);
456     }
457 
458     /**
459      * Return a {@link TaskDisplayAreaWrapper} that can be used if a source display area is
460      * not found. First check the default display for the user. If it is absent select
461      * the first passenger display if present.  If both are absent return {@code null}
462      *
463      * @param userId ID of the active user
464      * @return {@link TaskDisplayAreaWrapper} that is recommended when a display area is
465      *     not specified
466      */
467     @GuardedBy("mLock")
468     @Nullable
fallbackDisplayAreaForUserLocked(@serIdInt int userId)469     private TaskDisplayAreaWrapper fallbackDisplayAreaForUserLocked(@UserIdInt int userId) {
470         int displayIdForUserProfile = mDefaultDisplayForProfileUser.get(userId,
471                 Display.INVALID_DISPLAY);
472         if (displayIdForUserProfile != Display.INVALID_DISPLAY) {
473             int displayId = mDefaultDisplayForProfileUser.get(userId);
474             return mBuiltin.getDefaultTaskDisplayAreaOnDisplay(displayId);
475         }
476         if (VersionUtils.isPlatformVersionAtLeastU()) {
477             int displayId = mBuiltin.getMainDisplayAssignedToUser(userId);
478             if (displayId != Display.INVALID_DISPLAY) {
479                 return mBuiltin.getDefaultTaskDisplayAreaOnDisplay(displayId);
480             }
481         }
482         if (!mPassengerDisplays.isEmpty()) {
483             int displayId = mPassengerDisplays.get(0);
484             if (DBG) {
485                 Slogf.d(TAG, "fallbackDisplayAreaForUserLocked: userId=%d, displayId=%d",
486                         userId, displayId);
487             }
488             return mBuiltin.getDefaultTaskDisplayAreaOnDisplay(displayId);
489         }
490         return null;
491     }
492 
493     /**
494      * See {@link CarActivityManager#setPersistentActivity(android.content.ComponentName,int, int)}
495      */
setPersistentActivity(ComponentName activity, int displayId, int featureId)496     public int setPersistentActivity(ComponentName activity, int displayId, int featureId) {
497         if (DBG) {
498             Slogf.d(TAG, "setPersistentActivity: activity=%s, displayId=%d, featureId=%d",
499                     activity, displayId, featureId);
500         }
501         if (featureId == DisplayAreaOrganizerHelper.FEATURE_UNDEFINED) {
502             synchronized (mLock) {
503                 TaskDisplayAreaWrapper removed = mPersistentActivities.remove(activity);
504                 if (removed == null) {
505                     throw new ServiceSpecificException(
506                             CarActivityManager.ERROR_CODE_ACTIVITY_NOT_FOUND,
507                             "Failed to remove " + activity.toShortString());
508                 }
509                 return CarActivityManager.RESULT_SUCCESS;
510             }
511         }
512         TaskDisplayAreaWrapper tda = mBuiltin.findTaskDisplayArea(displayId, featureId);
513         if (tda == null) {
514             throw new IllegalArgumentException("Unknown display=" + displayId
515                     + " or feature=" + featureId);
516         }
517         synchronized (mLock) {
518             mPersistentActivities.put(activity, tda);
519         }
520         return CarActivityManager.RESULT_SUCCESS;
521     }
522 
523     /**
524      * Dump {code CarLaunchParamsModifierUpdatableImpl#mPersistentActivities}
525      */
dump(IndentingPrintWriter writer)526     public void dump(IndentingPrintWriter writer) {
527         writer.println(TAG);
528         writer.increaseIndent();
529         writer.println("Persistent Activities:");
530         writer.increaseIndent();
531         synchronized (mLock) {
532             if (mPersistentActivities.size() == 0) {
533                 writer.println("No activity persisted on a task display area");
534             } else {
535                 for (int i = 0; i < mPersistentActivities.size(); i++) {
536                     TaskDisplayAreaWrapper taskDisplayAreaWrapper =
537                             mPersistentActivities.valueAt(i);
538                     writer.println(
539                             "Activity name: " + mPersistentActivities.keyAt(i) + " - Display ID: "
540                                     + taskDisplayAreaWrapper.getDisplay().getDisplayId()
541                                     + " , Feature ID: " + taskDisplayAreaWrapper.getFeatureId());
542                 }
543             }
544         }
545         writer.decreaseIndent();
546         writer.decreaseIndent();
547     }
548 }
549