1 /*
2  * Copyright (C) 2010 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.app.admin;
18 
19 import android.annotation.NonNull;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.pm.ActivityInfo;
23 import android.content.pm.PackageManager;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.content.pm.ResolveInfo;
26 import android.content.res.Resources;
27 import android.content.res.Resources.NotFoundException;
28 import android.content.res.TypedArray;
29 import android.content.res.XmlResourceParser;
30 import android.graphics.drawable.Drawable;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.os.PersistableBundle;
34 import android.util.AttributeSet;
35 import android.util.Log;
36 import android.util.Printer;
37 import android.util.SparseArray;
38 import android.util.Xml;
39 
40 import org.xmlpull.v1.XmlPullParser;
41 import org.xmlpull.v1.XmlPullParserException;
42 import org.xmlpull.v1.XmlSerializer;
43 
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.HashMap;
47 
48 /**
49  * This class is used to specify meta information of a device administrator
50  * component.
51  */
52 public final class DeviceAdminInfo implements Parcelable {
53     static final String TAG = "DeviceAdminInfo";
54 
55     /**
56      * A type of policy that this device admin can use: device owner meta-policy
57      * for an admin that is designated as owner of the device.
58      *
59      * @hide
60      */
61     public static final int USES_POLICY_DEVICE_OWNER = -2;
62 
63     /**
64      * A type of policy that this device admin can use: profile owner meta-policy
65      * for admins that have been installed as owner of some user profile.
66      *
67      * @hide
68      */
69     public static final int USES_POLICY_PROFILE_OWNER = -1;
70 
71     /**
72      * A type of policy that this device admin can use: limit the passwords
73      * that the user can select, via {@link DevicePolicyManager#setPasswordQuality}
74      * and {@link DevicePolicyManager#setPasswordMinimumLength}.
75      *
76      * <p>To control this policy, the device admin must have a "limit-password"
77      * tag in the "uses-policies" section of its meta-data.
78      *
79      * <p>This policy is deprecated for use by a device admin.  In future releases, it will
80      * only be possible for a device owner or profile owner to enforce constraints on user
81      * passwords.
82      */
83     public static final int USES_POLICY_LIMIT_PASSWORD = 0;
84 
85     /**
86      * A type of policy that this device admin can use: able to watch login
87      * attempts from the user, via {@link DeviceAdminReceiver#ACTION_PASSWORD_FAILED},
88      * {@link DeviceAdminReceiver#ACTION_PASSWORD_SUCCEEDED}, and
89      * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts}.
90      *
91      * <p>To control this policy, the device admin must have a "watch-login"
92      * tag in the "uses-policies" section of its meta-data.
93      */
94     public static final int USES_POLICY_WATCH_LOGIN = 1;
95 
96     /**
97      * A type of policy that this device admin can use: able to reset the
98      * user's password via
99      * {@link DevicePolicyManager#resetPassword}.
100      *
101      * <p>To control this policy, the device admin must have a "reset-password"
102      * tag in the "uses-policies" section of its meta-data.
103      */
104     public static final int USES_POLICY_RESET_PASSWORD = 2;
105 
106     /**
107      * A type of policy that this device admin can use: able to force the device
108      * to lock via{@link DevicePolicyManager#lockNow} or limit the
109      * maximum lock timeout for the device via
110      * {@link DevicePolicyManager#setMaximumTimeToLock}.
111      *
112      * <p>To control this policy, the device admin must have a "force-lock"
113      * tag in the "uses-policies" section of its meta-data.
114      */
115     public static final int USES_POLICY_FORCE_LOCK = 3;
116 
117     /**
118      * A type of policy that this device admin can use: able to factory
119      * reset the device, erasing all of the user's data, via
120      * {@link DevicePolicyManager#wipeData}.
121      *
122      * <p>To control this policy, the device admin must have a "wipe-data"
123      * tag in the "uses-policies" section of its meta-data.
124      */
125     public static final int USES_POLICY_WIPE_DATA = 4;
126 
127     /**
128      * A type of policy that this device admin can use: able to specify the
129      * device Global Proxy, via {@link DevicePolicyManager#setGlobalProxy}.
130      *
131      * <p>To control this policy, the device admin must have a "set-global-proxy"
132      * tag in the "uses-policies" section of its meta-data.
133      * @hide
134      */
135     public static final int USES_POLICY_SETS_GLOBAL_PROXY = 5;
136 
137     /**
138      * A type of policy that this device admin can use: force the user to
139      * change their password after an administrator-defined time limit.
140      *
141      * <p>To control this policy, the device admin must have an "expire-password"
142      * tag in the "uses-policies" section of its meta-data.
143      *
144      * <p>This policy is deprecated for use by a device admin.  In future releases, it will
145      * only be possible for a device owner or profile owner to enforce password expiry.
146      */
147     public static final int USES_POLICY_EXPIRE_PASSWORD = 6;
148 
149     /**
150      * A type of policy that this device admin can use: require encryption of stored data.
151      *
152      * <p>To control this policy, the device admin must have a "encrypted-storage"
153      * tag in the "uses-policies" section of its meta-data.
154      */
155     public static final int USES_ENCRYPTED_STORAGE = 7;
156 
157     /**
158      * A type of policy that this device admin can use: disables use of all device cameras.
159      *
160      * <p>To control this policy, the device admin must have a "disable-camera"
161      * tag in the "uses-policies" section of its meta-data.
162      *
163      * <p>This policy is deprecated for use by a device admin.  In future releases, it will
164      * only be possible for a device owner or profile owner to disable use of the camera.
165      */
166     public static final int USES_POLICY_DISABLE_CAMERA = 8;
167 
168     /**
169      * A type of policy that this device admin can use: disables use of keyguard features.
170      *
171      * <p>To control this policy, the device admin must have a "disable-keyguard-features"
172      * tag in the "uses-policies" section of its meta-data.
173      *
174      * <p>This policy is deprecated for use by a device admin.  In future releases, it will
175      * only be possible for a device owner or profile owner to disable use of keyguard
176      * features.
177      */
178     public static final int USES_POLICY_DISABLE_KEYGUARD_FEATURES = 9;
179 
180     /** @hide */
181     public static class PolicyInfo {
182         public final int ident;
183         public final String tag;
184         public final int label;
185         public final int description;
186         public final int labelForSecondaryUsers;
187         public final int descriptionForSecondaryUsers;
188 
PolicyInfo(int ident, String tag, int label, int description)189         public PolicyInfo(int ident, String tag, int label, int description) {
190             this(ident, tag, label, description, label, description);
191         }
192 
PolicyInfo(int ident, String tag, int label, int description, int labelForSecondaryUsers, int descriptionForSecondaryUsers)193         public PolicyInfo(int ident, String tag, int label, int description,
194                 int labelForSecondaryUsers, int descriptionForSecondaryUsers) {
195             this.ident = ident;
196             this.tag = tag;
197             this.label = label;
198             this.description = description;
199             this.labelForSecondaryUsers = labelForSecondaryUsers;
200             this.descriptionForSecondaryUsers = descriptionForSecondaryUsers;
201         }
202     }
203 
204     static ArrayList<PolicyInfo> sPoliciesDisplayOrder = new ArrayList<PolicyInfo>();
205     static HashMap<String, Integer> sKnownPolicies = new HashMap<String, Integer>();
206     static SparseArray<PolicyInfo> sRevKnownPolicies = new SparseArray<PolicyInfo>();
207 
208     static {
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WIPE_DATA, "wipe-data", com.android.internal.R.string.policylab_wipeData, com.android.internal.R.string.policydesc_wipeData, com.android.internal.R.string.policylab_wipeData_secondaryUser, com.android.internal.R.string.policydesc_wipeData_secondaryUser ))209         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WIPE_DATA, "wipe-data",
210                 com.android.internal.R.string.policylab_wipeData,
211                 com.android.internal.R.string.policydesc_wipeData,
212                 com.android.internal.R.string.policylab_wipeData_secondaryUser,
213                 com.android.internal.R.string.policydesc_wipeData_secondaryUser
214                 ));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_RESET_PASSWORD, "reset-password", com.android.internal.R.string.policylab_resetPassword, com.android.internal.R.string.policydesc_resetPassword))215         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_RESET_PASSWORD, "reset-password",
216                 com.android.internal.R.string.policylab_resetPassword,
217                 com.android.internal.R.string.policydesc_resetPassword));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_LIMIT_PASSWORD, "limit-password", com.android.internal.R.string.policylab_limitPassword, com.android.internal.R.string.policydesc_limitPassword))218         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_LIMIT_PASSWORD, "limit-password",
219                 com.android.internal.R.string.policylab_limitPassword,
220                 com.android.internal.R.string.policydesc_limitPassword));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WATCH_LOGIN, "watch-login", com.android.internal.R.string.policylab_watchLogin, com.android.internal.R.string.policydesc_watchLogin, com.android.internal.R.string.policylab_watchLogin, com.android.internal.R.string.policydesc_watchLogin_secondaryUser ))221         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WATCH_LOGIN, "watch-login",
222                 com.android.internal.R.string.policylab_watchLogin,
223                 com.android.internal.R.string.policydesc_watchLogin,
224                 com.android.internal.R.string.policylab_watchLogin,
225                 com.android.internal.R.string.policydesc_watchLogin_secondaryUser
226         ));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_FORCE_LOCK, "force-lock", com.android.internal.R.string.policylab_forceLock, com.android.internal.R.string.policydesc_forceLock))227         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_FORCE_LOCK, "force-lock",
228                 com.android.internal.R.string.policylab_forceLock,
229                 com.android.internal.R.string.policydesc_forceLock));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_SETS_GLOBAL_PROXY, "set-global-proxy", com.android.internal.R.string.policylab_setGlobalProxy, com.android.internal.R.string.policydesc_setGlobalProxy))230         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_SETS_GLOBAL_PROXY, "set-global-proxy",
231                 com.android.internal.R.string.policylab_setGlobalProxy,
232                 com.android.internal.R.string.policydesc_setGlobalProxy));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_EXPIRE_PASSWORD, "expire-password", com.android.internal.R.string.policylab_expirePassword, com.android.internal.R.string.policydesc_expirePassword))233         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_EXPIRE_PASSWORD, "expire-password",
234                 com.android.internal.R.string.policylab_expirePassword,
235                 com.android.internal.R.string.policydesc_expirePassword));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_ENCRYPTED_STORAGE, "encrypted-storage", com.android.internal.R.string.policylab_encryptedStorage, com.android.internal.R.string.policydesc_encryptedStorage))236         sPoliciesDisplayOrder.add(new PolicyInfo(USES_ENCRYPTED_STORAGE, "encrypted-storage",
237                 com.android.internal.R.string.policylab_encryptedStorage,
238                 com.android.internal.R.string.policydesc_encryptedStorage));
sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_DISABLE_CAMERA, "disable-camera", com.android.internal.R.string.policylab_disableCamera, com.android.internal.R.string.policydesc_disableCamera))239         sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_DISABLE_CAMERA, "disable-camera",
240                 com.android.internal.R.string.policylab_disableCamera,
241                 com.android.internal.R.string.policydesc_disableCamera));
sPoliciesDisplayOrder.add(new PolicyInfo( USES_POLICY_DISABLE_KEYGUARD_FEATURES, "disable-keyguard-features", com.android.internal.R.string.policylab_disableKeyguardFeatures, com.android.internal.R.string.policydesc_disableKeyguardFeatures))242         sPoliciesDisplayOrder.add(new PolicyInfo(
243                 USES_POLICY_DISABLE_KEYGUARD_FEATURES, "disable-keyguard-features",
244                 com.android.internal.R.string.policylab_disableKeyguardFeatures,
245                 com.android.internal.R.string.policydesc_disableKeyguardFeatures));
246 
247         for (int i=0; i<sPoliciesDisplayOrder.size(); i++) {
248             PolicyInfo pi = sPoliciesDisplayOrder.get(i);
sRevKnownPolicies.put(pi.ident, pi)249             sRevKnownPolicies.put(pi.ident, pi);
sKnownPolicies.put(pi.tag, pi.ident)250             sKnownPolicies.put(pi.tag, pi.ident);
251         }
252     }
253 
254     /**
255      * The BroadcastReceiver that implements this device admin component.
256      */
257     final ActivityInfo mActivityInfo;
258 
259     /**
260      * Whether this should be visible to the user.
261      */
262     boolean mVisible;
263 
264     /**
265      * The policies this administrator needs access to.
266      */
267     int mUsesPolicies;
268 
269     /**
270      * Whether this administrator can be a target in an ownership transfer.
271      *
272      * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
273      */
274     boolean mSupportsTransferOwnership;
275 
276     /**
277      * Constructor.
278      *
279      * @param context The Context in which we are parsing the device admin.
280      * @param resolveInfo The ResolveInfo returned from the package manager about
281      * this device admin's component.
282      */
DeviceAdminInfo(Context context, ResolveInfo resolveInfo)283     public DeviceAdminInfo(Context context, ResolveInfo resolveInfo)
284             throws XmlPullParserException, IOException {
285         this(context, resolveInfo.activityInfo);
286     }
287     /**
288      * Constructor.
289      *
290      * @param context The Context in which we are parsing the device admin.
291      * @param activityInfo The ActivityInfo returned from the package manager about
292      * this device admin's component.
293      *
294      * @hide
295      */
DeviceAdminInfo(Context context, ActivityInfo activityInfo)296     public DeviceAdminInfo(Context context, ActivityInfo activityInfo)
297             throws XmlPullParserException, IOException {
298         mActivityInfo = activityInfo;
299 
300         PackageManager pm = context.getPackageManager();
301 
302         XmlResourceParser parser = null;
303         try {
304             parser = mActivityInfo.loadXmlMetaData(pm, DeviceAdminReceiver.DEVICE_ADMIN_META_DATA);
305             if (parser == null) {
306                 throw new XmlPullParserException("No "
307                         + DeviceAdminReceiver.DEVICE_ADMIN_META_DATA + " meta-data");
308             }
309 
310             Resources res = pm.getResourcesForApplication(mActivityInfo.applicationInfo);
311 
312             AttributeSet attrs = Xml.asAttributeSet(parser);
313 
314             int type;
315             while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
316                     && type != XmlPullParser.START_TAG) {
317             }
318 
319             String nodeName = parser.getName();
320             if (!"device-admin".equals(nodeName)) {
321                 throw new XmlPullParserException(
322                         "Meta-data does not start with device-admin tag");
323             }
324 
325             TypedArray sa = res.obtainAttributes(attrs,
326                     com.android.internal.R.styleable.DeviceAdmin);
327 
328             mVisible = sa.getBoolean(
329                     com.android.internal.R.styleable.DeviceAdmin_visible, true);
330 
331             sa.recycle();
332 
333             int outerDepth = parser.getDepth();
334             while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
335                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
336                 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
337                     continue;
338                 }
339                 String tagName = parser.getName();
340                 if (tagName.equals("uses-policies")) {
341                     int innerDepth = parser.getDepth();
342                     while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
343                            && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
344                         if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
345                             continue;
346                         }
347                         String policyName = parser.getName();
348                         Integer val = sKnownPolicies.get(policyName);
349                         if (val != null) {
350                             mUsesPolicies |= 1 << val.intValue();
351                         } else {
352                             Log.w(TAG, "Unknown tag under uses-policies of "
353                                     + getComponent() + ": " + policyName);
354                         }
355                     }
356                 } else if (tagName.equals("support-transfer-ownership")) {
357                     if (parser.next() != XmlPullParser.END_TAG) {
358                         throw new XmlPullParserException(
359                                 "support-transfer-ownership tag must be empty.");
360                     }
361                     mSupportsTransferOwnership = true;
362                 }
363             }
364         } catch (NameNotFoundException e) {
365             throw new XmlPullParserException(
366                     "Unable to create context for: " + mActivityInfo.packageName);
367         } finally {
368             if (parser != null) parser.close();
369         }
370     }
371 
DeviceAdminInfo(Parcel source)372     DeviceAdminInfo(Parcel source) {
373         mActivityInfo = ActivityInfo.CREATOR.createFromParcel(source);
374         mUsesPolicies = source.readInt();
375         mSupportsTransferOwnership = source.readBoolean();
376     }
377 
378     /**
379      * Return the .apk package that implements this device admin.
380      */
getPackageName()381     public String getPackageName() {
382         return mActivityInfo.packageName;
383     }
384 
385     /**
386      * Return the class name of the receiver component that implements
387      * this device admin.
388      */
getReceiverName()389     public String getReceiverName() {
390         return mActivityInfo.name;
391     }
392 
393     /**
394      * Return the raw information about the receiver implementing this
395      * device admin.  Do not modify the returned object.
396      */
getActivityInfo()397     public ActivityInfo getActivityInfo() {
398         return mActivityInfo;
399     }
400 
401     /**
402      * Return the component of the receiver that implements this device admin.
403      */
404     @NonNull
getComponent()405     public ComponentName getComponent() {
406         return new ComponentName(mActivityInfo.packageName,
407                 mActivityInfo.name);
408     }
409 
410     /**
411      * Load the user-displayed label for this device admin.
412      *
413      * @param pm Supply a PackageManager used to load the device admin's
414      * resources.
415      */
loadLabel(PackageManager pm)416     public CharSequence loadLabel(PackageManager pm) {
417         return mActivityInfo.loadLabel(pm);
418     }
419 
420     /**
421      * Load user-visible description associated with this device admin.
422      *
423      * @param pm Supply a PackageManager used to load the device admin's
424      * resources.
425      */
loadDescription(PackageManager pm)426     public CharSequence loadDescription(PackageManager pm) throws NotFoundException {
427         if (mActivityInfo.descriptionRes != 0) {
428             return pm.getText(mActivityInfo.packageName,
429                     mActivityInfo.descriptionRes, mActivityInfo.applicationInfo);
430         }
431         throw new NotFoundException();
432     }
433 
434     /**
435      * Load the user-displayed icon for this device admin.
436      *
437      * @param pm Supply a PackageManager used to load the device admin's
438      * resources.
439      */
loadIcon(PackageManager pm)440     public Drawable loadIcon(PackageManager pm) {
441         return mActivityInfo.loadIcon(pm);
442     }
443 
444     /**
445      * Returns whether this device admin would like to be visible to the
446      * user, even when it is not enabled.
447      */
isVisible()448     public boolean isVisible() {
449         return mVisible;
450     }
451 
452     /**
453      * Return true if the device admin has requested that it be able to use
454      * the given policy control.  The possible policy identifier inputs are:
455      * {@link #USES_POLICY_LIMIT_PASSWORD}, {@link #USES_POLICY_WATCH_LOGIN},
456      * {@link #USES_POLICY_RESET_PASSWORD}, {@link #USES_POLICY_FORCE_LOCK},
457      * {@link #USES_POLICY_WIPE_DATA},
458      * {@link #USES_POLICY_EXPIRE_PASSWORD}, {@link #USES_ENCRYPTED_STORAGE},
459      * {@link #USES_POLICY_DISABLE_CAMERA}.
460      */
usesPolicy(int policyIdent)461     public boolean usesPolicy(int policyIdent) {
462         return (mUsesPolicies & (1<<policyIdent)) != 0;
463     }
464 
465     /**
466      * Return the XML tag name for the given policy identifier.  Valid identifiers
467      * are as per {@link #usesPolicy(int)}.  If the given identifier is not
468      * known, null is returned.
469      */
getTagForPolicy(int policyIdent)470     public String getTagForPolicy(int policyIdent) {
471         return sRevKnownPolicies.get(policyIdent).tag;
472     }
473 
474     /**
475      * Return true if this administrator can be a target in an ownership transfer.
476      */
supportsTransferOwnership()477     public boolean supportsTransferOwnership() {
478         return mSupportsTransferOwnership;
479     }
480 
481     /** @hide */
getUsedPolicies()482     public ArrayList<PolicyInfo> getUsedPolicies() {
483         ArrayList<PolicyInfo> res = new ArrayList<PolicyInfo>();
484         for (int i=0; i<sPoliciesDisplayOrder.size(); i++) {
485             PolicyInfo pi = sPoliciesDisplayOrder.get(i);
486             if (usesPolicy(pi.ident)) {
487                 res.add(pi);
488             }
489         }
490         return res;
491     }
492 
493     /** @hide */
writePoliciesToXml(XmlSerializer out)494     public void writePoliciesToXml(XmlSerializer out)
495             throws IllegalArgumentException, IllegalStateException, IOException {
496         out.attribute(null, "flags", Integer.toString(mUsesPolicies));
497     }
498 
499     /** @hide */
readPoliciesFromXml(XmlPullParser parser)500     public void readPoliciesFromXml(XmlPullParser parser)
501             throws XmlPullParserException, IOException {
502         mUsesPolicies = Integer.parseInt(
503                 parser.getAttributeValue(null, "flags"));
504     }
505 
dump(Printer pw, String prefix)506     public void dump(Printer pw, String prefix) {
507         pw.println(prefix + "Receiver:");
508         mActivityInfo.dump(pw, prefix + "  ");
509     }
510 
511     @Override
toString()512     public String toString() {
513         return "DeviceAdminInfo{" + mActivityInfo.name + "}";
514     }
515 
516     /**
517      * Used to package this object into a {@link Parcel}.
518      *
519      * @param dest The {@link Parcel} to be written.
520      * @param flags The flags used for parceling.
521      */
writeToParcel(Parcel dest, int flags)522     public void writeToParcel(Parcel dest, int flags) {
523         mActivityInfo.writeToParcel(dest, flags);
524         dest.writeInt(mUsesPolicies);
525         dest.writeBoolean(mSupportsTransferOwnership);
526     }
527 
528     /**
529      * Used to make this class parcelable.
530      */
531     public static final Parcelable.Creator<DeviceAdminInfo> CREATOR =
532             new Parcelable.Creator<DeviceAdminInfo>() {
533         public DeviceAdminInfo createFromParcel(Parcel source) {
534             return new DeviceAdminInfo(source);
535         }
536 
537         public DeviceAdminInfo[] newArray(int size) {
538             return new DeviceAdminInfo[size];
539         }
540     };
541 
describeContents()542     public int describeContents() {
543         return 0;
544     }
545 }
546