TrustManagerService.java revision 4e68f11672bdb2d11b0da5cef942cfc9bfabd696
1/*
2 * Copyright (C) 2014 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
17package com.android.server.trust;
18
19import com.android.internal.content.PackageMonitor;
20import com.android.internal.widget.LockPatternUtils;
21import com.android.server.SystemService;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25
26import android.Manifest;
27import android.app.ActivityManagerNative;
28import android.app.admin.DevicePolicyManager;
29import android.app.trust.ITrustListener;
30import android.app.trust.ITrustManager;
31import android.content.BroadcastReceiver;
32import android.content.ComponentName;
33import android.content.Context;
34import android.content.Intent;
35import android.content.IntentFilter;
36import android.content.pm.PackageManager;
37import android.content.pm.ResolveInfo;
38import android.content.pm.UserInfo;
39import android.content.res.Resources;
40import android.content.res.TypedArray;
41import android.content.res.XmlResourceParser;
42import android.graphics.drawable.Drawable;
43import android.os.DeadObjectException;
44import android.os.Handler;
45import android.os.IBinder;
46import android.os.Message;
47import android.os.RemoteException;
48import android.os.SystemClock;
49import android.os.UserHandle;
50import android.os.UserManager;
51import android.service.trust.TrustAgentService;
52import android.util.ArraySet;
53import android.util.AttributeSet;
54import android.util.Log;
55import android.util.Slog;
56import android.util.SparseBooleanArray;
57import android.util.Xml;
58
59import java.io.FileDescriptor;
60import java.io.IOException;
61import java.io.PrintWriter;
62import java.util.ArrayList;
63import java.util.List;
64
65/**
66 * Manages trust agents and trust listeners.
67 *
68 * It is responsible for binding to the enabled {@link android.service.trust.TrustAgentService}s
69 * of each user and notifies them about events that are relevant to them.
70 * It start and stops them based on the value of
71 * {@link com.android.internal.widget.LockPatternUtils#getEnabledTrustAgents(int)}.
72 *
73 * It also keeps a set of {@link android.app.trust.ITrustListener}s that are notified whenever the
74 * trust state changes for any user.
75 *
76 * Trust state and the setting of enabled agents is kept per user and each user has its own
77 * instance of a {@link android.service.trust.TrustAgentService}.
78 */
79public class TrustManagerService extends SystemService {
80
81    private static final boolean DEBUG = false;
82    private static final String TAG = "TrustManagerService";
83
84    private static final Intent TRUST_AGENT_INTENT =
85            new Intent(TrustAgentService.SERVICE_INTERFACE);
86    private static final String PERMISSION_PROVIDE_AGENT = Manifest.permission.PROVIDE_TRUST_AGENT;
87
88    private static final int MSG_REGISTER_LISTENER = 1;
89    private static final int MSG_UNREGISTER_LISTENER = 2;
90    private static final int MSG_DISPATCH_UNLOCK_ATTEMPT = 3;
91    private static final int MSG_ENABLED_AGENTS_CHANGED = 4;
92    private static final int MSG_REQUIRE_CREDENTIAL_ENTRY = 5;
93
94    private final ArraySet<AgentInfo> mActiveAgents = new ArraySet<AgentInfo>();
95    private final ArrayList<ITrustListener> mTrustListeners = new ArrayList<ITrustListener>();
96    private final Receiver mReceiver = new Receiver();
97    private final SparseBooleanArray mUserHasAuthenticatedSinceBoot = new SparseBooleanArray();
98    /* package */ final TrustArchive mArchive = new TrustArchive();
99    private final Context mContext;
100
101    private UserManager mUserManager;
102
103    public TrustManagerService(Context context) {
104        super(context);
105        mContext = context;
106        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
107    }
108
109    @Override
110    public void onStart() {
111        publishBinderService(Context.TRUST_SERVICE, mService);
112    }
113
114    @Override
115    public void onBootPhase(int phase) {
116        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY && !isSafeMode()) {
117            mPackageMonitor.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
118            mReceiver.register(mContext);
119            refreshAgentList(UserHandle.USER_ALL);
120        }
121    }
122
123    // Agent management
124
125    private static final class AgentInfo {
126        CharSequence label;
127        Drawable icon;
128        ComponentName component; // service that implements ITrustAgent
129        ComponentName settings; // setting to launch to modify agent.
130        TrustAgentWrapper agent;
131        int userId;
132
133        @Override
134        public boolean equals(Object other) {
135            if (!(other instanceof AgentInfo)) {
136                return false;
137            }
138            AgentInfo o = (AgentInfo) other;
139            return component.equals(o.component) && userId == o.userId;
140        }
141
142        @Override
143        public int hashCode() {
144            return component.hashCode() * 31 + userId;
145        }
146    }
147
148    private void updateTrustAll() {
149        List<UserInfo> userInfos = mUserManager.getUsers(true /* excludeDying */);
150        for (UserInfo userInfo : userInfos) {
151            updateTrust(userInfo.id, false);
152        }
153    }
154
155    public void updateTrust(int userId, boolean initiatedByUser) {
156        dispatchOnTrustManagedChanged(aggregateIsTrustManaged(userId), userId);
157        dispatchOnTrustChanged(aggregateIsTrusted(userId), userId, initiatedByUser);
158    }
159
160    void refreshAgentList(int userId) {
161        if (DEBUG) Slog.d(TAG, "refreshAgentList()");
162        PackageManager pm = mContext.getPackageManager();
163
164        List<UserInfo> userInfos;
165        if (userId == UserHandle.USER_ALL) {
166            userInfos = mUserManager.getUsers(true /* excludeDying */);
167        } else {
168            userInfos = new ArrayList<>();
169            userInfos.add(mUserManager.getUserInfo(userId));
170        }
171        LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext);
172
173        ArraySet<AgentInfo> obsoleteAgents = new ArraySet<>();
174        obsoleteAgents.addAll(mActiveAgents);
175
176        for (UserInfo userInfo : userInfos) {
177            if (lockPatternUtils.getKeyguardStoredPasswordQuality(userInfo.id)
178                    == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) continue;
179            if (!mUserHasAuthenticatedSinceBoot.get(userInfo.id)) continue;
180            DevicePolicyManager dpm = lockPatternUtils.getDevicePolicyManager();
181            int disabledFeatures = dpm.getKeyguardDisabledFeatures(null, userInfo.id);
182            final boolean disableTrustAgents =
183                    (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS) != 0;
184
185            List<ComponentName> enabledAgents = lockPatternUtils.getEnabledTrustAgents(userInfo.id);
186            if (enabledAgents == null) {
187                continue;
188            }
189            List<ResolveInfo> resolveInfos = pm.queryIntentServicesAsUser(TRUST_AGENT_INTENT,
190                    PackageManager.GET_META_DATA, userInfo.id);
191            for (ResolveInfo resolveInfo : resolveInfos) {
192                if (resolveInfo.serviceInfo == null) continue;
193
194                String packageName = resolveInfo.serviceInfo.packageName;
195                if (pm.checkPermission(PERMISSION_PROVIDE_AGENT, packageName)
196                        != PackageManager.PERMISSION_GRANTED) {
197                    Log.w(TAG, "Skipping agent because package " + packageName
198                            + " does not have permission " + PERMISSION_PROVIDE_AGENT + ".");
199                    continue;
200                }
201
202                ComponentName name = getComponentName(resolveInfo);
203                if (!enabledAgents.contains(name)) continue;
204
205                if (disableTrustAgents) {
206                    List<String> features =
207                            dpm.getTrustAgentFeaturesEnabled(null /* admin */, name);
208                    // Disable agent if no features are enabled.
209                    if (features == null || features.isEmpty()) continue;
210                }
211
212                AgentInfo agentInfo = new AgentInfo();
213                agentInfo.component = name;
214                agentInfo.userId = userInfo.id;
215                if (!mActiveAgents.contains(agentInfo)) {
216                    agentInfo.label = resolveInfo.loadLabel(pm);
217                    agentInfo.icon = resolveInfo.loadIcon(pm);
218                    agentInfo.settings = getSettingsComponentName(pm, resolveInfo);
219                    agentInfo.agent = new TrustAgentWrapper(mContext, this,
220                            new Intent().setComponent(name), userInfo.getUserHandle());
221                    mActiveAgents.add(agentInfo);
222                } else {
223                    obsoleteAgents.remove(agentInfo);
224                }
225            }
226        }
227
228        boolean trustMayHaveChanged = false;
229        for (int i = 0; i < obsoleteAgents.size(); i++) {
230            AgentInfo info = obsoleteAgents.valueAt(i);
231            if (info.agent.isManagingTrust()) {
232                trustMayHaveChanged = true;
233            }
234            info.agent.unbind();
235            mActiveAgents.remove(info);
236        }
237
238        if (trustMayHaveChanged) {
239            updateTrustAll();
240        }
241    }
242
243    void updateDevicePolicyFeatures() {
244        for (int i = 0; i < mActiveAgents.size(); i++) {
245            AgentInfo info = mActiveAgents.valueAt(i);
246            if (info.agent.isConnected()) {
247                info.agent.updateDevicePolicyFeatures();
248            }
249        }
250    }
251
252    private void removeAgentsOfPackage(String packageName) {
253        boolean trustMayHaveChanged = false;
254        for (int i = mActiveAgents.size() - 1; i >= 0; i--) {
255            AgentInfo info = mActiveAgents.valueAt(i);
256            if (packageName.equals(info.component.getPackageName())) {
257                Log.i(TAG, "Resetting agent " + info.component.flattenToShortString());
258                if (info.agent.isManagingTrust()) {
259                    trustMayHaveChanged = true;
260                }
261                info.agent.unbind();
262                mActiveAgents.removeAt(i);
263            }
264        }
265        if (trustMayHaveChanged) {
266            updateTrustAll();
267        }
268    }
269
270    public void resetAgent(ComponentName name, int userId) {
271        boolean trustMayHaveChanged = false;
272        for (int i = mActiveAgents.size() - 1; i >= 0; i--) {
273            AgentInfo info = mActiveAgents.valueAt(i);
274            if (name.equals(info.component) && userId == info.userId) {
275                Log.i(TAG, "Resetting agent " + info.component.flattenToShortString());
276                if (info.agent.isManagingTrust()) {
277                    trustMayHaveChanged = true;
278                }
279                info.agent.unbind();
280                mActiveAgents.removeAt(i);
281            }
282        }
283        if (trustMayHaveChanged) {
284            updateTrust(userId, false);
285        }
286        refreshAgentList(userId);
287    }
288
289    private ComponentName getSettingsComponentName(PackageManager pm, ResolveInfo resolveInfo) {
290        if (resolveInfo == null || resolveInfo.serviceInfo == null
291                || resolveInfo.serviceInfo.metaData == null) return null;
292        String cn = null;
293        XmlResourceParser parser = null;
294        Exception caughtException = null;
295        try {
296            parser = resolveInfo.serviceInfo.loadXmlMetaData(pm,
297                    TrustAgentService.TRUST_AGENT_META_DATA);
298            if (parser == null) {
299                Slog.w(TAG, "Can't find " + TrustAgentService.TRUST_AGENT_META_DATA + " meta-data");
300                return null;
301            }
302            Resources res = pm.getResourcesForApplication(resolveInfo.serviceInfo.applicationInfo);
303            AttributeSet attrs = Xml.asAttributeSet(parser);
304            int type;
305            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
306                    && type != XmlPullParser.START_TAG) {
307                // Drain preamble.
308            }
309            String nodeName = parser.getName();
310            if (!"trust-agent".equals(nodeName)) {
311                Slog.w(TAG, "Meta-data does not start with trust-agent tag");
312                return null;
313            }
314            TypedArray sa = res
315                    .obtainAttributes(attrs, com.android.internal.R.styleable.TrustAgent);
316            cn = sa.getString(com.android.internal.R.styleable.TrustAgent_settingsActivity);
317            sa.recycle();
318        } catch (PackageManager.NameNotFoundException e) {
319            caughtException = e;
320        } catch (IOException e) {
321            caughtException = e;
322        } catch (XmlPullParserException e) {
323            caughtException = e;
324        } finally {
325            if (parser != null) parser.close();
326        }
327        if (caughtException != null) {
328            Slog.w(TAG, "Error parsing : " + resolveInfo.serviceInfo.packageName, caughtException);
329            return null;
330        }
331        if (cn == null) {
332            return null;
333        }
334        if (cn.indexOf('/') < 0) {
335            cn = resolveInfo.serviceInfo.packageName + "/" + cn;
336        }
337        return ComponentName.unflattenFromString(cn);
338    }
339
340    private ComponentName getComponentName(ResolveInfo resolveInfo) {
341        if (resolveInfo == null || resolveInfo.serviceInfo == null) return null;
342        return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
343    }
344
345    // Agent dispatch and aggregation
346
347    private boolean aggregateIsTrusted(int userId) {
348        if (!mUserHasAuthenticatedSinceBoot.get(userId)) {
349            return false;
350        }
351        for (int i = 0; i < mActiveAgents.size(); i++) {
352            AgentInfo info = mActiveAgents.valueAt(i);
353            if (info.userId == userId) {
354                if (info.agent.isTrusted()) {
355                    return true;
356                }
357            }
358        }
359        return false;
360    }
361
362    private boolean aggregateIsTrustManaged(int userId) {
363        if (!mUserHasAuthenticatedSinceBoot.get(userId)) {
364            return false;
365        }
366        for (int i = 0; i < mActiveAgents.size(); i++) {
367            AgentInfo info = mActiveAgents.valueAt(i);
368            if (info.userId == userId) {
369                if (info.agent.isManagingTrust()) {
370                    return true;
371                }
372            }
373        }
374        return false;
375    }
376
377    private void dispatchUnlockAttempt(boolean successful, int userId) {
378        for (int i = 0; i < mActiveAgents.size(); i++) {
379            AgentInfo info = mActiveAgents.valueAt(i);
380            if (info.userId == userId) {
381                info.agent.onUnlockAttempt(successful);
382            }
383        }
384
385        if (successful) {
386            updateUserHasAuthenticated(userId);
387        }
388    }
389
390    private void updateUserHasAuthenticated(int userId) {
391        if (!mUserHasAuthenticatedSinceBoot.get(userId)) {
392            mUserHasAuthenticatedSinceBoot.put(userId, true);
393            refreshAgentList(userId);
394        }
395    }
396
397
398    private void requireCredentialEntry(int userId) {
399        if (userId == UserHandle.USER_ALL) {
400            mUserHasAuthenticatedSinceBoot.clear();
401            refreshAgentList(UserHandle.USER_ALL);
402        } else {
403            mUserHasAuthenticatedSinceBoot.put(userId, false);
404            refreshAgentList(userId);
405        }
406    }
407
408    // Listeners
409
410    private void addListener(ITrustListener listener) {
411        for (int i = 0; i < mTrustListeners.size(); i++) {
412            if (mTrustListeners.get(i).asBinder() == listener.asBinder()) {
413                return;
414            }
415        }
416        mTrustListeners.add(listener);
417    }
418
419    private void removeListener(ITrustListener listener) {
420        for (int i = 0; i < mTrustListeners.size(); i++) {
421            if (mTrustListeners.get(i).asBinder() == listener.asBinder()) {
422                mTrustListeners.remove(i);
423                return;
424            }
425        }
426    }
427
428    private void dispatchOnTrustChanged(boolean enabled, int userId, boolean initiatedByUser) {
429        if (!enabled) initiatedByUser = false;
430        for (int i = 0; i < mTrustListeners.size(); i++) {
431            try {
432                mTrustListeners.get(i).onTrustChanged(enabled, userId, initiatedByUser);
433            } catch (DeadObjectException e) {
434                Slog.d(TAG, "Removing dead TrustListener.");
435                mTrustListeners.remove(i);
436                i--;
437            } catch (RemoteException e) {
438                Slog.e(TAG, "Exception while notifying TrustListener.", e);
439            }
440        }
441    }
442
443    private void dispatchOnTrustManagedChanged(boolean managed, int userId) {
444        for (int i = 0; i < mTrustListeners.size(); i++) {
445            try {
446                mTrustListeners.get(i).onTrustManagedChanged(managed, userId);
447            } catch (DeadObjectException e) {
448                Slog.d(TAG, "Removing dead TrustListener.");
449                mTrustListeners.remove(i);
450                i--;
451            } catch (RemoteException e) {
452                Slog.e(TAG, "Exception while notifying TrustListener.", e);
453            }
454        }
455    }
456
457    // Plumbing
458
459    private final IBinder mService = new ITrustManager.Stub() {
460        @Override
461        public void reportUnlockAttempt(boolean authenticated, int userId) throws RemoteException {
462            enforceReportPermission();
463            mHandler.obtainMessage(MSG_DISPATCH_UNLOCK_ATTEMPT, authenticated ? 1 : 0, userId)
464                    .sendToTarget();
465        }
466
467        @Override
468        public void reportEnabledTrustAgentsChanged(int userId) throws RemoteException {
469            enforceReportPermission();
470            // coalesce refresh messages.
471            mHandler.removeMessages(MSG_ENABLED_AGENTS_CHANGED);
472            mHandler.sendEmptyMessage(MSG_ENABLED_AGENTS_CHANGED);
473        }
474
475        @Override
476        public void reportRequireCredentialEntry(int userId) throws RemoteException {
477            enforceReportPermission();
478            if (userId == UserHandle.USER_ALL || userId >= UserHandle.USER_OWNER) {
479                mHandler.obtainMessage(MSG_REQUIRE_CREDENTIAL_ENTRY, userId, 0).sendToTarget();
480            } else {
481                throw new IllegalArgumentException(
482                        "userId must be an explicit user id or USER_ALL");
483            }
484        }
485
486        @Override
487        public void registerTrustListener(ITrustListener trustListener) throws RemoteException {
488            enforceListenerPermission();
489            mHandler.obtainMessage(MSG_REGISTER_LISTENER, trustListener).sendToTarget();
490        }
491
492        @Override
493        public void unregisterTrustListener(ITrustListener trustListener) throws RemoteException {
494            enforceListenerPermission();
495            mHandler.obtainMessage(MSG_UNREGISTER_LISTENER, trustListener).sendToTarget();
496        }
497
498        private void enforceReportPermission() {
499            mContext.enforceCallingOrSelfPermission(
500                    Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE, "reporting trust events");
501        }
502
503        private void enforceListenerPermission() {
504            mContext.enforceCallingPermission(Manifest.permission.TRUST_LISTENER,
505                    "register trust listener");
506        }
507
508        @Override
509        protected void dump(FileDescriptor fd, final PrintWriter fout, String[] args) {
510            mContext.enforceCallingPermission(Manifest.permission.DUMP,
511                    "dumping TrustManagerService");
512            final UserInfo currentUser;
513            final List<UserInfo> userInfos = mUserManager.getUsers(true /* excludeDying */);
514            try {
515                currentUser = ActivityManagerNative.getDefault().getCurrentUser();
516            } catch (RemoteException e) {
517                throw new RuntimeException(e);
518            }
519            mHandler.runWithScissors(new Runnable() {
520                @Override
521                public void run() {
522                    fout.println("Trust manager state:");
523                    for (UserInfo user : userInfos) {
524                        dumpUser(fout, user, user.id == currentUser.id);
525                    }
526                }
527            }, 1500);
528        }
529
530        private void dumpUser(PrintWriter fout, UserInfo user, boolean isCurrent) {
531            fout.printf(" User \"%s\" (id=%d, flags=%#x)",
532                    user.name, user.id, user.flags);
533            if (isCurrent) {
534                fout.print(" (current)");
535            }
536            fout.print(": trusted=" + dumpBool(aggregateIsTrusted(user.id)));
537            fout.print(", trustManaged=" + dumpBool(aggregateIsTrustManaged(user.id)));
538            fout.println();
539            fout.println("   Enabled agents:");
540            boolean duplicateSimpleNames = false;
541            ArraySet<String> simpleNames = new ArraySet<String>();
542            for (AgentInfo info : mActiveAgents) {
543                if (info.userId != user.id) { continue; }
544                boolean trusted = info.agent.isTrusted();
545                fout.print("    "); fout.println(info.component.flattenToShortString());
546                fout.print("     bound=" + dumpBool(info.agent.isBound()));
547                fout.print(", connected=" + dumpBool(info.agent.isConnected()));
548                fout.print(", managingTrust=" + dumpBool(info.agent.isManagingTrust()));
549                fout.print(", trusted=" + dumpBool(trusted));
550                fout.println();
551                if (trusted) {
552                    fout.println("      message=\"" + info.agent.getMessage() + "\"");
553                }
554                if (!info.agent.isConnected()) {
555                    String restartTime = TrustArchive.formatDuration(
556                            info.agent.getScheduledRestartUptimeMillis()
557                                    - SystemClock.uptimeMillis());
558                    fout.println("      restartScheduledAt=" + restartTime);
559                }
560                if (!simpleNames.add(TrustArchive.getSimpleName(info.component))) {
561                    duplicateSimpleNames = true;
562                }
563            }
564            fout.println("   Events:");
565            mArchive.dump(fout, 50, user.id, "    " /* linePrefix */, duplicateSimpleNames);
566            fout.println();
567        }
568
569        private String dumpBool(boolean b) {
570            return b ? "1" : "0";
571        }
572    };
573
574    private final Handler mHandler = new Handler() {
575        @Override
576        public void handleMessage(Message msg) {
577            switch (msg.what) {
578                case MSG_REGISTER_LISTENER:
579                    addListener((ITrustListener) msg.obj);
580                    break;
581                case MSG_UNREGISTER_LISTENER:
582                    removeListener((ITrustListener) msg.obj);
583                    break;
584                case MSG_DISPATCH_UNLOCK_ATTEMPT:
585                    dispatchUnlockAttempt(msg.arg1 != 0, msg.arg2);
586                    break;
587                case MSG_ENABLED_AGENTS_CHANGED:
588                    refreshAgentList(UserHandle.USER_ALL);
589                    break;
590                case MSG_REQUIRE_CREDENTIAL_ENTRY:
591                    requireCredentialEntry(msg.arg1);
592                    break;
593            }
594        }
595    };
596
597    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
598        @Override
599        public void onSomePackagesChanged() {
600            refreshAgentList(UserHandle.USER_ALL);
601        }
602
603        @Override
604        public boolean onPackageChanged(String packageName, int uid, String[] components) {
605            // We're interested in all changes, even if just some components get enabled / disabled.
606            return true;
607        }
608
609        @Override
610        public void onPackageDisappeared(String packageName, int reason) {
611            removeAgentsOfPackage(packageName);
612        }
613    };
614
615    private class Receiver extends BroadcastReceiver {
616
617        @Override
618        public void onReceive(Context context, Intent intent) {
619            if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(
620                    intent.getAction())) {
621                refreshAgentList(getSendingUserId());
622                updateDevicePolicyFeatures();
623            } else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {
624                updateUserHasAuthenticated(getSendingUserId());
625            }
626        }
627
628        public void register(Context context) {
629            IntentFilter filter = new IntentFilter();
630            filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
631            filter.addAction(Intent.ACTION_USER_PRESENT);
632            context.registerReceiverAsUser(this,
633                    UserHandle.ALL,
634                    filter,
635                    null /* permission */,
636                    null /* scheduler */);
637        }
638    }
639}
640