1 /*
2  * Copyright (C) 2008 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 android.view;
18 
19 import com.android.SdkConstants;
20 import com.android.ide.common.rendering.api.LayoutLog;
21 import com.android.ide.common.rendering.api.LayoutlibCallback;
22 import com.android.ide.common.rendering.api.MergeCookie;
23 import com.android.ide.common.rendering.api.ResourceNamespace;
24 import com.android.ide.common.rendering.api.ResourceReference;
25 import com.android.ide.common.rendering.api.ResourceValue;
26 import com.android.layoutlib.bridge.Bridge;
27 import com.android.layoutlib.bridge.BridgeConstants;
28 import com.android.layoutlib.bridge.MockView;
29 import com.android.layoutlib.bridge.android.BridgeContext;
30 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
31 import com.android.layoutlib.bridge.android.UnresolvedResourceValue;
32 import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil;
33 import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
34 import com.android.layoutlib.bridge.impl.ParserFactory;
35 import com.android.layoutlib.bridge.util.ReflectionUtils;
36 import com.android.resources.ResourceType;
37 import com.android.tools.layoutlib.annotations.NotNull;
38 import com.android.tools.layoutlib.annotations.Nullable;
39 
40 import org.xmlpull.v1.XmlPullParser;
41 
42 import android.annotation.NonNull;
43 import android.content.Context;
44 import android.content.res.TypedArray;
45 import android.graphics.drawable.Animatable;
46 import android.graphics.drawable.Drawable;
47 import android.util.AttributeSet;
48 import android.util.ResolvingAttributeSet;
49 import android.view.View.OnAttachStateChangeListener;
50 import android.widget.ImageView;
51 import android.widget.NumberPicker;
52 
53 import java.lang.reflect.Constructor;
54 import java.lang.reflect.InvocationTargetException;
55 import java.lang.reflect.Method;
56 import java.util.HashMap;
57 import java.util.Map;
58 import java.util.function.BiFunction;
59 
60 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
61 
62 /**
63  * Custom implementation of {@link LayoutInflater} to handle custom views.
64  */
65 public final class BridgeInflater extends LayoutInflater {
66     private static final String INFLATER_CLASS_ATTR_NAME = "viewInflaterClass";
67     private static final ResourceReference RES_AUTO_INFLATER_CLASS_ATTR =
68             ResourceReference.attr(ResourceNamespace.RES_AUTO, INFLATER_CLASS_ATTR_NAME);
69     private static final ResourceReference LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR =
70             ResourceReference.attr(ResourceNamespace.APPCOMPAT_LEGACY, INFLATER_CLASS_ATTR_NAME);
71     private static final ResourceReference ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR =
72             ResourceReference.attr(ResourceNamespace.APPCOMPAT, INFLATER_CLASS_ATTR_NAME);
73     private static final String LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME =
74             "android.support.v7.app.AppCompatViewInflater";
75     private static final String ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME =
76             "androidx.appcompat.app.AppCompatViewInflater";
77     private final LayoutlibCallback mLayoutlibCallback;
78 
79     private boolean mIsInMerge = false;
80     private ResourceReference mResourceReference;
81     private Map<View, String> mOpenDrawerLayouts;
82 
83     // Keep in sync with the same value in LayoutInflater.
84     private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
85 
86     /**
87      * List of class prefixes which are tried first by default.
88      * <p/>
89      * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater.
90      */
91     private static final String[] sClassPrefixList = {
92         "android.widget.",
93         "android.webkit.",
94         "android.app."
95     };
96     private BiFunction<String, AttributeSet, View> mCustomInflater;
97 
getClassPrefixList()98     public static String[] getClassPrefixList() {
99         return sClassPrefixList;
100     }
101 
BridgeInflater(LayoutInflater original, Context newContext)102     private BridgeInflater(LayoutInflater original, Context newContext) {
103         super(original, newContext);
104         newContext = getBaseContext(newContext);
105         mLayoutlibCallback = (newContext instanceof BridgeContext) ?
106                 ((BridgeContext) newContext).getLayoutlibCallback() :
107                 null;
108     }
109 
110     /**
111      * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object.
112      *
113      * @param context The Android application context.
114      * @param layoutlibCallback the {@link LayoutlibCallback} object.
115      */
BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback)116     public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
117         super(context);
118         mLayoutlibCallback = layoutlibCallback;
119         mConstructorArgs[0] = context;
120     }
121 
122     @Override
onCreateView(String name, AttributeSet attrs)123     public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
124         View view = createViewFromCustomInflater(name, attrs);
125 
126         if (view == null) {
127             try {
128                 // First try to find a class using the default Android prefixes
129                 for (String prefix : sClassPrefixList) {
130                     try {
131                         view = createView(name, prefix, attrs);
132                         if (view != null) {
133                             break;
134                         }
135                     } catch (ClassNotFoundException e) {
136                         // Ignore. We'll try again using the base class below.
137                     }
138                 }
139 
140                 // Next try using the parent loader. This will most likely only work for
141                 // fully-qualified class names.
142                 try {
143                     if (view == null) {
144                         view = super.onCreateView(name, attrs);
145                     }
146                 } catch (ClassNotFoundException e) {
147                     // Ignore. We'll try again using the custom view loader below.
148                 }
149 
150                 // Finally try again using the custom view loader
151                 if (view == null) {
152                     view = loadCustomView(name, attrs);
153                 }
154             } catch (InflateException e) {
155                 // Don't catch the InflateException below as that results in hiding the real cause.
156                 throw e;
157             } catch (Exception e) {
158                 // Wrap the real exception in a ClassNotFoundException, so that the calling method
159                 // can deal with it.
160                 throw new ClassNotFoundException("onCreateView", e);
161             }
162         }
163 
164         setupViewInContext(view, attrs);
165 
166         return view;
167     }
168 
169     /**
170      * Finds the createView method in the given customInflaterClass. Since createView is
171      * currently package protected, it will show in the declared class so we iterate up the
172      * hierarchy and return the first instance we find.
173      * The returned method will be accessible.
174      */
175     @NotNull
getCreateViewMethod(Class<?> customInflaterClass)176     private static Method getCreateViewMethod(Class<?> customInflaterClass) throws NoSuchMethodException {
177         Class<?> current = customInflaterClass;
178         do {
179             try {
180                 Method method = current.getDeclaredMethod("createView", View.class, String.class,
181                                 Context.class, AttributeSet.class, boolean.class, boolean.class,
182                                 boolean.class, boolean.class);
183                 method.setAccessible(true);
184                 return method;
185             } catch (NoSuchMethodException ignore) {
186             }
187             current = current.getSuperclass();
188         } while (current != null && current != Object.class);
189 
190         throw new NoSuchMethodException();
191     }
192 
193     /**
194      * Finds the custom inflater class. If it's defined in the theme, we'll use that one (if the
195      * class does not exist, null is returned).
196      * If {@code viewInflaterClass} is not defined in the theme, we'll try to instantiate
197      * {@code android.support.v7.app.AppCompatViewInflater}
198      */
199     @Nullable
findCustomInflater(@otNull BridgeContext bc, @NotNull LayoutlibCallback layoutlibCallback)200     private static Class<?> findCustomInflater(@NotNull BridgeContext bc,
201             @NotNull LayoutlibCallback layoutlibCallback) {
202         ResourceReference attrRef;
203         if (layoutlibCallback.isResourceNamespacingRequired()) {
204             if (layoutlibCallback.hasLegacyAppCompat()) {
205                 attrRef = LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR;
206             } else if (layoutlibCallback.hasAndroidXAppCompat()) {
207                 attrRef = ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR;
208             } else {
209                 return null;
210             }
211         } else {
212             attrRef = RES_AUTO_INFLATER_CLASS_ATTR;
213         }
214         ResourceValue value = bc.getRenderResources().findItemInTheme(attrRef);
215         String inflaterName = value != null ? value.getValue() : null;
216 
217         if (inflaterName != null) {
218             try {
219                 return layoutlibCallback.findClass(inflaterName);
220             } catch (ClassNotFoundException ignore) {
221             }
222 
223             // viewInflaterClass was defined but we couldn't find the class.
224         } else if (bc.isAppCompatTheme()) {
225             // Older versions of AppCompat do not define the viewInflaterClass so try to get it
226             // manually.
227             try {
228                 if (layoutlibCallback.hasLegacyAppCompat()) {
229                     return layoutlibCallback.findClass(LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME);
230                 } else if (layoutlibCallback.hasAndroidXAppCompat()) {
231                     return layoutlibCallback.findClass(ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME);
232                 }
233             } catch (ClassNotFoundException ignore) {
234             }
235         }
236 
237         return null;
238     }
239 
240     /**
241      * Checks if there is a custom inflater and, when present, tries to instantiate the view
242      * using it.
243      */
244     @Nullable
createViewFromCustomInflater(@otNull String name, @NotNull AttributeSet attrs)245     private View createViewFromCustomInflater(@NotNull String name, @NotNull AttributeSet attrs) {
246         if (mCustomInflater == null) {
247             Context context = getContext();
248             context = getBaseContext(context);
249             if (context instanceof BridgeContext) {
250                 BridgeContext bc = (BridgeContext) context;
251                 Class<?> inflaterClass = findCustomInflater(bc, mLayoutlibCallback);
252 
253                 if (inflaterClass != null) {
254                     try {
255                         Constructor<?> constructor =  inflaterClass.getDeclaredConstructor();
256                         constructor.setAccessible(true);
257                         Object inflater = constructor.newInstance();
258                         Method method = getCreateViewMethod(inflaterClass);
259                         Context finalContext = context;
260                         mCustomInflater = (viewName, attributeSet) -> {
261                             try {
262                                 return (View) method.invoke(inflater, null, viewName, finalContext,
263                                         attributeSet,
264                                         false,
265                                         false /*readAndroidTheme*/, // No need after L
266                                         true /*readAppTheme*/,
267                                         true /*wrapContext*/);
268                             } catch (IllegalAccessException | InvocationTargetException e) {
269                                 assert false : "Call to createView failed";
270                             }
271                             return null;
272                         };
273                     } catch (InvocationTargetException | IllegalAccessException |
274                             NoSuchMethodException | InstantiationException ignore) {
275                     }
276                 }
277             }
278 
279             if (mCustomInflater == null) {
280                 // There is no custom inflater. We'll create a nop custom inflater to avoid the
281                 // penalty of trying to instantiate again
282                 mCustomInflater = (s, attributeSet) -> null;
283             }
284         }
285 
286         return mCustomInflater.apply(name, attrs);
287     }
288 
289     @Override
createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)290     public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
291             boolean ignoreThemeAttr) {
292         View view = null;
293         if (name.equals("view")) {
294             // This is usually done by the superclass but this allows us catching the error and
295             // reporting something useful.
296             name = attrs.getAttributeValue(null, "class");
297 
298             if (name == null) {
299                 Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Unable to inflate view tag without " +
300                   "class attribute", null);
301                 // We weren't able to resolve the view so we just pass a mock View to be able to
302                 // continue rendering.
303                 view = new MockView(context, attrs);
304                 ((MockView) view).setText("view");
305             }
306         }
307 
308         try {
309             if (view == null) {
310                 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr);
311             }
312         } catch (InflateException e) {
313             // Creation of ContextThemeWrapper code is same as in the super method.
314             // Apply a theme wrapper, if allowed and one is specified.
315             if (!ignoreThemeAttr) {
316                 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
317                 final int themeResId = ta.getResourceId(0, 0);
318                 if (themeResId != 0) {
319                     context = new ContextThemeWrapper(context, themeResId);
320                 }
321                 ta.recycle();
322             }
323             if (!(e.getCause() instanceof ClassNotFoundException)) {
324                 // There is some unknown inflation exception in inflating a View that was found.
325                 view = new MockView(context, attrs);
326                 ((MockView) view).setText(name);
327                 Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null);
328             } else {
329                 final Object lastContext = mConstructorArgs[0];
330                 mConstructorArgs[0] = context;
331                 // try to load the class from using the custom view loader
332                 try {
333                     view = loadCustomView(name, attrs);
334                 } catch (Exception e2) {
335                     // Wrap the real exception in an InflateException so that the calling
336                     // method can deal with it.
337                     InflateException exception = new InflateException();
338                     if (!e2.getClass().equals(ClassNotFoundException.class)) {
339                         exception.initCause(e2);
340                     } else {
341                         exception.initCause(e);
342                     }
343                     throw exception;
344                 } finally {
345                     mConstructorArgs[0] = lastContext;
346                 }
347             }
348         }
349 
350         setupViewInContext(view, attrs);
351 
352         return view;
353     }
354 
355     @Override
inflate(int resource, ViewGroup root)356     public View inflate(int resource, ViewGroup root) {
357         Context context = getContext();
358         context = getBaseContext(context);
359         if (context instanceof BridgeContext) {
360             BridgeContext bridgeContext = (BridgeContext)context;
361 
362             ResourceValue value = null;
363 
364             ResourceReference layoutInfo = Bridge.resolveResourceId(resource);
365             if (layoutInfo == null) {
366                 layoutInfo = mLayoutlibCallback.resolveResourceId(resource);
367 
368             }
369             if (layoutInfo != null) {
370                 value = bridgeContext.getRenderResources().getResolvedResource(layoutInfo);
371             }
372 
373             if (value != null) {
374                 String path = value.getValue();
375                 try {
376                     XmlPullParser parser = ParserFactory.create(path, true);
377                     if (parser == null) {
378                         return null;
379                     }
380 
381                     BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
382                             parser, bridgeContext, value.getNamespace());
383 
384                     return inflate(bridgeParser, root);
385                 } catch (Exception e) {
386                     Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
387                             "Failed to parse file " + path, e, null);
388 
389                     return null;
390                 }
391             }
392         }
393         return null;
394     }
395 
396     /**
397      * Instantiates the given view name and returns the instance. If the view doesn't exist, a
398      * MockView or null might be returned.
399      * @param name the custom view name
400      * @param attrs the {@link AttributeSet} to be passed to the view constructor
401      * @param silent if true, errors while loading the view won't be reported and, if the view
402      * doesn't exist, null will be returned.
403      */
loadCustomView(String name, AttributeSet attrs, boolean silent)404     private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception {
405         if (mLayoutlibCallback != null) {
406             // first get the classname in case it's not the node name
407             if (name.equals("view")) {
408                 name = attrs.getAttributeValue(null, "class");
409                 if (name == null) {
410                     return null;
411                 }
412             }
413 
414             mConstructorArgs[1] = attrs;
415 
416             Object customView = silent ?
417                     mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs)
418                     : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs);
419 
420             if (customView instanceof View) {
421                 return (View)customView;
422             }
423         }
424 
425         return null;
426     }
427 
loadCustomView(String name, AttributeSet attrs)428     private View loadCustomView(String name, AttributeSet attrs) throws Exception {
429         return loadCustomView(name, attrs, false);
430     }
431 
setupViewInContext(View view, AttributeSet attrs)432     private void setupViewInContext(View view, AttributeSet attrs) {
433         Context context = getContext();
434         context = getBaseContext(context);
435         if (!(context instanceof BridgeContext)) {
436             return;
437         }
438 
439         BridgeContext bc = (BridgeContext) context;
440         // get the view key
441         Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge);
442         if (viewKey != null) {
443             bc.addViewKey(view, viewKey);
444         }
445         String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX");
446         if (scrollPosX != null && scrollPosX.endsWith("px")) {
447             int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2));
448             bc.setScrollXPos(view, value);
449         }
450         String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY");
451         if (scrollPosY != null && scrollPosY.endsWith("px")) {
452             int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2));
453             bc.setScrollYPos(view, value);
454         }
455         if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
456             int resourceId = 0;
457             int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI,
458                     BridgeConstants.ATTR_ITEM_COUNT, -1);
459             if (attrs instanceof ResolvingAttributeSet) {
460                 ResourceValue attrListItemValue =
461                         ((ResolvingAttributeSet) attrs).getResolvedAttributeValue(
462                                 BridgeConstants.NS_TOOLS_URI, BridgeConstants.ATTR_LIST_ITEM);
463                 if (attrListItemValue != null) {
464                     resourceId = bc.getResourceId(attrListItemValue.asReference(), 0);
465                 }
466             }
467             RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue);
468         } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) {
469             String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
470                     BridgeConstants.ATTR_OPEN_DRAWER);
471             if (attrVal != null) {
472                 getDrawerLayoutMap().put(view, attrVal);
473             }
474         }
475         else if (view instanceof NumberPicker) {
476             NumberPicker numberPicker = (NumberPicker) view;
477             String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue");
478             if (minValue != null) {
479                 numberPicker.setMinValue(Integer.parseInt(minValue));
480             }
481             String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue");
482             if (maxValue != null) {
483                 numberPicker.setMaxValue(Integer.parseInt(maxValue));
484             }
485         }
486         else if (view instanceof ImageView) {
487             ImageView img = (ImageView) view;
488             Drawable drawable = img.getDrawable();
489             if (drawable instanceof Animatable) {
490                 if (!((Animatable) drawable).isRunning()) {
491                     ((Animatable) drawable).start();
492                 }
493             }
494         }
495         else if (view instanceof ViewStub) {
496             // By default, ViewStub will be set to GONE and won't be inflate. If the XML has the
497             // tools:visibility attribute we'll workaround that behavior.
498             String visibility = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
499                     SdkConstants.ATTR_VISIBILITY);
500 
501             boolean isVisible = "visible".equals(visibility);
502             if (isVisible || "invisible".equals(visibility)) {
503                 // We can not inflate the view until is attached to its parent so we need to delay
504                 // the setVisible call until after that happens.
505                 final int visibilityValue = isVisible ? View.VISIBLE : View.INVISIBLE;
506                 view.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
507                     @Override
508                     public void onViewAttachedToWindow(View v) {
509                         v.removeOnAttachStateChangeListener(this);
510                         view.setVisibility(visibilityValue);
511                     }
512 
513                     @Override
514                     public void onViewDetachedFromWindow(View v) {}
515                 });
516             }
517         }
518 
519     }
520 
setIsInMerge(boolean isInMerge)521     public void setIsInMerge(boolean isInMerge) {
522         mIsInMerge = isInMerge;
523     }
524 
setResourceReference(ResourceReference reference)525     public void setResourceReference(ResourceReference reference) {
526         mResourceReference = reference;
527     }
528 
529     @Override
cloneInContext(Context newContext)530     public LayoutInflater cloneInContext(Context newContext) {
531         return new BridgeInflater(this, newContext);
532     }
533 
getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, ResourceReference resourceReference, boolean isInMerge)534     /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc,
535             ResourceReference resourceReference, boolean isInMerge) {
536 
537         if (!(attrs instanceof BridgeXmlBlockParser)) {
538             return null;
539         }
540         BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs);
541 
542         // get the view key
543         Object viewKey = parser.getViewCookie();
544 
545         if (viewKey == null) {
546             int currentDepth = parser.getDepth();
547 
548             // test whether we are in an included file or in a adapter binding view.
549             BridgeXmlBlockParser previousParser = bc.getPreviousParser();
550             if (previousParser != null) {
551                 // looks like we are inside an embedded layout.
552                 // only apply the cookie of the calling node (<include>) if we are at the
553                 // top level of the embedded layout. If there is a merge tag, then
554                 // skip it and look for the 2nd level
555                 int testDepth = isInMerge ? 2 : 1;
556                 if (currentDepth == testDepth) {
557                     viewKey = previousParser.getViewCookie();
558                     // if we are in a merge, wrap the cookie in a MergeCookie.
559                     if (viewKey != null && isInMerge) {
560                         viewKey = new MergeCookie(viewKey);
561                     }
562                 }
563             } else if (resourceReference != null && currentDepth == 1) {
564                 // else if there's a resource reference, this means we are in an adapter
565                 // binding case. Set the resource ref as the view cookie only for the top
566                 // level view.
567                 viewKey = resourceReference;
568             }
569         }
570 
571         return viewKey;
572     }
573 
postInflateProcess(View view)574     public void postInflateProcess(View view) {
575         if (mOpenDrawerLayouts != null) {
576             String gravity = mOpenDrawerLayouts.get(view);
577             if (gravity != null) {
578                 DrawerLayoutUtil.openDrawer(view, gravity);
579             }
580             mOpenDrawerLayouts.remove(view);
581         }
582     }
583 
584     @NonNull
getDrawerLayoutMap()585     private Map<View, String> getDrawerLayoutMap() {
586         if (mOpenDrawerLayouts == null) {
587             mOpenDrawerLayouts = new HashMap<>(4);
588         }
589         return mOpenDrawerLayouts;
590     }
591 
onDoneInflation()592     public void onDoneInflation() {
593         if (mOpenDrawerLayouts != null) {
594             mOpenDrawerLayouts.clear();
595         }
596     }
597 }
598