PackageInstallerService.java revision ab2340996a515ea0c437ad5bb1ea1fa88ab9edff
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.pm;
18
19import static com.android.internal.util.XmlUtils.readBitmapAttribute;
20import static com.android.internal.util.XmlUtils.readBooleanAttribute;
21import static com.android.internal.util.XmlUtils.readIntAttribute;
22import static com.android.internal.util.XmlUtils.readLongAttribute;
23import static com.android.internal.util.XmlUtils.readStringAttribute;
24import static com.android.internal.util.XmlUtils.readUriAttribute;
25import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
26import static com.android.internal.util.XmlUtils.writeIntAttribute;
27import static com.android.internal.util.XmlUtils.writeLongAttribute;
28import static com.android.internal.util.XmlUtils.writeStringAttribute;
29import static com.android.internal.util.XmlUtils.writeUriAttribute;
30import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
31import static org.xmlpull.v1.XmlPullParser.START_TAG;
32
33import android.Manifest;
34import android.app.ActivityManager;
35import android.app.AppGlobals;
36import android.app.AppOpsManager;
37import android.app.Notification;
38import android.app.NotificationManager;
39import android.app.PackageDeleteObserver;
40import android.app.PackageInstallObserver;
41import android.app.admin.DevicePolicyManager;
42import android.content.Context;
43import android.content.Intent;
44import android.content.IntentSender;
45import android.content.IntentSender.SendIntentException;
46import android.content.pm.ApplicationInfo;
47import android.content.pm.IPackageInstaller;
48import android.content.pm.IPackageInstallerCallback;
49import android.content.pm.IPackageInstallerSession;
50import android.content.pm.PackageInfo;
51import android.content.pm.PackageInstaller;
52import android.content.pm.PackageInstaller.SessionInfo;
53import android.content.pm.PackageInstaller.SessionParams;
54import android.content.pm.PackageManager;
55import android.content.pm.ParceledListSlice;
56import android.graphics.Bitmap;
57import android.graphics.Bitmap.CompressFormat;
58import android.graphics.BitmapFactory;
59import android.net.Uri;
60import android.os.Binder;
61import android.os.Bundle;
62import android.os.Environment;
63import android.os.Handler;
64import android.os.HandlerThread;
65import android.os.Looper;
66import android.os.Message;
67import android.os.Process;
68import android.os.RemoteCallbackList;
69import android.os.RemoteException;
70import android.os.SELinux;
71import android.os.UserHandle;
72import android.os.UserManager;
73import android.os.storage.StorageManager;
74import android.os.storage.VolumeInfo;
75import android.system.ErrnoException;
76import android.system.Os;
77import android.text.TextUtils;
78import android.text.format.DateUtils;
79import android.util.ArraySet;
80import android.util.AtomicFile;
81import android.util.ExceptionUtils;
82import android.util.Slog;
83import android.util.SparseArray;
84import android.util.SparseBooleanArray;
85import android.util.Xml;
86
87import libcore.io.IoUtils;
88
89import com.android.internal.R;
90import com.android.internal.annotations.GuardedBy;
91import com.android.internal.content.PackageHelper;
92import com.android.internal.util.FastXmlSerializer;
93import com.android.internal.util.ImageUtils;
94import com.android.internal.util.IndentingPrintWriter;
95import com.android.server.IoThread;
96import com.google.android.collect.Sets;
97
98import org.xmlpull.v1.XmlPullParser;
99import org.xmlpull.v1.XmlPullParserException;
100import org.xmlpull.v1.XmlSerializer;
101
102import java.io.File;
103import java.io.FileInputStream;
104import java.io.FileNotFoundException;
105import java.io.FileOutputStream;
106import java.io.FilenameFilter;
107import java.io.IOException;
108import java.nio.charset.StandardCharsets;
109import java.security.SecureRandom;
110import java.util.ArrayList;
111import java.util.List;
112import java.util.Objects;
113import java.util.Random;
114
115public class PackageInstallerService extends IPackageInstaller.Stub {
116    private static final String TAG = "PackageInstaller";
117    private static final boolean LOGD = false;
118
119    // TODO: remove outstanding sessions when installer package goes away
120    // TODO: notify listeners in other users when package has been installed there
121    // TODO: purge expired sessions periodically in addition to at reboot
122
123    /** XML constants used in {@link #mSessionsFile} */
124    private static final String TAG_SESSIONS = "sessions";
125    private static final String TAG_SESSION = "session";
126    private static final String ATTR_SESSION_ID = "sessionId";
127    private static final String ATTR_USER_ID = "userId";
128    private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName";
129    private static final String ATTR_INSTALLER_UID = "installerUid";
130    private static final String ATTR_CREATED_MILLIS = "createdMillis";
131    private static final String ATTR_SESSION_STAGE_DIR = "sessionStageDir";
132    private static final String ATTR_SESSION_STAGE_CID = "sessionStageCid";
133    private static final String ATTR_PREPARED = "prepared";
134    private static final String ATTR_SEALED = "sealed";
135    private static final String ATTR_MODE = "mode";
136    private static final String ATTR_INSTALL_FLAGS = "installFlags";
137    private static final String ATTR_INSTALL_LOCATION = "installLocation";
138    private static final String ATTR_SIZE_BYTES = "sizeBytes";
139    private static final String ATTR_APP_PACKAGE_NAME = "appPackageName";
140    @Deprecated
141    private static final String ATTR_APP_ICON = "appIcon";
142    private static final String ATTR_APP_LABEL = "appLabel";
143    private static final String ATTR_ORIGINATING_URI = "originatingUri";
144    private static final String ATTR_REFERRER_URI = "referrerUri";
145    private static final String ATTR_ABI_OVERRIDE = "abiOverride";
146    private static final String ATTR_VOLUME_UUID = "volumeUuid";
147
148    /** Automatically destroy sessions older than this */
149    private static final long MAX_AGE_MILLIS = 3 * DateUtils.DAY_IN_MILLIS;
150    /** Upper bound on number of active sessions for a UID */
151    private static final long MAX_ACTIVE_SESSIONS = 1024;
152    /** Upper bound on number of historical sessions for a UID */
153    private static final long MAX_HISTORICAL_SESSIONS = 1048576;
154
155    private final Context mContext;
156    private final PackageManagerService mPm;
157
158    private AppOpsManager mAppOps;
159    private StorageManager mStorage;
160
161    private final HandlerThread mInstallThread;
162    private final Handler mInstallHandler;
163
164    private final Callbacks mCallbacks;
165
166    /**
167     * File storing persisted {@link #mSessions} metadata.
168     */
169    private final AtomicFile mSessionsFile;
170
171    /**
172     * Directory storing persisted {@link #mSessions} metadata which is too
173     * heavy to store directly in {@link #mSessionsFile}.
174     */
175    private final File mSessionsDir;
176
177    private final InternalCallback mInternalCallback = new InternalCallback();
178
179    /**
180     * Used for generating session IDs. Since this is created at boot time,
181     * normal random might be predictable.
182     */
183    private final Random mRandom = new SecureRandom();
184
185    @GuardedBy("mSessions")
186    private final SparseArray<PackageInstallerSession> mSessions = new SparseArray<>();
187
188    /** Historical sessions kept around for debugging purposes */
189    @GuardedBy("mSessions")
190    private final SparseArray<PackageInstallerSession> mHistoricalSessions = new SparseArray<>();
191
192    /** Sessions allocated to legacy users */
193    @GuardedBy("mSessions")
194    private final SparseBooleanArray mLegacySessions = new SparseBooleanArray();
195
196    private static final FilenameFilter sStageFilter = new FilenameFilter() {
197        @Override
198        public boolean accept(File dir, String name) {
199            return isStageName(name);
200        }
201    };
202
203    public PackageInstallerService(Context context, PackageManagerService pm) {
204        mContext = context;
205        mPm = pm;
206
207        mInstallThread = new HandlerThread(TAG);
208        mInstallThread.start();
209
210        mInstallHandler = new Handler(mInstallThread.getLooper());
211
212        mCallbacks = new Callbacks(mInstallThread.getLooper());
213
214        mSessionsFile = new AtomicFile(
215                new File(Environment.getSystemSecureDirectory(), "install_sessions.xml"));
216        mSessionsDir = new File(Environment.getSystemSecureDirectory(), "install_sessions");
217        mSessionsDir.mkdirs();
218
219        synchronized (mSessions) {
220            readSessionsLocked();
221
222            final File internalStagingDir = buildInternalStagingDir();
223            final ArraySet<File> unclaimedStages = Sets.newArraySet(
224                    internalStagingDir.listFiles(sStageFilter));
225            final ArraySet<File> unclaimedIcons = Sets.newArraySet(
226                    mSessionsDir.listFiles());
227
228            // Ignore stages and icons claimed by active sessions
229            for (int i = 0; i < mSessions.size(); i++) {
230                final PackageInstallerSession session = mSessions.valueAt(i);
231                unclaimedStages.remove(session.stageDir);
232                unclaimedIcons.remove(buildAppIconFile(session.sessionId));
233            }
234
235            // Clean up orphaned staging directories
236            for (File stage : unclaimedStages) {
237                Slog.w(TAG, "Deleting orphan stage " + stage);
238                if (stage.isDirectory()) {
239                    mPm.mInstaller.rmPackageDir(stage.getAbsolutePath());
240                } else {
241                    stage.delete();
242                }
243            }
244
245            // Clean up orphaned icons
246            for (File icon : unclaimedIcons) {
247                Slog.w(TAG, "Deleting orphan icon " + icon);
248                icon.delete();
249            }
250        }
251    }
252
253    public void systemReady() {
254        mAppOps = mContext.getSystemService(AppOpsManager.class);
255        mStorage = mContext.getSystemService(StorageManager.class);
256    }
257
258    public void onSecureContainersAvailable() {
259        synchronized (mSessions) {
260            final ArraySet<String> unclaimed = new ArraySet<>();
261            for (String cid : PackageHelper.getSecureContainerList()) {
262                if (isStageName(cid)) {
263                    unclaimed.add(cid);
264                }
265            }
266
267            // Ignore stages claimed by active sessions
268            for (int i = 0; i < mSessions.size(); i++) {
269                final PackageInstallerSession session = mSessions.valueAt(i);
270                final String cid = session.stageCid;
271
272                if (unclaimed.remove(cid)) {
273                    // Claimed by active session, mount it
274                    PackageHelper.mountSdDir(cid, PackageManagerService.getEncryptKey(),
275                            Process.SYSTEM_UID);
276                }
277            }
278
279            // Clean up orphaned staging containers
280            for (String cid : unclaimed) {
281                Slog.w(TAG, "Deleting orphan container " + cid);
282                PackageHelper.destroySdDir(cid);
283            }
284        }
285    }
286
287    public static boolean isStageName(String name) {
288        final boolean isFile = name.startsWith("vmdl") && name.endsWith(".tmp");
289        final boolean isContainer = name.startsWith("smdl") && name.endsWith(".tmp");
290        final boolean isLegacyContainer = name.startsWith("smdl2tmp");
291        return isFile || isContainer || isLegacyContainer;
292    }
293
294    @Deprecated
295    public File allocateStageDirLegacy(String volumeUuid) throws IOException {
296        synchronized (mSessions) {
297            try {
298                final int sessionId = allocateSessionIdLocked();
299                mLegacySessions.put(sessionId, true);
300                final File stageDir = buildStageDir(volumeUuid, sessionId);
301                prepareStageDir(stageDir);
302                return stageDir;
303            } catch (IllegalStateException e) {
304                throw new IOException(e);
305            }
306        }
307    }
308
309    @Deprecated
310    public String allocateExternalStageCidLegacy() {
311        synchronized (mSessions) {
312            final int sessionId = allocateSessionIdLocked();
313            mLegacySessions.put(sessionId, true);
314            return "smdl" + sessionId + ".tmp";
315        }
316    }
317
318    private void readSessionsLocked() {
319        if (LOGD) Slog.v(TAG, "readSessionsLocked()");
320
321        mSessions.clear();
322
323        FileInputStream fis = null;
324        try {
325            fis = mSessionsFile.openRead();
326            final XmlPullParser in = Xml.newPullParser();
327            in.setInput(fis, StandardCharsets.UTF_8.name());
328
329            int type;
330            while ((type = in.next()) != END_DOCUMENT) {
331                if (type == START_TAG) {
332                    final String tag = in.getName();
333                    if (TAG_SESSION.equals(tag)) {
334                        final PackageInstallerSession session = readSessionLocked(in);
335                        final long age = System.currentTimeMillis() - session.createdMillis;
336
337                        final boolean valid;
338                        if (age >= MAX_AGE_MILLIS) {
339                            Slog.w(TAG, "Abandoning old session first created at "
340                                    + session.createdMillis);
341                            valid = false;
342                        } else {
343                            valid = true;
344                        }
345
346                        if (valid) {
347                            mSessions.put(session.sessionId, session);
348                        } else {
349                            // Since this is early during boot we don't send
350                            // any observer events about the session, but we
351                            // keep details around for dumpsys.
352                            mHistoricalSessions.put(session.sessionId, session);
353                        }
354                    }
355                }
356            }
357        } catch (FileNotFoundException e) {
358            // Missing sessions are okay, probably first boot
359        } catch (IOException e) {
360            Slog.wtf(TAG, "Failed reading install sessions", e);
361        } catch (XmlPullParserException e) {
362            Slog.wtf(TAG, "Failed reading install sessions", e);
363        } finally {
364            IoUtils.closeQuietly(fis);
365        }
366    }
367
368    private PackageInstallerSession readSessionLocked(XmlPullParser in) throws IOException {
369        final int sessionId = readIntAttribute(in, ATTR_SESSION_ID);
370        final int userId = readIntAttribute(in, ATTR_USER_ID);
371        final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME);
372        final int installerUid = readIntAttribute(in, ATTR_INSTALLER_UID,
373                mPm.getPackageUid(installerPackageName, userId));
374        final long createdMillis = readLongAttribute(in, ATTR_CREATED_MILLIS);
375        final String stageDirRaw = readStringAttribute(in, ATTR_SESSION_STAGE_DIR);
376        final File stageDir = (stageDirRaw != null) ? new File(stageDirRaw) : null;
377        final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID);
378        final boolean prepared = readBooleanAttribute(in, ATTR_PREPARED, true);
379        final boolean sealed = readBooleanAttribute(in, ATTR_SEALED);
380
381        final SessionParams params = new SessionParams(
382                SessionParams.MODE_INVALID);
383        params.mode = readIntAttribute(in, ATTR_MODE);
384        params.installFlags = readIntAttribute(in, ATTR_INSTALL_FLAGS);
385        params.installLocation = readIntAttribute(in, ATTR_INSTALL_LOCATION);
386        params.sizeBytes = readLongAttribute(in, ATTR_SIZE_BYTES);
387        params.appPackageName = readStringAttribute(in, ATTR_APP_PACKAGE_NAME);
388        params.appIcon = readBitmapAttribute(in, ATTR_APP_ICON);
389        params.appLabel = readStringAttribute(in, ATTR_APP_LABEL);
390        params.originatingUri = readUriAttribute(in, ATTR_ORIGINATING_URI);
391        params.referrerUri = readUriAttribute(in, ATTR_REFERRER_URI);
392        params.abiOverride = readStringAttribute(in, ATTR_ABI_OVERRIDE);
393        params.volumeUuid = readStringAttribute(in, ATTR_VOLUME_UUID);
394
395        final File appIconFile = buildAppIconFile(sessionId);
396        if (appIconFile.exists()) {
397            params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath());
398            params.appIconLastModified = appIconFile.lastModified();
399        }
400
401        return new PackageInstallerSession(mInternalCallback, mContext, mPm,
402                mInstallThread.getLooper(), sessionId, userId, installerPackageName, installerUid,
403                params, createdMillis, stageDir, stageCid, prepared, sealed);
404    }
405
406    private void writeSessionsLocked() {
407        if (LOGD) Slog.v(TAG, "writeSessionsLocked()");
408
409        FileOutputStream fos = null;
410        try {
411            fos = mSessionsFile.startWrite();
412
413            XmlSerializer out = new FastXmlSerializer();
414            out.setOutput(fos, StandardCharsets.UTF_8.name());
415            out.startDocument(null, true);
416            out.startTag(null, TAG_SESSIONS);
417            final int size = mSessions.size();
418            for (int i = 0; i < size; i++) {
419                final PackageInstallerSession session = mSessions.valueAt(i);
420                writeSessionLocked(out, session);
421            }
422            out.endTag(null, TAG_SESSIONS);
423            out.endDocument();
424
425            mSessionsFile.finishWrite(fos);
426        } catch (IOException e) {
427            if (fos != null) {
428                mSessionsFile.failWrite(fos);
429            }
430        }
431    }
432
433    private void writeSessionLocked(XmlSerializer out, PackageInstallerSession session)
434            throws IOException {
435        final SessionParams params = session.params;
436
437        out.startTag(null, TAG_SESSION);
438
439        writeIntAttribute(out, ATTR_SESSION_ID, session.sessionId);
440        writeIntAttribute(out, ATTR_USER_ID, session.userId);
441        writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME,
442                session.installerPackageName);
443        writeIntAttribute(out, ATTR_INSTALLER_UID, session.installerUid);
444        writeLongAttribute(out, ATTR_CREATED_MILLIS, session.createdMillis);
445        if (session.stageDir != null) {
446            writeStringAttribute(out, ATTR_SESSION_STAGE_DIR,
447                    session.stageDir.getAbsolutePath());
448        }
449        if (session.stageCid != null) {
450            writeStringAttribute(out, ATTR_SESSION_STAGE_CID, session.stageCid);
451        }
452        writeBooleanAttribute(out, ATTR_PREPARED, session.isPrepared());
453        writeBooleanAttribute(out, ATTR_SEALED, session.isSealed());
454
455        writeIntAttribute(out, ATTR_MODE, params.mode);
456        writeIntAttribute(out, ATTR_INSTALL_FLAGS, params.installFlags);
457        writeIntAttribute(out, ATTR_INSTALL_LOCATION, params.installLocation);
458        writeLongAttribute(out, ATTR_SIZE_BYTES, params.sizeBytes);
459        writeStringAttribute(out, ATTR_APP_PACKAGE_NAME, params.appPackageName);
460        writeStringAttribute(out, ATTR_APP_LABEL, params.appLabel);
461        writeUriAttribute(out, ATTR_ORIGINATING_URI, params.originatingUri);
462        writeUriAttribute(out, ATTR_REFERRER_URI, params.referrerUri);
463        writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride);
464        writeStringAttribute(out, ATTR_VOLUME_UUID, params.volumeUuid);
465
466        // Persist app icon if changed since last written
467        final File appIconFile = buildAppIconFile(session.sessionId);
468        if (params.appIcon == null && appIconFile.exists()) {
469            appIconFile.delete();
470        } else if (params.appIcon != null
471                && appIconFile.lastModified() != params.appIconLastModified) {
472            if (LOGD) Slog.w(TAG, "Writing changed icon " + appIconFile);
473            FileOutputStream os = null;
474            try {
475                os = new FileOutputStream(appIconFile);
476                params.appIcon.compress(CompressFormat.PNG, 90, os);
477            } catch (IOException e) {
478                Slog.w(TAG, "Failed to write icon " + appIconFile + ": " + e.getMessage());
479            } finally {
480                IoUtils.closeQuietly(os);
481            }
482
483            params.appIconLastModified = appIconFile.lastModified();
484        }
485
486        out.endTag(null, TAG_SESSION);
487    }
488
489    private File buildAppIconFile(int sessionId) {
490        return new File(mSessionsDir, "app_icon." + sessionId + ".png");
491    }
492
493    private void writeSessionsAsync() {
494        IoThread.getHandler().post(new Runnable() {
495            @Override
496            public void run() {
497                synchronized (mSessions) {
498                    writeSessionsLocked();
499                }
500            }
501        });
502    }
503
504    @Override
505    public int createSession(SessionParams params, String installerPackageName, int userId) {
506        try {
507            return createSessionInternal(params, installerPackageName, userId);
508        } catch (IOException e) {
509            throw ExceptionUtils.wrap(e);
510        }
511    }
512
513    private int createSessionInternal(SessionParams params, String installerPackageName, int userId)
514            throws IOException {
515        final int callingUid = Binder.getCallingUid();
516        mPm.enforceCrossUserPermission(callingUid, userId, true, true, "createSession");
517
518        if (mPm.isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {
519            throw new SecurityException("User restriction prevents installing");
520        }
521
522        if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {
523            params.installFlags |= PackageManager.INSTALL_FROM_ADB;
524
525        } else {
526            mAppOps.checkPackage(callingUid, installerPackageName);
527
528            params.installFlags &= ~PackageManager.INSTALL_FROM_ADB;
529            params.installFlags &= ~PackageManager.INSTALL_ALL_USERS;
530            params.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
531        }
532
533        // Only system components can circumvent runtime permissions when installing.
534        if ((params.installFlags & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0
535                && mContext.checkCallingOrSelfPermission(Manifest.permission
536                .INSTALL_GRANT_RUNTIME_PERMISSIONS) == PackageManager.PERMISSION_DENIED) {
537            throw new SecurityException("You need the "
538                    + "android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS permission "
539                    + "to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag");
540        }
541
542        // Defensively resize giant app icons
543        if (params.appIcon != null) {
544            final ActivityManager am = (ActivityManager) mContext.getSystemService(
545                    Context.ACTIVITY_SERVICE);
546            final int iconSize = am.getLauncherLargeIconSize();
547            if ((params.appIcon.getWidth() > iconSize * 2)
548                    || (params.appIcon.getHeight() > iconSize * 2)) {
549                params.appIcon = Bitmap.createScaledBitmap(params.appIcon, iconSize, iconSize,
550                        true);
551            }
552        }
553
554        switch (params.mode) {
555            case SessionParams.MODE_FULL_INSTALL:
556            case SessionParams.MODE_INHERIT_EXISTING:
557                break;
558            default:
559                throw new IllegalArgumentException("Invalid install mode: " + params.mode);
560        }
561
562        // If caller requested explicit location, sanity check it, otherwise
563        // resolve the best internal or adopted location.
564        if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
565            if (!PackageHelper.fitsOnInternal(mContext, params.sizeBytes)) {
566                throw new IOException("No suitable internal storage available");
567            }
568
569        } else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) {
570            if (!PackageHelper.fitsOnExternal(mContext, params.sizeBytes)) {
571                throw new IOException("No suitable external storage available");
572            }
573
574        } else if ((params.installFlags & PackageManager.INSTALL_FORCE_VOLUME_UUID) != 0) {
575            // For now, installs to adopted media are treated as internal from
576            // an install flag point-of-view.
577            params.setInstallFlagsInternal();
578
579        } else {
580            // For now, installs to adopted media are treated as internal from
581            // an install flag point-of-view.
582            params.setInstallFlagsInternal();
583
584            // Resolve best location for install, based on combination of
585            // requested install flags, delta size, and manifest settings.
586            final long ident = Binder.clearCallingIdentity();
587            try {
588                params.volumeUuid = PackageHelper.resolveInstallVolume(mContext,
589                        params.appPackageName, params.installLocation, params.sizeBytes);
590            } finally {
591                Binder.restoreCallingIdentity(ident);
592            }
593        }
594
595        final int sessionId;
596        final PackageInstallerSession session;
597        synchronized (mSessions) {
598            // Sanity check that installer isn't going crazy
599            final int activeCount = getSessionCount(mSessions, callingUid);
600            if (activeCount >= MAX_ACTIVE_SESSIONS) {
601                throw new IllegalStateException(
602                        "Too many active sessions for UID " + callingUid);
603            }
604            final int historicalCount = getSessionCount(mHistoricalSessions, callingUid);
605            if (historicalCount >= MAX_HISTORICAL_SESSIONS) {
606                throw new IllegalStateException(
607                        "Too many historical sessions for UID " + callingUid);
608            }
609
610            final long createdMillis = System.currentTimeMillis();
611            sessionId = allocateSessionIdLocked();
612
613            // We're staging to exactly one location
614            File stageDir = null;
615            String stageCid = null;
616            if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
617                stageDir = buildStageDir(params.volumeUuid, sessionId);
618            } else {
619                stageCid = buildExternalStageCid(sessionId);
620            }
621
622            session = new PackageInstallerSession(mInternalCallback, mContext, mPm,
623                    mInstallThread.getLooper(), sessionId, userId, installerPackageName, callingUid,
624                    params, createdMillis, stageDir, stageCid, false, false);
625            mSessions.put(sessionId, session);
626        }
627
628        mCallbacks.notifySessionCreated(session.sessionId, session.userId);
629        writeSessionsAsync();
630        return sessionId;
631    }
632
633    @Override
634    public void updateSessionAppIcon(int sessionId, Bitmap appIcon) {
635        synchronized (mSessions) {
636            final PackageInstallerSession session = mSessions.get(sessionId);
637            if (session == null || !isCallingUidOwner(session)) {
638                throw new SecurityException("Caller has no access to session " + sessionId);
639            }
640
641            // Defensively resize giant app icons
642            if (appIcon != null) {
643                final ActivityManager am = (ActivityManager) mContext.getSystemService(
644                        Context.ACTIVITY_SERVICE);
645                final int iconSize = am.getLauncherLargeIconSize();
646                if ((appIcon.getWidth() > iconSize * 2)
647                        || (appIcon.getHeight() > iconSize * 2)) {
648                    appIcon = Bitmap.createScaledBitmap(appIcon, iconSize, iconSize, true);
649                }
650            }
651
652            session.params.appIcon = appIcon;
653            session.params.appIconLastModified = -1;
654
655            mInternalCallback.onSessionBadgingChanged(session);
656        }
657    }
658
659    @Override
660    public void updateSessionAppLabel(int sessionId, String appLabel) {
661        synchronized (mSessions) {
662            final PackageInstallerSession session = mSessions.get(sessionId);
663            if (session == null || !isCallingUidOwner(session)) {
664                throw new SecurityException("Caller has no access to session " + sessionId);
665            }
666            session.params.appLabel = appLabel;
667            mInternalCallback.onSessionBadgingChanged(session);
668        }
669    }
670
671    @Override
672    public void abandonSession(int sessionId) {
673        synchronized (mSessions) {
674            final PackageInstallerSession session = mSessions.get(sessionId);
675            if (session == null || !isCallingUidOwner(session)) {
676                throw new SecurityException("Caller has no access to session " + sessionId);
677            }
678            session.abandon();
679        }
680    }
681
682    @Override
683    public IPackageInstallerSession openSession(int sessionId) {
684        try {
685            return openSessionInternal(sessionId);
686        } catch (IOException e) {
687            throw ExceptionUtils.wrap(e);
688        }
689    }
690
691    private IPackageInstallerSession openSessionInternal(int sessionId) throws IOException {
692        synchronized (mSessions) {
693            final PackageInstallerSession session = mSessions.get(sessionId);
694            if (session == null || !isCallingUidOwner(session)) {
695                throw new SecurityException("Caller has no access to session " + sessionId);
696            }
697            session.open();
698            return session;
699        }
700    }
701
702    private int allocateSessionIdLocked() {
703        int n = 0;
704        int sessionId;
705        do {
706            sessionId = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
707            if (mSessions.get(sessionId) == null && mHistoricalSessions.get(sessionId) == null
708                    && !mLegacySessions.get(sessionId, false)) {
709                return sessionId;
710            }
711        } while (n++ < 32);
712
713        throw new IllegalStateException("Failed to allocate session ID");
714    }
715
716    private File buildInternalStagingDir() {
717        return new File(Environment.getDataDirectory(), "app");
718    }
719
720    private File buildStagingDir(String volumeUuid) throws FileNotFoundException {
721        if (volumeUuid == null) {
722            return buildInternalStagingDir();
723        } else {
724            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
725            if (vol != null && vol.type == VolumeInfo.TYPE_PRIVATE
726                    && vol.isMountedWritable()) {
727                return new File(vol.path, "app");
728            } else {
729                throw new FileNotFoundException("Failed to find volume for UUID " + volumeUuid);
730            }
731        }
732    }
733
734    private File buildStageDir(String volumeUuid, int sessionId) throws FileNotFoundException {
735        final File stagingDir = buildStagingDir(volumeUuid);
736        return new File(stagingDir, "vmdl" + sessionId + ".tmp");
737    }
738
739    static void prepareStageDir(File stageDir) throws IOException {
740        if (stageDir.exists()) {
741            throw new IOException("Session dir already exists: " + stageDir);
742        }
743
744        try {
745            Os.mkdir(stageDir.getAbsolutePath(), 0755);
746            Os.chmod(stageDir.getAbsolutePath(), 0755);
747        } catch (ErrnoException e) {
748            // This purposefully throws if directory already exists
749            throw new IOException("Failed to prepare session dir: " + stageDir, e);
750        }
751
752        if (!SELinux.restorecon(stageDir)) {
753            throw new IOException("Failed to restorecon session dir: " + stageDir);
754        }
755    }
756
757    private String buildExternalStageCid(int sessionId) {
758        return "smdl" + sessionId + ".tmp";
759    }
760
761    static void prepareExternalStageCid(String stageCid, long sizeBytes) throws IOException {
762        if (PackageHelper.createSdDir(sizeBytes, stageCid, PackageManagerService.getEncryptKey(),
763                Process.SYSTEM_UID, true) == null) {
764            throw new IOException("Failed to create session cid: " + stageCid);
765        }
766    }
767
768    @Override
769    public SessionInfo getSessionInfo(int sessionId) {
770        synchronized (mSessions) {
771            final PackageInstallerSession session = mSessions.get(sessionId);
772            return session != null ? session.generateInfo() : null;
773        }
774    }
775
776    @Override
777    public ParceledListSlice<SessionInfo> getAllSessions(int userId) {
778        mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false, "getAllSessions");
779
780        final List<SessionInfo> result = new ArrayList<>();
781        synchronized (mSessions) {
782            for (int i = 0; i < mSessions.size(); i++) {
783                final PackageInstallerSession session = mSessions.valueAt(i);
784                if (session.userId == userId) {
785                    result.add(session.generateInfo());
786                }
787            }
788        }
789        return new ParceledListSlice<>(result);
790    }
791
792    @Override
793    public ParceledListSlice<SessionInfo> getMySessions(String installerPackageName, int userId) {
794        mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false, "getMySessions");
795        mAppOps.checkPackage(Binder.getCallingUid(), installerPackageName);
796
797        final List<SessionInfo> result = new ArrayList<>();
798        synchronized (mSessions) {
799            for (int i = 0; i < mSessions.size(); i++) {
800                final PackageInstallerSession session = mSessions.valueAt(i);
801                if (Objects.equals(session.installerPackageName, installerPackageName)
802                        && session.userId == userId) {
803                    result.add(session.generateInfo());
804                }
805            }
806        }
807        return new ParceledListSlice<>(result);
808    }
809
810    @Override
811    public void uninstall(String packageName, String callerPackageName, int flags,
812                IntentSender statusReceiver, int userId) {
813        final int callingUid = Binder.getCallingUid();
814        mPm.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
815        if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) {
816            mAppOps.checkPackage(callingUid, callerPackageName);
817        }
818
819        // Check whether the caller is device owner
820        DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(
821                Context.DEVICE_POLICY_SERVICE);
822        boolean isDeviceOwner = (dpm != null) && dpm.isDeviceOwnerApp(callerPackageName);
823
824        final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
825                statusReceiver, packageName, isDeviceOwner, userId);
826        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES)
827                == PackageManager.PERMISSION_GRANTED) {
828            // Sweet, call straight through!
829            mPm.deletePackage(packageName, adapter.getBinder(), userId, flags);
830        } else if (isDeviceOwner) {
831            // Allow the DeviceOwner to silently delete packages
832            // Need to clear the calling identity to get DELETE_PACKAGES permission
833            long ident = Binder.clearCallingIdentity();
834            try {
835                mPm.deletePackage(packageName, adapter.getBinder(), userId, flags);
836            } finally {
837                Binder.restoreCallingIdentity(ident);
838            }
839        } else {
840            // Take a short detour to confirm with user
841            final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
842            intent.setData(Uri.fromParts("package", packageName, null));
843            intent.putExtra(PackageInstaller.EXTRA_CALLBACK, adapter.getBinder().asBinder());
844            adapter.onUserActionRequired(intent);
845        }
846    }
847
848    @Override
849    public void setPermissionsResult(int sessionId, boolean accepted) {
850        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, TAG);
851
852        synchronized (mSessions) {
853            mSessions.get(sessionId).setPermissionsResult(accepted);
854        }
855    }
856
857    @Override
858    public void registerCallback(IPackageInstallerCallback callback, int userId) {
859        mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false, "registerCallback");
860        mCallbacks.register(callback, userId);
861    }
862
863    @Override
864    public void unregisterCallback(IPackageInstallerCallback callback) {
865        mCallbacks.unregister(callback);
866    }
867
868    private static int getSessionCount(SparseArray<PackageInstallerSession> sessions,
869            int installerUid) {
870        int count = 0;
871        final int size = sessions.size();
872        for (int i = 0; i < size; i++) {
873            final PackageInstallerSession session = sessions.valueAt(i);
874            if (session.installerUid == installerUid) {
875                count++;
876            }
877        }
878        return count;
879    }
880
881    private boolean isCallingUidOwner(PackageInstallerSession session) {
882        final int callingUid = Binder.getCallingUid();
883        if (callingUid == Process.ROOT_UID) {
884            return true;
885        } else {
886            return (session != null) && (callingUid == session.installerUid);
887        }
888    }
889
890    static class PackageDeleteObserverAdapter extends PackageDeleteObserver {
891        private final Context mContext;
892        private final IntentSender mTarget;
893        private final String mPackageName;
894        private final Notification mNotification;
895
896        public PackageDeleteObserverAdapter(Context context, IntentSender target,
897                String packageName, boolean showNotification, int userId) {
898            mContext = context;
899            mTarget = target;
900            mPackageName = packageName;
901            if (showNotification) {
902                mNotification = buildSuccessNotification(mContext,
903                        mContext.getResources().getString(R.string.package_deleted_device_owner),
904                        packageName,
905                        userId);
906            } else {
907                mNotification = null;
908            }
909        }
910
911        @Override
912        public void onUserActionRequired(Intent intent) {
913            final Intent fillIn = new Intent();
914            fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
915            fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
916                    PackageInstaller.STATUS_PENDING_USER_ACTION);
917            fillIn.putExtra(Intent.EXTRA_INTENT, intent);
918            try {
919                mTarget.sendIntent(mContext, 0, fillIn, null, null);
920            } catch (SendIntentException ignored) {
921            }
922        }
923
924        @Override
925        public void onPackageDeleted(String basePackageName, int returnCode, String msg) {
926            if (PackageManager.DELETE_SUCCEEDED == returnCode && mNotification != null) {
927                NotificationManager notificationManager = (NotificationManager)
928                        mContext.getSystemService(Context.NOTIFICATION_SERVICE);
929                notificationManager.notify(basePackageName, 0, mNotification);
930            }
931            final Intent fillIn = new Intent();
932            fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
933            fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
934                    PackageManager.deleteStatusToPublicStatus(returnCode));
935            fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE,
936                    PackageManager.deleteStatusToString(returnCode, msg));
937            fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode);
938            try {
939                mTarget.sendIntent(mContext, 0, fillIn, null, null);
940            } catch (SendIntentException ignored) {
941            }
942        }
943    }
944
945    static class PackageInstallObserverAdapter extends PackageInstallObserver {
946        private final Context mContext;
947        private final IntentSender mTarget;
948        private final int mSessionId;
949        private final boolean mShowNotification;
950        private final int mUserId;
951
952        public PackageInstallObserverAdapter(Context context, IntentSender target, int sessionId,
953                boolean showNotification, int userId) {
954            mContext = context;
955            mTarget = target;
956            mSessionId = sessionId;
957            mShowNotification = showNotification;
958            mUserId = userId;
959        }
960
961        @Override
962        public void onUserActionRequired(Intent intent) {
963            final Intent fillIn = new Intent();
964            fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, mSessionId);
965            fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
966                    PackageInstaller.STATUS_PENDING_USER_ACTION);
967            fillIn.putExtra(Intent.EXTRA_INTENT, intent);
968            try {
969                mTarget.sendIntent(mContext, 0, fillIn, null, null);
970            } catch (SendIntentException ignored) {
971            }
972        }
973
974        @Override
975        public void onPackageInstalled(String basePackageName, int returnCode, String msg,
976                Bundle extras) {
977            if (PackageManager.INSTALL_SUCCEEDED == returnCode && mShowNotification) {
978                boolean update = (extras != null) && extras.getBoolean(Intent.EXTRA_REPLACING);
979                Notification notification = buildSuccessNotification(mContext,
980                        mContext.getResources()
981                                .getString(update ? R.string.package_updated_device_owner :
982                                        R.string.package_installed_device_owner),
983                        basePackageName,
984                        mUserId);
985                if (notification != null) {
986                    NotificationManager notificationManager = (NotificationManager)
987                            mContext.getSystemService(Context.NOTIFICATION_SERVICE);
988                    notificationManager.notify(basePackageName, 0, notification);
989                }
990            }
991            final Intent fillIn = new Intent();
992            fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, basePackageName);
993            fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, mSessionId);
994            fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
995                    PackageManager.installStatusToPublicStatus(returnCode));
996            fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE,
997                    PackageManager.installStatusToString(returnCode, msg));
998            fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode);
999            if (extras != null) {
1000                final String existing = extras.getString(
1001                        PackageManager.EXTRA_FAILURE_EXISTING_PACKAGE);
1002                if (!TextUtils.isEmpty(existing)) {
1003                    fillIn.putExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME, existing);
1004                }
1005            }
1006            try {
1007                mTarget.sendIntent(mContext, 0, fillIn, null, null);
1008            } catch (SendIntentException ignored) {
1009            }
1010        }
1011    }
1012
1013    /**
1014     * Build a notification for package installation / deletion by device owners that is shown if
1015     * the operation succeeds.
1016     */
1017    private static Notification buildSuccessNotification(Context context, String contentText,
1018            String basePackageName, int userId) {
1019        PackageInfo packageInfo = null;
1020        try {
1021            packageInfo = AppGlobals.getPackageManager().getPackageInfo(
1022                    basePackageName, 0, userId);
1023        } catch (RemoteException ignored) {
1024        }
1025        if (packageInfo == null || packageInfo.applicationInfo == null) {
1026            Slog.w(TAG, "Notification not built for package: " + basePackageName);
1027            return null;
1028        }
1029        PackageManager pm = context.getPackageManager();
1030        Bitmap packageIcon = ImageUtils.buildScaledBitmap(
1031                packageInfo.applicationInfo.loadIcon(pm),
1032                context.getResources().getDimensionPixelSize(
1033                        android.R.dimen.notification_large_icon_width),
1034                context.getResources().getDimensionPixelSize(
1035                        android.R.dimen.notification_large_icon_height));
1036        CharSequence packageLabel = packageInfo.applicationInfo.loadLabel(pm);
1037        return new Notification.Builder(context)
1038                .setSmallIcon(R.drawable.ic_check_circle_24px)
1039                .setColor(context.getResources().getColor(
1040                        R.color.system_notification_accent_color))
1041                .setContentTitle(packageLabel)
1042                .setContentText(contentText)
1043                .setStyle(new Notification.BigTextStyle().bigText(contentText))
1044                .setLargeIcon(packageIcon)
1045                .build();
1046    }
1047
1048    private static class Callbacks extends Handler {
1049        private static final int MSG_SESSION_CREATED = 1;
1050        private static final int MSG_SESSION_BADGING_CHANGED = 2;
1051        private static final int MSG_SESSION_ACTIVE_CHANGED = 3;
1052        private static final int MSG_SESSION_PROGRESS_CHANGED = 4;
1053        private static final int MSG_SESSION_FINISHED = 5;
1054
1055        private final RemoteCallbackList<IPackageInstallerCallback>
1056                mCallbacks = new RemoteCallbackList<>();
1057
1058        public Callbacks(Looper looper) {
1059            super(looper);
1060        }
1061
1062        public void register(IPackageInstallerCallback callback, int userId) {
1063            mCallbacks.register(callback, new UserHandle(userId));
1064        }
1065
1066        public void unregister(IPackageInstallerCallback callback) {
1067            mCallbacks.unregister(callback);
1068        }
1069
1070        @Override
1071        public void handleMessage(Message msg) {
1072            final int userId = msg.arg2;
1073            final int n = mCallbacks.beginBroadcast();
1074            for (int i = 0; i < n; i++) {
1075                final IPackageInstallerCallback callback = mCallbacks.getBroadcastItem(i);
1076                final UserHandle user = (UserHandle) mCallbacks.getBroadcastCookie(i);
1077                // TODO: dispatch notifications for slave profiles
1078                if (userId == user.getIdentifier()) {
1079                    try {
1080                        invokeCallback(callback, msg);
1081                    } catch (RemoteException ignored) {
1082                    }
1083                }
1084            }
1085            mCallbacks.finishBroadcast();
1086        }
1087
1088        private void invokeCallback(IPackageInstallerCallback callback, Message msg)
1089                throws RemoteException {
1090            final int sessionId = msg.arg1;
1091            switch (msg.what) {
1092                case MSG_SESSION_CREATED:
1093                    callback.onSessionCreated(sessionId);
1094                    break;
1095                case MSG_SESSION_BADGING_CHANGED:
1096                    callback.onSessionBadgingChanged(sessionId);
1097                    break;
1098                case MSG_SESSION_ACTIVE_CHANGED:
1099                    callback.onSessionActiveChanged(sessionId, (boolean) msg.obj);
1100                    break;
1101                case MSG_SESSION_PROGRESS_CHANGED:
1102                    callback.onSessionProgressChanged(sessionId, (float) msg.obj);
1103                    break;
1104                case MSG_SESSION_FINISHED:
1105                    callback.onSessionFinished(sessionId, (boolean) msg.obj);
1106                    break;
1107            }
1108        }
1109
1110        private void notifySessionCreated(int sessionId, int userId) {
1111            obtainMessage(MSG_SESSION_CREATED, sessionId, userId).sendToTarget();
1112        }
1113
1114        private void notifySessionBadgingChanged(int sessionId, int userId) {
1115            obtainMessage(MSG_SESSION_BADGING_CHANGED, sessionId, userId).sendToTarget();
1116        }
1117
1118        private void notifySessionActiveChanged(int sessionId, int userId, boolean active) {
1119            obtainMessage(MSG_SESSION_ACTIVE_CHANGED, sessionId, userId, active).sendToTarget();
1120        }
1121
1122        private void notifySessionProgressChanged(int sessionId, int userId, float progress) {
1123            obtainMessage(MSG_SESSION_PROGRESS_CHANGED, sessionId, userId, progress).sendToTarget();
1124        }
1125
1126        public void notifySessionFinished(int sessionId, int userId, boolean success) {
1127            obtainMessage(MSG_SESSION_FINISHED, sessionId, userId, success).sendToTarget();
1128        }
1129    }
1130
1131    void dump(IndentingPrintWriter pw) {
1132        synchronized (mSessions) {
1133            pw.println("Active install sessions:");
1134            pw.increaseIndent();
1135            int N = mSessions.size();
1136            for (int i = 0; i < N; i++) {
1137                final PackageInstallerSession session = mSessions.valueAt(i);
1138                session.dump(pw);
1139                pw.println();
1140            }
1141            pw.println();
1142            pw.decreaseIndent();
1143
1144            pw.println("Historical install sessions:");
1145            pw.increaseIndent();
1146            N = mHistoricalSessions.size();
1147            for (int i = 0; i < N; i++) {
1148                final PackageInstallerSession session = mHistoricalSessions.valueAt(i);
1149                session.dump(pw);
1150                pw.println();
1151            }
1152            pw.println();
1153            pw.decreaseIndent();
1154
1155            pw.println("Legacy install sessions:");
1156            pw.increaseIndent();
1157            pw.println(mLegacySessions.toString());
1158            pw.decreaseIndent();
1159        }
1160    }
1161
1162    class InternalCallback {
1163        public void onSessionBadgingChanged(PackageInstallerSession session) {
1164            mCallbacks.notifySessionBadgingChanged(session.sessionId, session.userId);
1165            writeSessionsAsync();
1166        }
1167
1168        public void onSessionActiveChanged(PackageInstallerSession session, boolean active) {
1169            mCallbacks.notifySessionActiveChanged(session.sessionId, session.userId, active);
1170        }
1171
1172        public void onSessionProgressChanged(PackageInstallerSession session, float progress) {
1173            mCallbacks.notifySessionProgressChanged(session.sessionId, session.userId, progress);
1174        }
1175
1176        public void onSessionFinished(final PackageInstallerSession session, boolean success) {
1177            mCallbacks.notifySessionFinished(session.sessionId, session.userId, success);
1178
1179            mInstallHandler.post(new Runnable() {
1180                @Override
1181                public void run() {
1182                    synchronized (mSessions) {
1183                        mSessions.remove(session.sessionId);
1184                        mHistoricalSessions.put(session.sessionId, session);
1185
1186                        final File appIconFile = buildAppIconFile(session.sessionId);
1187                        if (appIconFile.exists()) {
1188                            appIconFile.delete();
1189                        }
1190
1191                        writeSessionsLocked();
1192                    }
1193                }
1194            });
1195        }
1196
1197        public void onSessionPrepared(PackageInstallerSession session) {
1198            // We prepared the destination to write into; we want to persist
1199            // this, but it's not critical enough to block for.
1200            writeSessionsAsync();
1201        }
1202
1203        public void onSessionSealedBlocking(PackageInstallerSession session) {
1204            // It's very important that we block until we've recorded the
1205            // session as being sealed, since we never want to allow mutation
1206            // after sealing.
1207            synchronized (mSessions) {
1208                writeSessionsLocked();
1209            }
1210        }
1211    }
1212}
1213