1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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.ide.common.layout.relative;
17 
18 import static com.android.SdkConstants.ANDROID_URI;
19 import static com.android.SdkConstants.ATTR_ID;
20 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
21 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
22 import static com.android.SdkConstants.ID_PREFIX;
23 import static com.android.SdkConstants.NEW_ID_PREFIX;
24 import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix;
25 import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL;
26 import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL;
27 
28 import com.android.SdkConstants;
29 import com.android.annotations.NonNull;
30 import com.android.annotations.Nullable;
31 import com.android.ide.common.api.INode;
32 import com.android.ide.common.api.INode.IAttribute;
33 import com.google.common.collect.Maps;
34 import com.google.common.collect.Sets;
35 
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 
40 /**
41  * Handles deletions in a relative layout, transferring constraints across
42  * deleted nodes
43  * <p>
44  * TODO: Consider adding the
45  * {@link SdkConstants#ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING} attribute to a
46  * node if it's pointing to a node which is deleted and which has no transitive
47  * reference to another node.
48  */
49 public class DeletionHandler {
50     private final INode mLayout;
51     private final INode[] mChildren;
52     private final List<INode> mDeleted;
53     private final Set<String> mDeletedIds;
54     private final Map<String, INode> mNodeMap;
55     private final List<INode> mMoved;
56 
57     /**
58      * Creates a new {@link DeletionHandler}
59      *
60      * @param deleted the deleted nodes
61      * @param moved nodes that were moved (e.g. deleted, but also inserted elsewhere)
62      * @param layout the parent layout of the deleted nodes
63      */
DeletionHandler(@onNull List<INode> deleted, @NonNull List<INode> moved, @NonNull INode layout)64     public DeletionHandler(@NonNull List<INode> deleted, @NonNull List<INode> moved,
65             @NonNull INode layout) {
66         mDeleted = deleted;
67         mMoved = moved;
68         mLayout = layout;
69 
70         mChildren = mLayout.getChildren();
71         mNodeMap = Maps.newHashMapWithExpectedSize(mChildren.length);
72         for (INode child : mChildren) {
73             String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
74             if (id != null) {
75                 mNodeMap.put(stripIdPrefix(id), child);
76             }
77         }
78 
79         mDeletedIds = Sets.newHashSetWithExpectedSize(mDeleted.size());
80         for (INode node : mDeleted) {
81             String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
82             if (id != null) {
83                 mDeletedIds.add(stripIdPrefix(id));
84             }
85         }
86 
87         // Any widgets that remain (e.g. typically because they were moved) should
88         // keep their incoming dependencies
89         for (INode node : mMoved) {
90             String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
91             if (id != null) {
92                 mDeletedIds.remove(stripIdPrefix(id));
93             }
94         }
95     }
96 
97     @Nullable
getId(@onNull IAttribute attribute)98     private static String getId(@NonNull IAttribute attribute) {
99         if (attribute.getName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
100                 && ANDROID_URI.equals(attribute.getUri())
101                 && !attribute.getName().startsWith(ATTR_LAYOUT_MARGIN)) {
102             String id = attribute.getValue();
103             // It might not be an id reference, so check manually rather than just
104             // calling stripIdPrefix():
105             if (id.startsWith(NEW_ID_PREFIX)) {
106                 return id.substring(NEW_ID_PREFIX.length());
107             } else if (id.startsWith(ID_PREFIX)) {
108                 return id.substring(ID_PREFIX.length());
109             }
110         }
111 
112         return null;
113     }
114 
115     /**
116      * Updates the constraints in the layout to handle deletion of a set of
117      * nodes. This ensures that any constraints pointing to one of the deleted
118      * nodes are changed properly to point to a non-deleted node with similar
119      * constraints.
120      */
updateConstraints()121     public void updateConstraints() {
122         if (mChildren.length == mDeleted.size()) {
123             // Deleting everything: Nothing to be done
124             return;
125         }
126 
127         // Now remove incoming edges to any views that were deleted. If possible,
128         // don't just delete them but replace them with a transitive constraint, e.g.
129         // if we have "A <= B <= C" and "B" is removed, then we end up with "A <= C",
130 
131         for (INode child : mChildren) {
132             if (mDeleted.contains(child)) {
133                 continue;
134             }
135 
136             for (IAttribute attribute : child.getLiveAttributes()) {
137                 String id = getId(attribute);
138                 if (id != null) {
139                     if (mDeletedIds.contains(id)) {
140                         // Unset this reference to a deleted widget. It might be
141                         // replaced if the pointed to node points to some other node
142                         // on the same side, but it may use a different constraint name,
143                         // or have none at all (e.g. parent).
144                         String name = attribute.getName();
145                         child.setAttribute(ANDROID_URI, name, null);
146 
147                         INode deleted = mNodeMap.get(id);
148                         if (deleted != null) {
149                             ConstraintType type = ConstraintType.fromAttribute(name);
150                             if (type != null) {
151                                 transfer(deleted, child, type, 0);
152                             }
153                         }
154                     }
155                 }
156             }
157         }
158     }
159 
transfer(INode deleted, INode target, ConstraintType targetType, int depth)160     private void transfer(INode deleted, INode target, ConstraintType targetType, int depth) {
161         if (depth == 20) {
162             // Prevent really deep flow or unbounded recursion in case there is a bug in
163             // the cycle detection code
164             return;
165         }
166 
167         assert mDeleted.contains(deleted);
168 
169         for (IAttribute attribute : deleted.getLiveAttributes()) {
170             String name = attribute.getName();
171             ConstraintType type = ConstraintType.fromAttribute(name);
172             if (type == null) {
173                 continue;
174             }
175 
176             ConstraintType transfer = getCompatibleConstraint(type, targetType);
177             if (transfer != null) {
178                 String id = getId(attribute);
179                 if (id != null) {
180                     if (mDeletedIds.contains(id)) {
181                         INode nextDeleted = mNodeMap.get(id);
182                         if (nextDeleted != null) {
183                             // Points to another deleted node: recurse
184                             transfer(nextDeleted, target, targetType, depth + 1);
185                         }
186                     } else {
187                         // Found an undeleted node destination: point to it directly.
188                         // Note that we're using the
189                         target.setAttribute(ANDROID_URI, transfer.name, attribute.getValue());
190                     }
191                 } else {
192                     // Pointing to parent or center etc (non-id ref): replicate this on the target
193                     target.setAttribute(ANDROID_URI, name, attribute.getValue());
194                 }
195             }
196         }
197     }
198 
199     /**
200      * Determines if two constraints are in the same direction and if so returns
201      * the constraint in the same direction. Rather than returning boolean true
202      * or false, this returns the constraint which is sometimes modified. For
203      * example, if you have a node which points left to a node which is centered
204      * in parent, then the constraint is turned into center horizontal.
205      */
206     @Nullable
getCompatibleConstraint( @onNull ConstraintType first, @NonNull ConstraintType second)207     private static ConstraintType getCompatibleConstraint(
208             @NonNull ConstraintType first, @NonNull ConstraintType second) {
209         if (first == second) {
210             return first;
211         }
212 
213         switch (second) {
214             case ALIGN_LEFT:
215             case LAYOUT_RIGHT_OF:
216                 switch (first) {
217                     case LAYOUT_CENTER_HORIZONTAL:
218                     case LAYOUT_LEFT_OF:
219                     case ALIGN_LEFT:
220                         return first;
221                     case LAYOUT_CENTER_IN_PARENT:
222                         return LAYOUT_CENTER_HORIZONTAL;
223                 }
224                 return null;
225 
226             case ALIGN_RIGHT:
227             case LAYOUT_LEFT_OF:
228                 switch (first) {
229                     case LAYOUT_CENTER_HORIZONTAL:
230                     case ALIGN_RIGHT:
231                     case LAYOUT_LEFT_OF:
232                         return first;
233                     case LAYOUT_CENTER_IN_PARENT:
234                         return LAYOUT_CENTER_HORIZONTAL;
235                 }
236                 return null;
237 
238             case ALIGN_TOP:
239             case LAYOUT_BELOW:
240             case ALIGN_BASELINE:
241                 switch (first) {
242                     case LAYOUT_CENTER_VERTICAL:
243                     case ALIGN_TOP:
244                     case LAYOUT_BELOW:
245                     case ALIGN_BASELINE:
246                         return first;
247                     case LAYOUT_CENTER_IN_PARENT:
248                         return LAYOUT_CENTER_VERTICAL;
249                 }
250                 return null;
251             case ALIGN_BOTTOM:
252             case LAYOUT_ABOVE:
253                 switch (first) {
254                     case LAYOUT_CENTER_VERTICAL:
255                     case ALIGN_BOTTOM:
256                     case LAYOUT_ABOVE:
257                     case ALIGN_BASELINE:
258                         return first;
259                     case LAYOUT_CENTER_IN_PARENT:
260                         return LAYOUT_CENTER_VERTICAL;
261                 }
262                 return null;
263         }
264 
265         return null;
266     }
267 }
268