blob: 1d3d24c270412e348951477614bba687de4b3558 [file] [log] [blame]
Patrick Baumannf62817e2019-05-31 10:46:53 -07001/*
2 * Copyright (C) 2019 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 android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
20import static android.content.pm.PackageParser.isApkFile;
21import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
22
23import static com.android.internal.content.NativeLibraryHelper.LIB64_DIR_NAME;
24import static com.android.internal.content.NativeLibraryHelper.LIB_DIR_NAME;
25import static com.android.server.pm.InstructionSets.getPreferredInstructionSet;
26import static com.android.server.pm.InstructionSets.getPrimaryInstructionSet;
27
28import android.annotation.Nullable;
29import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
31import android.content.pm.PackageParser;
32import android.os.Build;
33import android.os.Environment;
34import android.os.FileUtils;
35import android.os.Trace;
36import android.text.TextUtils;
Patrick Baumann235e52e2019-07-25 16:28:45 -070037import android.util.Pair;
Patrick Baumannf62817e2019-05-31 10:46:53 -070038import android.util.Slog;
39
40import com.android.internal.content.NativeLibraryHelper;
41import com.android.internal.util.ArrayUtils;
42
43import dalvik.system.VMRuntime;
44
45import libcore.io.IoUtils;
46
47import java.io.File;
48import java.io.IOException;
Patrick Baumannf62817e2019-05-31 10:46:53 -070049import java.util.Set;
50
51final class PackageAbiHelperImpl implements PackageAbiHelper {
52
Patrick Baumannf62817e2019-05-31 10:46:53 -070053 private static String calculateBundledApkRoot(final String codePathString) {
54 final File codePath = new File(codePathString);
55 final File codeRoot;
56 if (FileUtils.contains(Environment.getRootDirectory(), codePath)) {
57 codeRoot = Environment.getRootDirectory();
58 } else if (FileUtils.contains(Environment.getOemDirectory(), codePath)) {
59 codeRoot = Environment.getOemDirectory();
60 } else if (FileUtils.contains(Environment.getVendorDirectory(), codePath)) {
61 codeRoot = Environment.getVendorDirectory();
62 } else if (FileUtils.contains(Environment.getOdmDirectory(), codePath)) {
63 codeRoot = Environment.getOdmDirectory();
64 } else if (FileUtils.contains(Environment.getProductDirectory(), codePath)) {
65 codeRoot = Environment.getProductDirectory();
66 } else if (FileUtils.contains(Environment.getSystemExtDirectory(), codePath)) {
67 codeRoot = Environment.getSystemExtDirectory();
68 } else if (FileUtils.contains(Environment.getOdmDirectory(), codePath)) {
69 codeRoot = Environment.getOdmDirectory();
70 } else {
71 // Unrecognized code path; take its top real segment as the apk root:
72 // e.g. /something/app/blah.apk => /something
73 try {
74 File f = codePath.getCanonicalFile();
75 File parent = f.getParentFile(); // non-null because codePath is a file
76 File tmp;
77 while ((tmp = parent.getParentFile()) != null) {
78 f = parent;
79 parent = tmp;
80 }
81 codeRoot = f;
82 Slog.w(PackageManagerService.TAG, "Unrecognized code path "
83 + codePath + " - using " + codeRoot);
84 } catch (IOException e) {
85 // Can't canonicalize the code path -- shenanigans?
86 Slog.w(PackageManagerService.TAG, "Can't canonicalize code path " + codePath);
87 return Environment.getRootDirectory().getPath();
88 }
89 }
90 return codeRoot.getPath();
91 }
92
93 // Utility method that returns the relative package path with respect
94 // to the installation directory. Like say for /data/data/com.test-1.apk
95 // string com.test-1 is returned.
96 private static String deriveCodePathName(String codePath) {
97 if (codePath == null) {
98 return null;
99 }
100 final File codeFile = new File(codePath);
101 final String name = codeFile.getName();
102 if (codeFile.isDirectory()) {
103 return name;
104 } else if (name.endsWith(".apk") || name.endsWith(".tmp")) {
105 final int lastDot = name.lastIndexOf('.');
106 return name.substring(0, lastDot);
107 } else {
108 Slog.w(PackageManagerService.TAG, "Odd, " + codePath + " doesn't look like an APK");
109 return null;
110 }
111 }
112
113 private static void maybeThrowExceptionForMultiArchCopy(String message, int copyRet) throws
114 PackageManagerException {
115 if (copyRet < 0) {
116 if (copyRet != PackageManager.NO_NATIVE_LIBRARIES
117 && copyRet != PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS) {
118 throw new PackageManagerException(copyRet, message);
119 }
120 }
121 }
122
Patrick Baumann235e52e2019-07-25 16:28:45 -0700123 @Override
124 public NativeLibraryPaths getNativeLibraryPaths(
125 PackageParser.Package pkg, File appLib32InstallDir) {
126 return getNativeLibraryPaths(new Abis(pkg), appLib32InstallDir, pkg.codePath,
127 pkg.applicationInfo.sourceDir, pkg.applicationInfo.isSystemApp(),
128 pkg.applicationInfo.isUpdatedSystemApp());
129 }
130
131 private static NativeLibraryPaths getNativeLibraryPaths(final Abis abis,
132 final File appLib32InstallDir, final String codePath, final String sourceDir,
133 final boolean isSystemApp, final boolean isUpdatedSystemApp) {
134 final File codeFile = new File(codePath);
135 final boolean bundledApp = isSystemApp && !isUpdatedSystemApp;
136
137 final String nativeLibraryRootDir;
138 final boolean nativeLibraryRootRequiresIsa;
139 final String nativeLibraryDir;
140 final String secondaryNativeLibraryDir;
141
142 if (isApkFile(codeFile)) {
143 // Monolithic install
144 if (bundledApp) {
145 // If "/system/lib64/apkname" exists, assume that is the per-package
146 // native library directory to use; otherwise use "/system/lib/apkname".
147 final String apkRoot = calculateBundledApkRoot(sourceDir);
148 final boolean is64Bit = VMRuntime.is64BitInstructionSet(
149 getPrimaryInstructionSet(abis));
150
151 // This is a bundled system app so choose the path based on the ABI.
152 // if it's a 64 bit abi, use lib64 otherwise use lib32. Note that this
153 // is just the default path.
154 final String apkName = deriveCodePathName(codePath);
155 final String libDir = is64Bit ? LIB64_DIR_NAME : LIB_DIR_NAME;
156 nativeLibraryRootDir = Environment.buildPath(new File(apkRoot), libDir,
157 apkName).getAbsolutePath();
158
159 if (abis.secondary != null) {
160 final String secondaryLibDir = is64Bit ? LIB_DIR_NAME : LIB64_DIR_NAME;
161 secondaryNativeLibraryDir = Environment.buildPath(new File(apkRoot),
162 secondaryLibDir, apkName).getAbsolutePath();
163 } else {
164 secondaryNativeLibraryDir = null;
165 }
166 } else {
167 final String apkName = deriveCodePathName(codePath);
168 nativeLibraryRootDir = new File(appLib32InstallDir, apkName)
169 .getAbsolutePath();
170 secondaryNativeLibraryDir = null;
171 }
172
173 nativeLibraryRootRequiresIsa = false;
174 nativeLibraryDir = nativeLibraryRootDir;
175 } else {
176 // Cluster install
177 nativeLibraryRootDir = new File(codeFile, LIB_DIR_NAME).getAbsolutePath();
178 nativeLibraryRootRequiresIsa = true;
179
180 nativeLibraryDir = new File(nativeLibraryRootDir,
181 getPrimaryInstructionSet(abis)).getAbsolutePath();
182
183 if (abis.secondary != null) {
184 secondaryNativeLibraryDir = new File(nativeLibraryRootDir,
185 VMRuntime.getInstructionSet(abis.secondary)).getAbsolutePath();
186 } else {
187 secondaryNativeLibraryDir = null;
188 }
189 }
190 return new NativeLibraryPaths(nativeLibraryRootDir, nativeLibraryRootRequiresIsa,
191 nativeLibraryDir, secondaryNativeLibraryDir);
192 }
193
194 @Override
195 public Abis getBundledAppAbis(PackageParser.Package pkg) {
196 final String apkName = deriveCodePathName(pkg.applicationInfo.getCodePath());
197
198 // If "/system/lib64/apkname" exists, assume that is the per-package
199 // native library directory to use; otherwise use "/system/lib/apkname".
200 final String apkRoot = calculateBundledApkRoot(pkg.applicationInfo.sourceDir);
201 final Abis abis = getBundledAppAbi(pkg, apkRoot, apkName);
202 return abis;
203 }
204
205 /**
206 * Deduces the ABI of a bundled app and sets the relevant fields on the
207 * parsed pkg object.
208 *
209 * @param apkRoot the root of the installed apk, something like {@code /system} or
210 * {@code /oem} under which system libraries are installed.
211 * @param apkName the name of the installed package.
212 */
213 private Abis getBundledAppAbi(PackageParser.Package pkg, String apkRoot, String apkName) {
214 final File codeFile = new File(pkg.codePath);
215
216 final boolean has64BitLibs;
217 final boolean has32BitLibs;
218
219 final String primaryCpuAbi;
220 final String secondaryCpuAbi;
221 if (isApkFile(codeFile)) {
222 // Monolithic install
223 has64BitLibs =
224 (new File(apkRoot, new File(LIB64_DIR_NAME, apkName).getPath())).exists();
225 has32BitLibs = (new File(apkRoot, new File(LIB_DIR_NAME, apkName).getPath())).exists();
226 } else {
227 // Cluster install
228 final File rootDir = new File(codeFile, LIB_DIR_NAME);
229 if (!ArrayUtils.isEmpty(Build.SUPPORTED_64_BIT_ABIS)
230 && !TextUtils.isEmpty(Build.SUPPORTED_64_BIT_ABIS[0])) {
231 final String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_64_BIT_ABIS[0]);
232 has64BitLibs = (new File(rootDir, isa)).exists();
233 } else {
234 has64BitLibs = false;
235 }
236 if (!ArrayUtils.isEmpty(Build.SUPPORTED_32_BIT_ABIS)
237 && !TextUtils.isEmpty(Build.SUPPORTED_32_BIT_ABIS[0])) {
238 final String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_32_BIT_ABIS[0]);
239 has32BitLibs = (new File(rootDir, isa)).exists();
240 } else {
241 has32BitLibs = false;
242 }
243 }
244
245 if (has64BitLibs && !has32BitLibs) {
246 // The package has 64 bit libs, but not 32 bit libs. Its primary
247 // ABI should be 64 bit. We can safely assume here that the bundled
248 // native libraries correspond to the most preferred ABI in the list.
249
250 primaryCpuAbi = Build.SUPPORTED_64_BIT_ABIS[0];
251 secondaryCpuAbi = null;
252 } else if (has32BitLibs && !has64BitLibs) {
253 // The package has 32 bit libs but not 64 bit libs. Its primary
254 // ABI should be 32 bit.
255
256 primaryCpuAbi = Build.SUPPORTED_32_BIT_ABIS[0];
257 secondaryCpuAbi = null;
258 } else if (has32BitLibs && has64BitLibs) {
259 // The application has both 64 and 32 bit bundled libraries. We check
260 // here that the app declares multiArch support, and warn if it doesn't.
261 //
262 // We will be lenient here and record both ABIs. The primary will be the
263 // ABI that's higher on the list, i.e, a device that's configured to prefer
264 // 64 bit apps will see a 64 bit primary ABI,
265
266 if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_MULTIARCH) == 0) {
267 Slog.e(PackageManagerService.TAG,
268 "Package " + pkg + " has multiple bundled libs, but is not multiarch.");
269 }
270
271 if (VMRuntime.is64BitInstructionSet(getPreferredInstructionSet())) {
272 primaryCpuAbi = Build.SUPPORTED_64_BIT_ABIS[0];
273 secondaryCpuAbi = Build.SUPPORTED_32_BIT_ABIS[0];
274 } else {
275 primaryCpuAbi = Build.SUPPORTED_32_BIT_ABIS[0];
276 secondaryCpuAbi = Build.SUPPORTED_64_BIT_ABIS[0];
277 }
278 } else {
279 primaryCpuAbi = null;
280 secondaryCpuAbi = null;
281 }
282 return new Abis(primaryCpuAbi, secondaryCpuAbi);
283 }
284
285 @Override
286 public Pair<Abis, NativeLibraryPaths> derivePackageAbi(
287 PackageParser.Package pkg, String cpuAbiOverride, boolean extractLibs)
288 throws PackageManagerException {
289 // Give ourselves some initial paths; we'll come back for another
290 // pass once we've determined ABI below.
291 final NativeLibraryPaths initialLibraryPaths = getNativeLibraryPaths(new Abis(pkg),
292 PackageManagerService.sAppLib32InstallDir, pkg.codePath,
293 pkg.applicationInfo.sourceDir, pkg.applicationInfo.isSystemApp(),
294 pkg.applicationInfo.isUpdatedSystemApp());
295
296 // We shouldn't attempt to extract libs from system app when it was not updated.
297 if (PackageManagerService.isSystemApp(pkg) && !pkg.isUpdatedSystemApp()) {
298 extractLibs = false;
299 }
300
301 final String nativeLibraryRootStr = initialLibraryPaths.nativeLibraryRootDir;
302 final boolean useIsaSpecificSubdirs = initialLibraryPaths.nativeLibraryRootRequiresIsa;
303
304 String primaryCpuAbi = null;
305 String secondaryCpuAbi = null;
306
307 NativeLibraryHelper.Handle handle = null;
308 try {
309 handle = NativeLibraryHelper.Handle.create(pkg);
310 // TODO(multiArch): This can be null for apps that didn't go through the
311 // usual installation process. We can calculate it again, like we
312 // do during install time.
313 //
314 // TODO(multiArch): Why do we need to rescan ASEC apps again ? It seems totally
315 // unnecessary.
316 final File nativeLibraryRoot = new File(nativeLibraryRootStr);
317
318 // Null out the abis so that they can be recalculated.
319 primaryCpuAbi = null;
320 secondaryCpuAbi = null;
321 if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_MULTIARCH) != 0) {
322 // Warn if we've set an abiOverride for multi-lib packages..
323 // By definition, we need to copy both 32 and 64 bit libraries for
324 // such packages.
325 if (pkg.cpuAbiOverride != null
326 && !NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(pkg.cpuAbiOverride)) {
327 Slog.w(PackageManagerService.TAG,
328 "Ignoring abiOverride for multi arch application.");
329 }
330
331 int abi32 = PackageManager.NO_NATIVE_LIBRARIES;
332 int abi64 = PackageManager.NO_NATIVE_LIBRARIES;
333 if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
334 if (extractLibs) {
335 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "copyNativeBinaries");
336 abi32 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
337 nativeLibraryRoot, Build.SUPPORTED_32_BIT_ABIS,
338 useIsaSpecificSubdirs);
339 } else {
340 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "findSupportedAbi");
341 abi32 = NativeLibraryHelper.findSupportedAbi(
342 handle, Build.SUPPORTED_32_BIT_ABIS);
343 }
344 Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
345 }
346
347 // Shared library native code should be in the APK zip aligned
348 if (abi32 >= 0 && pkg.isLibrary() && extractLibs) {
349 throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
350 "Shared library native lib extraction not supported");
351 }
352
353 maybeThrowExceptionForMultiArchCopy(
354 "Error unpackaging 32 bit native libs for multiarch app.", abi32);
355
356 if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
357 if (extractLibs) {
358 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "copyNativeBinaries");
359 abi64 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
360 nativeLibraryRoot, Build.SUPPORTED_64_BIT_ABIS,
361 useIsaSpecificSubdirs);
362 } else {
363 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "findSupportedAbi");
364 abi64 = NativeLibraryHelper.findSupportedAbi(
365 handle, Build.SUPPORTED_64_BIT_ABIS);
366 }
367 Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
368 }
369
370 maybeThrowExceptionForMultiArchCopy(
371 "Error unpackaging 64 bit native libs for multiarch app.", abi64);
372
373 if (abi64 >= 0) {
374 // Shared library native libs should be in the APK zip aligned
375 if (extractLibs && pkg.isLibrary()) {
376 throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
377 "Shared library native lib extraction not supported");
378 }
379 primaryCpuAbi = Build.SUPPORTED_64_BIT_ABIS[abi64];
380 }
381
382 if (abi32 >= 0) {
383 final String abi = Build.SUPPORTED_32_BIT_ABIS[abi32];
384 if (abi64 >= 0) {
385 if (pkg.use32bitAbi) {
386 secondaryCpuAbi = primaryCpuAbi;
387 primaryCpuAbi = abi;
388 } else {
389 secondaryCpuAbi = abi;
390 }
391 } else {
392 primaryCpuAbi = abi;
393 }
394 }
395 } else {
396 String[] abiList = (cpuAbiOverride != null)
397 ? new String[]{cpuAbiOverride} : Build.SUPPORTED_ABIS;
398
399 // Enable gross and lame hacks for apps that are built with old
400 // SDK tools. We must scan their APKs for renderscript bitcode and
401 // not launch them if it's present. Don't bother checking on devices
402 // that don't have 64 bit support.
403 boolean needsRenderScriptOverride = false;
404 if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null
405 && NativeLibraryHelper.hasRenderscriptBitcode(handle)) {
406 abiList = Build.SUPPORTED_32_BIT_ABIS;
407 needsRenderScriptOverride = true;
408 }
409
410 final int copyRet;
411 if (extractLibs) {
412 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "copyNativeBinaries");
413 copyRet = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
414 nativeLibraryRoot, abiList, useIsaSpecificSubdirs);
415 } else {
416 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "findSupportedAbi");
417 copyRet = NativeLibraryHelper.findSupportedAbi(handle, abiList);
418 }
419 Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
420
421 if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES) {
422 throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
423 "Error unpackaging native libs for app, errorCode=" + copyRet);
424 }
425
426 if (copyRet >= 0) {
427 // Shared libraries that have native libs must be multi-architecture
428 if (pkg.isLibrary()) {
429 throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
430 "Shared library with native libs must be multiarch");
431 }
432 primaryCpuAbi = abiList[copyRet];
433 } else if (copyRet == PackageManager.NO_NATIVE_LIBRARIES
434 && cpuAbiOverride != null) {
435 primaryCpuAbi = cpuAbiOverride;
436 } else if (needsRenderScriptOverride) {
437 primaryCpuAbi = abiList[0];
438 }
439 }
440 } catch (IOException ioe) {
441 Slog.e(PackageManagerService.TAG, "Unable to get canonical file " + ioe.toString());
442 } finally {
443 IoUtils.closeQuietly(handle);
444 }
445
446 // Now that we've calculated the ABIs and determined if it's an internal app,
447 // we will go ahead and populate the nativeLibraryPath.
448
449 final Abis abis = new Abis(primaryCpuAbi, secondaryCpuAbi);
450 return new Pair<>(abis,
451 getNativeLibraryPaths(abis, PackageManagerService.sAppLib32InstallDir,
452 pkg.codePath, pkg.applicationInfo.sourceDir,
453 pkg.applicationInfo.isSystemApp(),
454 pkg.applicationInfo.isUpdatedSystemApp()));
455 }
456
457 /**
458 * Adjusts ABIs for a set of packages belonging to a shared user so that they all match.
459 * i.e, so that all packages can be run inside a single process if required.
460 *
461 * Optionally, callers can pass in a parsed package via {@code newPackage} in which case
462 * this function will either try and make the ABI for all packages in
463 * {@code packagesForUser} match {@code scannedPackage} or will update the ABI of
464 * {@code scannedPackage} to match the ABI selected for {@code packagesForUser}. This
465 * variant is used when installing or updating a package that belongs to a shared user.
466 *
467 * NOTE: We currently only match for the primary CPU abi string. Matching the secondary
468 * adds unnecessary complexity.
469 */
470 @Override
471 @Nullable
472 public String getAdjustedAbiForSharedUser(
473 Set<PackageSetting> packagesForUser, PackageParser.Package scannedPackage) {
474 String requiredInstructionSet = null;
475 if (scannedPackage != null && scannedPackage.applicationInfo.primaryCpuAbi != null) {
476 requiredInstructionSet = VMRuntime.getInstructionSet(
477 scannedPackage.applicationInfo.primaryCpuAbi);
478 }
479
480 PackageSetting requirer = null;
481 for (PackageSetting ps : packagesForUser) {
482 // If packagesForUser contains scannedPackage, we skip it. This will happen
483 // when scannedPackage is an update of an existing package. Without this check,
484 // we will never be able to change the ABI of any package belonging to a shared
485 // user, even if it's compatible with other packages.
486 if (scannedPackage != null && scannedPackage.packageName.equals(ps.name)) {
487 continue;
488 }
489 if (ps.primaryCpuAbiString == null) {
490 continue;
491 }
492
493 final String instructionSet =
494 VMRuntime.getInstructionSet(ps.primaryCpuAbiString);
495 if (requiredInstructionSet != null && !requiredInstructionSet.equals(instructionSet)) {
496 // We have a mismatch between instruction sets (say arm vs arm64) warn about
497 // this but there's not much we can do.
498 String errorMessage = "Instruction set mismatch, "
499 + ((requirer == null) ? "[caller]" : requirer)
500 + " requires " + requiredInstructionSet + " whereas " + ps
501 + " requires " + instructionSet;
502 Slog.w(PackageManagerService.TAG, errorMessage);
503 }
504
505 if (requiredInstructionSet == null) {
506 requiredInstructionSet = instructionSet;
507 requirer = ps;
508 }
509 }
510
511 if (requiredInstructionSet == null) {
512 return null;
513 }
514 final String adjustedAbi;
515 if (requirer != null) {
516 // requirer != null implies that either scannedPackage was null or that
517 // scannedPackage did not require an ABI, in which case we have to adjust
518 // scannedPackage to match the ABI of the set (which is the same as
519 // requirer's ABI)
520 adjustedAbi = requirer.primaryCpuAbiString;
521 } else {
522 // requirer == null implies that we're updating all ABIs in the set to
523 // match scannedPackage.
524 adjustedAbi = scannedPackage.applicationInfo.primaryCpuAbi;
525 }
526 return adjustedAbi;
527 }
Patrick Baumannf62817e2019-05-31 10:46:53 -0700528}