Android 8.1 Source Core Chapter -- Deep Study of Package Manager Service Series (2)

Keywords: Android Java xml Mobile

Beginning

Core source code

Key Classes Route
SystemServer.java frameworks/base/services/java/com/android/server/SystemServer.java
PackageManagerService.java frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
Process.java frameworks/base/core/java/android/os/Process.java
SystemConfig.java frameworks/base/core/java/com/android/server/SystemConfig.java
Settings.java frameworks/base/services/core/java/com/android/server/pm/Settings.java

brief introduction

Package Manager Service (PMS) is the first core service of System Server after its launch, and it is also one of the most commonly used services in Android system. It is responsible for the management of Package in the system, installation, uninstallation, information query and so on. If you're an engineer for Android system development, I don't need to go into the basics anymore. We'll follow the source code directly.

Constructor Analysis-Scanning Package

The second phase of the PMS constructor is the APK in the scanning system. Because you need to scan files one by one, the more programs you install on your phone, the more workload PMS will have, and the slower the system will start. That's why your phone will start faster or slower.

dex optimization of system library

Then the PMS constructor above continues to analyze the source code:

    public PackageManagerService(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        ... ...

        // DEX optimization
        mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context,
                "*dexopt*");
        mDexManager = new DexManager(this, mPackageDexOptimizer, installer, mInstallLock);
        
        synchronized (mPackages) {
            mHandlerThread = new ServiceThread(TAG,
                    Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
            mHandlerThread.start();
            mHandler = new PackageHandler(mHandlerThread.getLooper());
            mProcessLoggingHandler = new ProcessLoggingHandler();
            Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);

            mDefaultPermissionPolicy = new DefaultPermissionGrantPolicy(this);
            mInstantAppRegistry = new InstantAppRegistry(this);

            // Generate File instances for some subdirectories in data / directory
            File dataDir = Environment.getDataDirectory();
            // data/app Stores Third Party Applications
            mAppInstallDir = new File(dataDir, "app");
            mAppLib32InstallDir = new File(dataDir, "app-lib");
            mAsecInternalPath = new File(dataDir, "app-asec").getPath();
            // Application of data/app-private storage drm protection
            mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
            sUserManager = new UserManagerService(context, this,
                    new UserDataPreparer(mInstaller, mInstallLock, mContext, mOnlyCore), mPackages);

            // Get the permission information parsed in System Config for the < Permission > tag identifier and save it to Settings::mPermissions
            ArrayMap<String, SystemConfig.PermissionEntry> permConfig
                    = systemConfig.getPermissions();
                    
            for (int i=0; i<permConfig.size(); i++) {
                SystemConfig.PermissionEntry perm = permConfig.valueAt(i);
                BasePermission bp = mSettings.mPermissions.get(perm.name);
                if (bp == null) {
                    bp = new BasePermission(perm.name, "android", BasePermission.TYPE_BUILTIN);
                    mSettings.mPermissions.put(perm.name, bp);
                }
                if (perm.gids != null) {
                    bp.setGids(perm.gids, perm.perUser);
                }
            }

            // Get a list of shared libraries in systems other than framework, and get parsed data from System Config
            ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
            final int builtInLibCount = libConfig.size();
            for (int i = 0; i < builtInLibCount; i++) {
                String name = libConfig.keyAt(i);
                String path = libConfig.valueAt(i);
                addSharedLibraryLPw(path, null, name, SharedLibraryInfo.VERSION_UNDEFINED,
                        SharedLibraryInfo.TYPE_BUILTIN, PLATFORM_PACKAGE_NAME, 0);
            }

            mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();

            // Read the contents of packages.xml and assign the mSettings::mPackages and other members; the contents of the packages.xml file are the result of the last scan of the apk directory;
            // The result of the current scan is saved in the PackageManagerService::mPackages list.
            // Compare the results of the last scan to check if there are any system applications covered by the upgrade package in the scanned application, and if so, remove them from PackageManagerService::mPackages.
            // In this way, the record of PackageManagerService::mPackages is consistent with mSettings::mPackages.
            // The system will eventually rewrite the results of this apk scan to packages.xml
            mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false));
            
            // Clean up orphaned packages for which the code path doesn't exist
            // and they are an update to a system app - caused by bug/32321269
            final int packageSettingCount = mSettings.mPackages.size();
            // Clean up exception package s that do not exist in code paths
            for (int i = packageSettingCount - 1; i >= 0; i--) {
                PackageSetting ps = mSettings.mPackages.valueAt(i);
                if (!isExternal(ps) && (ps.codePath == null || !ps.codePath.exists())
                        && mSettings.getDisabledSystemPkgLPr(ps.name) != null) {
                    mSettings.mPackages.removeAt(i);
                    mSettings.enableSystemPackageLPw(ps.name);
                }
            }

            if (mFirstBoot) {
                requestCopyPreoptedFiles();
            }

            // Set up modules to replace the default esolverActivity in framework-res.apk
            String customResolverActivity = Resources.getSystem().getString(
                    R.string.config_customResolverActivity);
            if (TextUtils.isEmpty(customResolverActivity)) {
                customResolverActivity = null;
            } else {
                mCustomResolverComponentName = ComponentName.unflattenFromString(
                        customResolverActivity);
            }
            
            long startTime = SystemClock.uptimeMillis();    // Record the start time of the scan

            // Some jar s that require the system to load ahead of time
            final String bootClassPath = System.getenv("BOOTCLASSPATH");
            final String systemServerClassPath = System.getenv("SYSTEMSERVERCLASSPATH");

            if (bootClassPath == null) {
                Slog.w(TAG, "No BOOTCLASSPATH found!");
            }

            if (systemServerClassPath == null) {
                Slog.w(TAG, "No SYSTEMSERVERCLASSPATH found!");
            }

Scanning System Package

After clearing the cache file, PMS finally entered the key section. Next, look at the core content of the second phase of PMS work, namely scanning Package. The relevant code is as follows:

// PackageManagerService.java

            // Define frameworkDir to point to / system/frameworks directory
            File frameworkDir = new File(Environment.getRootDirectory(), "framework");

            final VersionInfo ver = mSettings.getInternalVersion();
            mIsUpgrade = !Build.FINGERPRINT.equals(ver.fingerprint);

            // when upgrading from pre-M, promote system app permissions from install to runtime
            mPromoteSystemApps =
                    mIsUpgrade && ver.sdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1;

            // When upgrading from pre-N, we need to handle package extraction like first boot,
            // as there is no profiling data available.
            mIsPreNUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N;

            mIsPreNMR1Upgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N_MR1;

            // save off the names of pre-existing system packages prior to scanning; we don't
            // want to automatically grant runtime permissions for new system apps
            // Do you need to upgrade permissions?
            if (mPromoteSystemApps) {
                Iterator<PackageSetting> pkgSettingIter = mSettings.mPackages.values().iterator();
                while (pkgSettingIter.hasNext()) {
                    PackageSetting ps = pkgSettingIter.next();
                    if (isSystemApp(ps)) {
                    // Traverse the Settings::mPackages collection and add the system APP to PackageManagerService:: mExisting System Packages
                        mExistingSystemPackages.add(ps.name);
                    }
                }
            }

            mCacheDir = preparePackageParserCache(mIsUpgrade);

            // Define scanning parameters
            int scanFlags = SCAN_BOOTING | SCAN_INITIAL;
            if (mIsUpgrade || mFirstBoot) {
                scanFlags = scanFlags | SCAN_FIRST_BOOT_OR_UPGRADE;
            }
            
            // Scan the / vendor/overlay directory first
            scanDirTracedLI(new File(VENDOR_OVERLAY_DIR), mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR
                    | PackageParser.PARSE_TRUSTED_OVERLAY, scanFlags | SCAN_TRUSTED_OVERLAY, 0);
                    
            // Call the scanDirTracedLI function to scan the / system/frameworks directory
            scanDirTracedLI(frameworkDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR
                    | PackageParser.PARSE_IS_PRIVILEGED,
                    scanFlags | SCAN_NO_DEX, 0);

            // Collected privileged system packages.
            final File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");
            // package under scanning / system/priv-app
            scanDirTracedLI(privilegedAppDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR
                    | PackageParser.PARSE_IS_PRIVILEGED, scanFlags, 0);

            // Collect ordinary system packages.
            final File systemAppDir = new File(Environment.getRootDirectory(), "app");
            // package under scan / system/app
            scanDirTracedLI(systemAppDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

            // Collect all vendor packages.
            File vendorAppDir = new File("/vendor/app");
            try {
                vendorAppDir = vendorAppDir.getCanonicalFile();
            } catch (IOException e) {
                // failed to look up canonical path, continue with original one
            }
            // package under scan / vendor/app
            scanDirTracedLI(vendorAppDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);
                    
            // Collect all OEM packages.
            final File oemAppDir = new File(Environment.getOemDirectory(), "app");
            // Package Scanning OEM
            scanDirTracedLI(oemAppDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

As can be seen from the above code, PMS will scan the following directories (highlights only):

/system/frameworks: The files in this directory are system libraries, such as framework.jar, services.jar, framework-res.apk. However, scanDirTracedLI scans only APK files, so framework-res.apk is the only "favorite" file in the directory.

/system/app: This directory is full of default system applications. For example: Browser.apk, Settings Provider. apk, etc.

/vendor/app: The files in this directory are provided by the manufacturer, that is, all the manufacturer-specific APK files. At present, the manufacturer on the market has put their own applications in the / system/app directory.

scanDirTracedLI

PMS calls the scanDirTracedLI function for scanning, which is analyzed below:

    public void scanDirTracedLI(File dir, final int parseFlags, int scanFlags, long currentTime) {
        try {
            scanDirLI(dir, parseFlags, scanFlags, currentTime);  // Call the scanDirLI function
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }
    }

We can see that the scanDirLI function is actually called to scan!

scanDirLI

The following focus is on the scanDirLI function:

    private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {
        // List the files in this directory
        final File[] files = dir.listFiles();
        if (ArrayUtils.isEmpty(files)) {
            Log.d(TAG, "No files in app dir " + dir);
            return;
        }

        ParallelPackageParser parallelPackageParser = new ParallelPackageParser(
                mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir,
                mParallelPackageParserCallback);

        int fileCount = 0;
        for (File file : files) {
            final boolean isPackage = (isApkFile(file) || file.isDirectory())
                    && !PackageInstallerService.isStageName(file.getName());
            if (!isPackage) {
                // Ignore entries which are not packages
                continue;
            }
            parallelPackageParser.submit(file, parseFlags);
            fileCount++;
        }

        // Process results one by one
        for (; fileCount > 0; fileCount--) {
            ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
            Throwable throwable = parseResult.throwable;
            int errorCode = PackageManager.INSTALL_SUCCEEDED;

            if (throwable == null) {
                // Static shared libraries have synthetic package names
                if (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) {
                    renameStaticSharedLibraryPackage(parseResult.pkg);
                }
                try {
                    if (errorCode == PackageManager.INSTALL_SUCCEEDED) {
                        // Call the scanPackageLI function to scan a specific file. The return value is PackageParser's inner class Package, whose instance represents an APK file, so it is the data structure corresponding to the APK file.
                        scanPackageLI(parseResult.pkg, parseResult.scanFile, parseFlags, scanFlags,
                                currentTime, null);
                    }
                ... ...
            }

            // Delete invalid userdata apps
            if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&
                    errorCode == PackageManager.INSTALL_FAILED_INVALID_APK) {
                // Non-System Package Scan Failed, File Deleted
                removeCodePathLI(parseResult.scanFile);
            }
        }
        parallelPackageParser.close();
    }

scanPackageLI - 01

There are three scanPackageLI s in PMS. We will analyze them one by one later. First, let's look at the first and first sanPackageLI function.

    private PackageParser.Package scanPackageLI(PackageParser.Package pkg, File scanFile,
            final int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user)
            throws PackageManagerException {
        if ((scanFlags & SCAN_CHECK_ONLY) == 0) {
            if (pkg.childPackages != null && pkg.childPackages.size() > 0) {
                scanFlags |= SCAN_CHECK_ONLY;
            }
        } else {
            scanFlags &= ~SCAN_CHECK_ONLY;
        }

        // Scan the parent
        PackageParser.Package scannedPkg = scanPackageInternalLI(pkg, scanFile, policyFlags,
                scanFlags, currentTime, user);

        // Scan the children
        final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;
        for (int i = 0; i < childCount; i++) {
            PackageParser.Package childPackage = pkg.childPackages.get(i);
            scanPackageInternalLI(childPackage, scanFile, policyFlags, scanFlags,
                    currentTime, user);
        }


        if ((scanFlags & SCAN_CHECK_ONLY) != 0) {
            return scanPackageLI(pkg, scanFile, policyFlags, scanFlags, currentTime, user);
        }

        return scannedPkg;
    }

scanPackageInternalLI

Call scanPackageInternalLI():

    private PackageParser.Package scanPackageInternalLI(PackageParser.Package pkg, File scanFile,
            int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user)
            throws PackageManagerException {
        PackageSetting ps = null;
        PackageSetting updatedPkg;
        // Determine whether the system APP needs to be updated
        synchronized (mPackages) {
            // Check to see if the installation package already exists. Find it through mSetting
            String oldName = mSettings.getRenamedPackageLPr(pkg.packageName);
            if (pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(oldName)) {
                // If an old installation package with the same package name exists and has been changed back to its original name
                ps = mSettings.getPackageLPr(oldName);
            }
            // If there is no original package, use the real package name
            if (ps == null) {
                ps = mSettings.getPackageLPr(pkg.packageName);
            }
            // Check if this package is a hidden or updatable system package?
            updatedPkg = mSettings.getDisabledSystemPkgLPr(ps != null ? ps.name : pkg.packageName);

            // If this is a package we don't know about on the system partition, we
            // may need to remove disabled child packages on the system partition
            // or may need to not add child packages if the parent apk is updated
            // on the data partition and no longer defines this child package.
            if ((policyFlags & PackageParser.PARSE_IS_SYSTEM) != 0) {
                // If this is a parent package for an updated system app and this system
                // app got an OTA update which no longer defines some of the child packages
                // we have to prune them from the disabled system packages.
                PackageSetting disabledPs = mSettings.getDisabledSystemPkgLPr(pkg.packageName);
                if (disabledPs != null) {
                    final int scannedChildCount = (pkg.childPackages != null)
                            ? pkg.childPackages.size() : 0;
                    final int disabledChildCount = disabledPs.childPackageNames != null
                            ? disabledPs.childPackageNames.size() : 0;
                    for (int i = 0; i < disabledChildCount; i++) {
                        String disabledChildPackageName = disabledPs.childPackageNames.get(i);
                        boolean disabledPackageAvailable = false;
                        for (int j = 0; j < scannedChildCount; j++) {
                            PackageParser.Package childPkg = pkg.childPackages.get(j);
                            if (childPkg.packageName.equals(disabledChildPackageName)) {
                                disabledPackageAvailable = true;
                                break;
                            }
                         }
                         if (!disabledPackageAvailable) {
                             mSettings.removeDisabledSystemPackageLPw(disabledChildPackageName);
                         }
                    }
                }
            }
        }
        ... ...
        
        // Note that we invoke the following method only if we are about to unpack an application
        // Call the second scanPackageLI function
        PackageParser.Package scannedPkg = scanPackageLI(pkg, policyFlags, scanFlags
                | SCAN_UPDATE_SIGNATURE, currentTime, user);

        ... ...
    }

scanPackageLI - 02

    private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags,
            long currentTime, UserHandle user) throws PackageManagerException {
        // Create a PackageParser object for parsing packages
        PackageParser pp = new PackageParser();

        // Setting three properties of PackageParse
        pp.setSeparateProcesses(mSeparateProcesses);
        pp.setOnlyCoreApps(mOnlyCore);
        pp.setDisplayMetrics(mMetrics);

        pp.setCallback(mPackageParserCallback);

        // Judging Scanning Mode
        if ((scanFlags & SCAN_TRUSTED_OVERLAY) != 0) {
            parseFlags |= PackageParser.PARSE_TRUSTED_OVERLAY;
        }

        // Parsing APK to get corresponding PackageParser.Package object pkg
        final PackageParser.Package pkg;

        // Parsing APK file by calling parsePackage function of PackageParser
        try {
            // (vi), (vi), (vi), (vi), (vi) Real Analysis
            pkg = pp.parsePackage(scanFile, parseFlags);
        } catch (PackageParserException e) {
            throw PackageManagerException.from(e);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }

        // Static shared libraries have synthetic package names
        if (pkg.applicationInfo.isStaticSharedLibrary()) {
            renameStaticSharedLibraryPackage(pkg);
        }

        return scanPackageLI(pkg, scanFile, parseFlags, scanFlags, currentTime, user);
    }

PackageParser

PackageParser is mainly responsible for parsing APK files, that is, parsing Android Manifest. XML in APK files.

Here we want to focus on the analysis of PackageParser class!!!

Official Interpretation

When Android installs an APK, it parses the APK first, while parsing the APK requires a tool class called PackageParser.

/**
 * Parser for package files (APKs) on disk. This supports apps packaged either
 * as a single "monolithic" APK, or apps packaged as a "cluster" of multiple
 * APKs in a single directory.
 * <p>
 * Apps packaged as multiple APKs always consist of a single "base" APK (with a
 * {@code null} split name) and zero or more "split" APKs (with unique split
 * names). Any subset of those split APKs are a valid install, as long as the
 * following constraints are met:
 * <ul>
 * <li>All APKs must have the exact same package name, version code, and signing
 * certificates.
 * <li>All APKs must have unique split names.
 * <li>All installations must contain a single base APK.
 * </ul>
 *
 * @hide
 */
public class PackageParser {

The explanation is as follows:

Parse APK installation package files on disk. It can parse both a "single" APK file and a "cluster" APK file (that is, an APK file contains multiple APK files).
A "cluster" APK consists of a "base APK" and other "split" APKs, in which these "split" APKs are divided by numbers.
These "split" APK s must be installed effectively and must satisfy the following conditions:
    All APK s must have exactly the same package name, version code and signature certificate
    All APK s must have unique splitting names
    All installations must contain a single APK.

Analytical steps

So we know the PackageParse class, which is mainly used to parse APK files on mobile phones (support Single APK and MultipleAPK), parsing an APK is mainly divided into two steps:
    Parsing APK into Package: The process of parsing APK files as Package objects.
    2. Converting Package to PackageInfo: The process of generating PackageInfo from Package objects.

Let's look at the parsePackage method next.

parsePackage

    /**
     * Parse the package at the given location. Automatically detects if the
     * package is a monolithic style (single APK file) or cluster style
     * (directory of APKs).
     * <p>
     * This performs sanity checking on cluster style packages, such as
     * requiring identical package name and version codes, a single base APK,
     * and unique split names.
     * <p>
     * Note that this <em>does not</em> perform signature verification; that
     * must be done separately in {@link #collectCertificates(Package, int)}.
     *
     * If {@code useCaches} is true, the package parser might return a cached
     * result from a previous parse of the same {@code packageFile} with the same
     * {@code flags}. Note that this method does not check whether {@code packageFile}
     * has changed since the last parse, it's up to callers to do so.
     *
     * @see #parsePackageLite(File, int)
     */
    public Package parsePackage(File packageFile, int flags) throws PackageParserException {
        return parsePackage(packageFile, flags, false /* useCaches */);
    }
    
    public Package parsePackage(File packageFile, int flags, boolean useCaches)
            throws PackageParserException {
        Package parsed = useCaches ? getCachedResult(packageFile, flags) : null;
        if (parsed != null) {
            return parsed;
        }

        long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
        if (packageFile.isDirectory()) {
            // Cluster APK
            parsed = parseClusterPackage(packageFile, flags);    // Get an XML resource parsing object
        } else {
            // Single APK
            parsed = parseMonolithicPackage(packageFile, flags); // Get an XML resource parsing object
        }

        long cacheTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
        cacheResult(packageFile, flags, parsed);
        if (LOG_PARSE_TIMINGS) {
            parseTime = cacheTime - parseTime;
            cacheTime = SystemClock.uptimeMillis() - cacheTime;
            if (parseTime + cacheTime > LOG_PARSE_TIMINGS_THRESHOLD_MS) {
                Slog.i(TAG, "Parse times for '" + packageFile + "': parse=" + parseTime
                        + "ms, update_cache=" + cacheTime + " ms");
            }
        }
        return parsed;
    }

parseMonolithicPackage

Here we take a single APK as an example to track the source code flow:

    /**
     * Parse the given APK file, treating it as as a single monolithic package.
     * <p>
     * Note that this <em>does not</em> perform signature verification; that
     * must be done separately in {@link #collectCertificates(Package, int)}.
     *
     * @deprecated external callers should move to
     *             {@link #parsePackage(File, int)}. Eventually this method will
     *             be marked private.
     */
    @Deprecated
    public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
        final AssetManager assets = newConfiguredAssetManager();
        // (viii), (viii), (viii) Focus analysis
        final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);   
        
        if (mOnlyCoreApps) {
            if (!lite.coreApp) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
                        "Not a coreApp: " + apkFile);
            }
        }

        try {
            // (viii), (viii), (viii) Focus analysis
            // Call the parseBaseApk() method to parse an apk and generate a Package object
            final Package pkg = parseBaseApk(apkFile, assets, flags);
            pkg.setCodePath(apkFile.getAbsolutePath());
            pkg.setUse32bitAbi(lite.use32bitAbi);
            return pkg;
        } finally {
            IoUtils.closeQuietly(assets);
        }
    }

[Explanation] Unfinished, due to the limitation of space and word number, the analysis of the remaining code can be found in [Android 8.1 Source Code Core Chapter - Deep Study of Package Manager Service Series (3)]

Posted by mike97gt on Sat, 19 Jan 2019 23:27:14 -0800