blob: 74c98f9ef444345d27406d6bef93e391954baba0 [file] [log] [blame]
Dario Frenibe98c3f2018-12-22 15:25:27 +00001/*
2 * Copyright (C) 2018 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 android.annotation.NonNull;
Nikita Ioffeda998cf2019-03-04 22:54:30 +000020import android.annotation.Nullable;
Narayan Kamath9dfa6742019-01-04 14:22:50 +000021import android.apex.ApexInfo;
Dario Freni276cd072019-01-09 11:13:44 +000022import android.apex.ApexInfoList;
Dario Freni61b3e252019-01-11 23:05:39 +000023import android.apex.ApexSessionInfo;
Oli Lanc72b0bb2019-12-02 14:03:55 +000024import android.apex.ApexSessionParams;
Rhed Jaob793c212019-10-30 20:54:37 +080025import android.content.BroadcastReceiver;
Narayan Kamathfcd4a042019-02-01 14:16:37 +000026import android.content.Context;
Dario Freni4b572c02019-01-29 09:40:31 +000027import android.content.IIntentReceiver;
28import android.content.IIntentSender;
29import android.content.Intent;
Rhed Jaob793c212019-10-30 20:54:37 +080030import android.content.IntentFilter;
Dario Freni4b572c02019-01-29 09:40:31 +000031import android.content.IntentSender;
Dario Freni2e8dffc2019-02-06 14:55:16 +000032import android.content.pm.PackageInfo;
Dario Frenibe98c3f2018-12-22 15:25:27 +000033import android.content.pm.PackageInstaller;
Narayan Kamath9dfa6742019-01-04 14:22:50 +000034import android.content.pm.PackageInstaller.SessionInfo;
35import android.content.pm.PackageManager;
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +000036import android.content.pm.PackageManagerInternal;
Nikita Ioffee0dbc982019-05-09 19:33:35 +010037import android.content.pm.PackageParser;
Narayan Kamath9dfa6742019-01-04 14:22:50 +000038import android.content.pm.PackageParser.PackageParserException;
39import android.content.pm.PackageParser.SigningDetails;
40import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion;
Dario Frenibe98c3f2018-12-22 15:25:27 +000041import android.content.pm.ParceledListSlice;
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +000042import android.content.pm.parsing.AndroidPackage;
Narayan Kamathfcd4a042019-02-01 14:16:37 +000043import android.content.rollback.IRollbackManager;
Oli Lanc72b0bb2019-12-02 14:03:55 +000044import android.content.rollback.RollbackInfo;
45import android.content.rollback.RollbackManager;
Dario Freni4b572c02019-01-29 09:40:31 +000046import android.os.Bundle;
Narayan Kamath94c93ab2019-01-04 10:47:00 +000047import android.os.Handler;
Dario Freni4b572c02019-01-29 09:40:31 +000048import android.os.IBinder;
Mohammad Samiul Islamef400272019-08-15 16:44:57 +010049import android.os.Looper;
50import android.os.Message;
Dario Freni4b572c02019-01-29 09:40:31 +000051import android.os.ParcelFileDescriptor;
Mohammad Samiul Islam8ce32fe2020-01-14 18:33:39 +000052import android.os.ParcelableException;
Nikita Ioffe39a6a5b2019-02-26 15:36:39 +000053import android.os.PowerManager;
Narayan Kamath9dfa6742019-01-04 14:22:50 +000054import android.os.RemoteException;
55import android.os.ServiceManager;
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +000056import android.os.UserHandle;
57import android.os.UserManagerInternal;
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +000058import android.os.storage.IStorageManager;
Mohammad Samiul Islamcc4c7d82019-11-05 18:18:28 +000059import android.os.storage.StorageManager;
Nikita Ioffee0dbc982019-05-09 19:33:35 +010060import android.util.IntArray;
Narayan Kamath9dfa6742019-01-04 14:22:50 +000061import android.util.Slog;
Dario Frenibe98c3f2018-12-22 15:25:27 +000062import android.util.SparseArray;
Oli Lanc72b0bb2019-12-02 14:03:55 +000063import android.util.SparseIntArray;
Narayan Kamath9dfa6742019-01-04 14:22:50 +000064import android.util.apk.ApkSignatureVerifier;
Dario Frenibe98c3f2018-12-22 15:25:27 +000065
66import com.android.internal.annotations.GuardedBy;
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +000067import com.android.internal.content.PackageHelper;
Narayan Kamath94c93ab2019-01-04 10:47:00 +000068import com.android.internal.os.BackgroundThread;
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +000069import com.android.server.LocalServices;
Dario Frenibe98c3f2018-12-22 15:25:27 +000070
Dario Freni4b572c02019-01-29 09:40:31 +000071import java.io.File;
72import java.io.IOException;
Dario Frenibe98c3f2018-12-22 15:25:27 +000073import java.util.ArrayList;
Dario Freni015f9352019-01-14 21:56:17 +000074import java.util.Arrays;
Dario Frenibe98c3f2018-12-22 15:25:27 +000075import java.util.List;
Dario Freni4b572c02019-01-29 09:40:31 +000076import java.util.concurrent.LinkedBlockingQueue;
77import java.util.concurrent.TimeUnit;
Mohammad Samiul Islamef400272019-08-15 16:44:57 +010078import java.util.function.Consumer;
Dario Frenif141aab2019-02-04 14:48:30 +000079import java.util.function.Predicate;
Dario Freni015f9352019-01-14 21:56:17 +000080import java.util.stream.Collectors;
Dario Frenibe98c3f2018-12-22 15:25:27 +000081
82/**
83 * This class handles staged install sessions, i.e. install sessions that require packages to
84 * be installed only after a reboot.
85 */
86public class StagingManager {
87
88 private static final String TAG = "StagingManager";
89
Dario Freni4b572c02019-01-29 09:40:31 +000090 private final PackageInstallerService mPi;
Dario Freni2e8dffc2019-02-06 14:55:16 +000091 private final ApexManager mApexManager;
Nikita Ioffe39a6a5b2019-02-26 15:36:39 +000092 private final PowerManager mPowerManager;
Mohammad Samiul Islamcc4c7d82019-11-05 18:18:28 +000093 private final Context mContext;
Mohammad Samiul Islamef400272019-08-15 16:44:57 +010094 private final PreRebootVerificationHandler mPreRebootVerificationHandler;
Dario Frenibe98c3f2018-12-22 15:25:27 +000095
Dario Frenibe98c3f2018-12-22 15:25:27 +000096 @GuardedBy("mStagedSessions")
97 private final SparseArray<PackageInstallerSession> mStagedSessions = new SparseArray<>();
98
Oli Lanc72b0bb2019-12-02 14:03:55 +000099 @GuardedBy("mStagedSessions")
100 private final SparseIntArray mSessionRollbackIds = new SparseIntArray();
101
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +0000102 StagingManager(PackageInstallerService pi, Context context) {
Dario Freni4b572c02019-01-29 09:40:31 +0000103 mPi = pi;
Mohammad Samiul Islamcc4c7d82019-11-05 18:18:28 +0000104 mContext = context;
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +0000105
106 mApexManager = ApexManager.getInstance();
Nikita Ioffe39a6a5b2019-02-26 15:36:39 +0000107 mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
Mohammad Samiul Islamef400272019-08-15 16:44:57 +0100108 mPreRebootVerificationHandler = new PreRebootVerificationHandler(
109 BackgroundThread.get().getLooper());
Dario Frenibe98c3f2018-12-22 15:25:27 +0000110 }
111
112 private void updateStoredSession(@NonNull PackageInstallerSession sessionInfo) {
113 synchronized (mStagedSessions) {
114 PackageInstallerSession storedSession = mStagedSessions.get(sessionInfo.sessionId);
Narayan Kamath94c93ab2019-01-04 10:47:00 +0000115 // storedSession might be null if a call to abortSession was made before the session
116 // is updated.
117 if (storedSession != null) {
118 mStagedSessions.put(sessionInfo.sessionId, sessionInfo);
Dario Frenibe98c3f2018-12-22 15:25:27 +0000119 }
Dario Frenibe98c3f2018-12-22 15:25:27 +0000120 }
121 }
122
123 ParceledListSlice<PackageInstaller.SessionInfo> getSessions() {
124 final List<PackageInstaller.SessionInfo> result = new ArrayList<>();
125 synchronized (mStagedSessions) {
126 for (int i = 0; i < mStagedSessions.size(); i++) {
127 result.add(mStagedSessions.valueAt(i).generateInfo(false));
128 }
129 }
130 return new ParceledListSlice<>(result);
131 }
132
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100133 /**
134 * Validates the signature used to sign the container of the new apex package
135 *
136 * @param newApexPkg The new apex package that is being installed
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100137 * @throws PackageManagerException
138 */
Mohammad Samiul Islamd7472d02019-11-11 17:53:47 +0000139 private void validateApexSignature(PackageInfo newApexPkg)
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100140 throws PackageManagerException {
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100141 // Get signing details of the new package
142 final String apexPath = newApexPkg.applicationInfo.sourceDir;
143 final String packageName = newApexPkg.packageName;
144
Mohammad Samiul Islamd7472d02019-11-11 17:53:47 +0000145 final SigningDetails newSigningDetails;
Narayan Kamath9dfa6742019-01-04 14:22:50 +0000146 try {
Mohammad Samiul Islamd7472d02019-11-11 17:53:47 +0000147 newSigningDetails = ApkSignatureVerifier.verify(apexPath, SignatureSchemeVersion.JAR);
Narayan Kamath9dfa6742019-01-04 14:22:50 +0000148 } catch (PackageParserException e) {
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100149 throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
150 "Failed to parse APEX package " + apexPath, e);
Narayan Kamath9dfa6742019-01-04 14:22:50 +0000151 }
152
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100153 // Get signing details of the existing package
154 final PackageInfo existingApexPkg = mApexManager.getPackageInfo(packageName,
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100155 ApexManager.MATCH_ACTIVE_PACKAGE);
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100156 if (existingApexPkg == null) {
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100157 // This should never happen, because submitSessionToApexService ensures that no new
158 // apexes were installed.
159 throw new IllegalStateException("Unknown apex package " + packageName);
Narayan Kamath9dfa6742019-01-04 14:22:50 +0000160 }
161
162 final SigningDetails existingSigningDetails;
163 try {
164 existingSigningDetails = ApkSignatureVerifier.verify(
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100165 existingApexPkg.applicationInfo.sourceDir, SignatureSchemeVersion.JAR);
Narayan Kamath9dfa6742019-01-04 14:22:50 +0000166 } catch (PackageParserException e) {
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100167 throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100168 "Failed to parse APEX package " + existingApexPkg.applicationInfo.sourceDir, e);
Narayan Kamath9dfa6742019-01-04 14:22:50 +0000169 }
170
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100171 // Verify signing details for upgrade
Mohammad Samiul Islamd7472d02019-11-11 17:53:47 +0000172 if (newSigningDetails.checkCapability(existingSigningDetails,
173 SigningDetails.CertCapabilities.INSTALLED_DATA)
174 || existingSigningDetails.checkCapability(newSigningDetails,
175 SigningDetails.CertCapabilities.ROLLBACK)) {
Mohammad Samiul Islam8e35db12019-08-22 15:43:55 +0100176 return;
177 }
178
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100179 throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
Mohammad Samiul Islamb8841242019-07-29 14:38:38 +0100180 "APK-container signature of APEX package " + packageName + " with version "
181 + newApexPkg.versionCodeMajor + " and path " + apexPath + " is not"
182 + " compatible with the one currently installed on device");
Narayan Kamath9dfa6742019-01-04 14:22:50 +0000183 }
184
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100185 private List<PackageInfo> submitSessionToApexService(
186 @NonNull PackageInstallerSession session) throws PackageManagerException {
Oli Lanc72b0bb2019-12-02 14:03:55 +0000187 final IntArray childSessionIds = new IntArray();
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100188 if (session.isMultiPackage()) {
189 for (int id : session.getChildSessionIds()) {
190 if (isApexSession(mStagedSessions.get(id))) {
Oli Lanc72b0bb2019-12-02 14:03:55 +0000191 childSessionIds.add(id);
192 }
193 }
194 }
195 ApexSessionParams apexSessionParams = new ApexSessionParams();
196 apexSessionParams.sessionId = session.sessionId;
197 apexSessionParams.childSessionIds = childSessionIds.toArray();
198 if (session.params.installReason == PackageManager.INSTALL_REASON_ROLLBACK) {
199 apexSessionParams.isRollback = true;
200 apexSessionParams.rollbackId = retrieveRollbackIdForCommitSession(session.sessionId);
201 } else {
202 synchronized (mStagedSessions) {
203 int rollbackId = mSessionRollbackIds.get(session.sessionId, -1);
204 if (rollbackId != -1) {
205 apexSessionParams.hasRollbackEnabled = true;
206 apexSessionParams.rollbackId = rollbackId;
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100207 }
Nikita Ioffe8bbb8152019-02-21 15:54:54 +0000208 }
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100209 }
Nikita Ioffe4e7d24a2019-07-05 15:49:45 +0100210 // submitStagedSession will throw a PackageManagerException if apexd verification fails,
211 // which will be propagated to populate stagedSessionErrorMessage of this session.
Oli Lanc72b0bb2019-12-02 14:03:55 +0000212 final ApexInfoList apexInfoList = mApexManager.submitStagedSession(apexSessionParams);
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100213 final List<PackageInfo> result = new ArrayList<>();
Oli Lanc2c7a222019-07-31 15:27:22 +0100214 for (ApexInfo apexInfo : apexInfoList.apexInfos) {
215 final PackageInfo packageInfo;
216 int flags = PackageManager.GET_META_DATA;
217 PackageParser.Package pkg;
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100218 try {
Oli Lanc2c7a222019-07-31 15:27:22 +0100219 File apexFile = new File(apexInfo.modulePath);
220 PackageParser pp = new PackageParser();
221 pkg = pp.parsePackage(apexFile, flags, false);
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100222 } catch (PackageParserException e) {
223 throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
Oli Lanc2c7a222019-07-31 15:27:22 +0100224 "Failed to parse APEX package " + apexInfo.modulePath, e);
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100225 }
Oli Lanc2c7a222019-07-31 15:27:22 +0100226 packageInfo = PackageParser.generatePackageInfo(pkg, apexInfo, flags);
227 final PackageInfo activePackage = mApexManager.getPackageInfo(packageInfo.packageName,
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100228 ApexManager.MATCH_ACTIVE_PACKAGE);
229 if (activePackage == null) {
Oli Lanc2c7a222019-07-31 15:27:22 +0100230 Slog.w(TAG, "Attempting to install new APEX package " + packageInfo.packageName);
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100231 throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
232 "It is forbidden to install new APEX packages.");
233 }
234 checkRequiredVersionCode(session, activePackage);
Oli Lanc2c7a222019-07-31 15:27:22 +0100235 checkDowngrade(session, activePackage, packageInfo);
236 result.add(packageInfo);
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100237 }
Nikita Ioffef2c2e012019-07-23 17:48:38 +0100238 Slog.d(TAG, "Session " + session.sessionId + " has following APEX packages: ["
239 + result.stream().map(p -> p.packageName).collect(Collectors.joining(",")) + "]");
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100240 return result;
241 }
242
Oli Lanc72b0bb2019-12-02 14:03:55 +0000243 private int retrieveRollbackIdForCommitSession(int sessionId) throws PackageManagerException {
244 RollbackManager rm = mContext.getSystemService(RollbackManager.class);
245
246 List<RollbackInfo> rollbacks = rm.getRecentlyCommittedRollbacks();
247 for (RollbackInfo rollback : rollbacks) {
248 if (rollback.getCommittedSessionId() == sessionId) {
249 return rollback.getRollbackId();
250 }
251 }
252 throw new PackageManagerException(
253 "Could not find rollback id for commit session: " + sessionId);
254 }
255
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100256 private void checkRequiredVersionCode(final PackageInstallerSession session,
257 final PackageInfo activePackage) throws PackageManagerException {
258 if (session.params.requiredInstalledVersionCode == PackageManager.VERSION_CODE_HIGHEST) {
259 return;
260 }
261 final long activeVersion = activePackage.applicationInfo.longVersionCode;
262 if (activeVersion != session.params.requiredInstalledVersionCode) {
Mohammad Samiul Islam44ad95b2019-11-20 15:14:36 +0000263 if (!mApexManager.abortStagedSession(session.sessionId)) {
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100264 Slog.e(TAG, "Failed to abort apex session " + session.sessionId);
265 }
266 throw new PackageManagerException(
267 SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
268 "Installed version of APEX package " + activePackage.packageName
Richard Uhler2124d4b2019-04-25 13:01:39 +0100269 + " does not match required. Active version: " + activeVersion
270 + " required: " + session.params.requiredInstalledVersionCode);
Nikita Ioffe8bbb8152019-02-21 15:54:54 +0000271 }
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100272 }
273
274 private void checkDowngrade(final PackageInstallerSession session,
275 final PackageInfo activePackage, final PackageInfo newPackage)
276 throws PackageManagerException {
277 final long activeVersion = activePackage.applicationInfo.longVersionCode;
278 final long newVersionCode = newPackage.applicationInfo.longVersionCode;
Nikita Ioffef2c2e012019-07-23 17:48:38 +0100279 final boolean allowsDowngrade = PackageManagerServiceUtils.isDowngradePermitted(
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100280 session.params.installFlags, activePackage.applicationInfo.flags);
281 if (activeVersion > newVersionCode && !allowsDowngrade) {
Mohammad Samiul Islam44ad95b2019-11-20 15:14:36 +0000282 if (!mApexManager.abortStagedSession(session.sessionId)) {
Nikita Ioffee0dbc982019-05-09 19:33:35 +0100283 Slog.e(TAG, "Failed to abort apex session " + session.sessionId);
284 }
285 throw new PackageManagerException(
286 SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
287 "Downgrade of APEX package " + newPackage.packageName
288 + " is not allowed. Active version: " + activeVersion
289 + " attempted: " + newVersionCode);
290 }
Dario Freni015f9352019-01-14 21:56:17 +0000291 }
292
Dario Freni015f9352019-01-14 21:56:17 +0000293 private static boolean isApexSession(@NonNull PackageInstallerSession session) {
294 return (session.params.installFlags & PackageManager.INSTALL_APEX) != 0;
295 }
296
Dario Frenif141aab2019-02-04 14:48:30 +0000297 private boolean sessionContains(@NonNull PackageInstallerSession session,
298 Predicate<PackageInstallerSession> filter) {
Dario Freni4b572c02019-01-29 09:40:31 +0000299 if (!session.isMultiPackage()) {
Dario Frenif141aab2019-02-04 14:48:30 +0000300 return filter.test(session);
Dario Freni4b572c02019-01-29 09:40:31 +0000301 }
302 synchronized (mStagedSessions) {
303 return !(Arrays.stream(session.getChildSessionIds())
304 // Retrieve cached sessions matching ids.
305 .mapToObj(i -> mStagedSessions.get(i))
306 // Filter only the ones containing APEX.
Dario Frenif141aab2019-02-04 14:48:30 +0000307 .filter(childSession -> filter.test(childSession))
Dario Freni4b572c02019-01-29 09:40:31 +0000308 .collect(Collectors.toList())
309 .isEmpty());
310 }
311 }
312
Dario Frenif141aab2019-02-04 14:48:30 +0000313 private boolean sessionContainsApex(@NonNull PackageInstallerSession session) {
314 return sessionContains(session, (s) -> isApexSession(s));
315 }
316
317 private boolean sessionContainsApk(@NonNull PackageInstallerSession session) {
318 return sessionContains(session, (s) -> !isApexSession(s));
319 }
320
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000321 // Reverts apex sessions and user data (if checkpoint is supported). Also reboots the device.
322 private void abortCheckpoint() {
323 try {
324 if (supportsCheckpoint() && needsCheckpoint()) {
325 mApexManager.revertActiveSessions();
326 PackageHelper.getStorageManager().abortChanges(
327 "StagingManager initiated", false /*retry*/);
328 }
329 } catch (Exception e) {
330 Slog.wtf(TAG, "Failed to abort checkpoint", e);
331 mApexManager.revertActiveSessions();
332 mPowerManager.reboot(null);
333 }
334 }
335
336 private boolean supportsCheckpoint() throws RemoteException {
337 return PackageHelper.getStorageManager().supportsCheckpoint();
338 }
339
340 private boolean needsCheckpoint() throws RemoteException {
341 return PackageHelper.getStorageManager().needsCheckpoint();
342 }
343
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +0000344 /**
345 * Apks inside apex are not installed using apk-install flow. They are scanned from the system
346 * directory directly by PackageManager, as such, RollbackManager need to handle their data
347 * separately here.
348 */
349 private void snapshotAndRestoreApkInApexUserData(PackageInstallerSession session) {
350 // We want to process apks inside apex. So current session needs to contain apex.
351 if (!sessionContainsApex(session)) {
352 return;
353 }
354
355 boolean doSnapshotOrRestore =
356 (session.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0
357 || session.params.installReason == PackageManager.INSTALL_REASON_ROLLBACK;
358 if (!doSnapshotOrRestore) {
359 return;
360 }
361
362 // Find all the apex sessions that needs processing
363 List<PackageInstallerSession> apexSessions = new ArrayList<>();
364 if (session.isMultiPackage()) {
365 List<PackageInstallerSession> childrenSessions = new ArrayList<>();
366 synchronized (mStagedSessions) {
367 for (int childSessionId : session.getChildSessionIds()) {
368 PackageInstallerSession childSession = mStagedSessions.get(childSessionId);
369 if (childSession != null) {
370 childrenSessions.add(childSession);
371 }
372 }
373 }
374 for (PackageInstallerSession childSession : childrenSessions) {
375 if (sessionContainsApex(childSession)) {
376 apexSessions.add(childSession);
377 }
378 }
379 } else {
380 apexSessions.add(session);
381 }
382
383 // For each apex, process the apks inside it
384 for (PackageInstallerSession apexSession : apexSessions) {
385 List<String> apksInApex = mApexManager.getApksInApex(apexSession.getPackageName());
386 for (String apk: apksInApex) {
387 snapshotAndRestoreApkInApexUserData(apk);
388 }
389 }
390 }
391
392 private void snapshotAndRestoreApkInApexUserData(String packageName) {
393 IRollbackManager rm = IRollbackManager.Stub.asInterface(
394 ServiceManager.getService(Context.ROLLBACK_SERVICE));
395
396 PackageManagerInternal mPmi = LocalServices.getService(PackageManagerInternal.class);
397 AndroidPackage pkg = mPmi.getPackage(packageName);
398 if (pkg == null) {
399 Slog.e(TAG, "Could not find package: " + packageName
400 + "for snapshotting/restoring user data.");
401 return;
402 }
403 final String seInfo = pkg.getSeInfo();
404 final UserManagerInternal um = LocalServices.getService(UserManagerInternal.class);
405 final int[] allUsers = um.getUserIds();
406
407 int appId = -1;
408 long ceDataInode = -1;
409 final PackageSetting ps = (PackageSetting) mPmi.getPackageSetting(packageName);
410 if (ps != null && rm != null) {
411 appId = ps.appId;
412 ceDataInode = ps.getCeDataInode(UserHandle.USER_SYSTEM);
413 // NOTE: We ignore the user specified in the InstallParam because we know this is
414 // an update, and hence need to restore data for all installed users.
415 final int[] installedUsers = ps.queryInstalledUsers(allUsers, true);
416
417 try {
418 rm.snapshotAndRestoreUserData(packageName, installedUsers, appId, ceDataInode,
419 seInfo, 0 /*token*/);
420 } catch (RemoteException re) {
421 Slog.e(TAG, "Error snapshotting/restoring user data: " + re);
422 }
423 }
424 }
425
Dario Freni61b3e252019-01-11 23:05:39 +0000426 private void resumeSession(@NonNull PackageInstallerSession session) {
Nikita Ioffef2c2e012019-07-23 17:48:38 +0100427 Slog.d(TAG, "Resuming session " + session.sessionId);
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000428
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100429 final boolean hasApex = sessionContainsApex(session);
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000430 ApexSessionInfo apexSessionInfo = null;
Nikita Ioffea820bd92019-02-15 14:22:44 +0000431 if (hasApex) {
Dario Freni2e8dffc2019-02-06 14:55:16 +0000432 // Check with apexservice whether the apex packages have been activated.
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000433 apexSessionInfo = mApexManager.getStagedSessionInfo(session.sessionId);
434
435 if (apexSessionInfo != null && apexSessionInfo.isVerified) {
436 // Session has been previously submitted to apexd, but didn't complete all the
437 // pre-reboot verification, perhaps because the device rebooted in the meantime.
438 // Greedily re-trigger the pre-reboot verification. We want to avoid marking it as
439 // failed when not in checkpoint mode, hence it is being processed separately.
440 Slog.d(TAG, "Found pending staged session " + session.sessionId + " still to "
441 + "be verified, resuming pre-reboot verification");
442 mPreRebootVerificationHandler.startPreRebootVerification(session.sessionId);
443 return;
444 }
445 }
446
447 // Before we resume session, we check if revert is needed or not. Typically, we enter file-
448 // system checkpoint mode when we reboot first time in order to install staged sessions. We
449 // want to install staged sessions in this mode as rebooting now will revert user data. If
450 // something goes wrong, then we reboot again to enter fs-rollback mode. Rebooting now will
451 // have no effect on user data, so mark the sessions as failed instead.
452 try {
453 // If checkpoint is supported, then we only resume sessions if we are in checkpointing
454 // mode. If not, we fail all sessions.
455 if (supportsCheckpoint() && !needsCheckpoint()) {
456 // TODO(b/146343545): Persist failure reason across checkpoint reboot
457 session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_UNKNOWN,
458 "Reverting back to safe state");
459 return;
460 }
461 } catch (RemoteException e) {
462 // Cannot continue staged install without knowing if fs-checkpoint is supported
463 Slog.e(TAG, "Checkpoint support unknown. Aborting staged install for session "
464 + session.sessionId, e);
465 // TODO: Mark all staged sessions together and reboot only once
466 session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_UNKNOWN,
467 "Checkpoint support unknown. Aborting staged install.");
468 if (hasApex) {
469 mApexManager.revertActiveSessions();
470 }
471 mPowerManager.reboot("Checkpoint support unknown");
472 return;
473 }
474
475 if (hasApex) {
Dario Freni2e8dffc2019-02-06 14:55:16 +0000476 if (apexSessionInfo == null) {
477 session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
478 "apexd did not know anything about a staged session supposed to be"
479 + "activated");
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000480 abortCheckpoint();
Dario Freni4b572c02019-01-29 09:40:31 +0000481 return;
482 }
Nikita Ioffe82742222019-02-26 12:14:04 +0000483 if (isApexSessionFailed(apexSessionInfo)) {
Dario Frenib6d28962019-01-31 15:52:24 +0000484 session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
Dario Freni4b572c02019-01-29 09:40:31 +0000485 "APEX activation failed. Check logcat messages from apexd for "
486 + "more information.");
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000487 abortCheckpoint();
Dario Freni4b572c02019-01-29 09:40:31 +0000488 return;
489 }
Nikita Ioffea820bd92019-02-15 14:22:44 +0000490 if (!apexSessionInfo.isActivated && !apexSessionInfo.isSuccess) {
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000491 // Apexd did not apply the session for some unknown reason. There is no guarantee
492 // that apexd will install it next time. Safer to proactively mark as failed.
493 session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
494 "Staged session " + session.sessionId + "at boot didn't "
495 + "activate nor fail. Marking it as failed anyway.");
496 abortCheckpoint();
Dario Freni4b572c02019-01-29 09:40:31 +0000497 return;
498 }
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +0000499 snapshotAndRestoreApkInApexUserData(session);
Nikita Ioffef2c2e012019-07-23 17:48:38 +0100500 Slog.i(TAG, "APEX packages in session " + session.sessionId
501 + " were successfully activated. Proceeding with APK packages, if any");
Dario Freni4b572c02019-01-29 09:40:31 +0000502 }
503 // The APEX part of the session is activated, proceed with the installation of APKs.
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100504 try {
Nikita Ioffef2c2e012019-07-23 17:48:38 +0100505 Slog.d(TAG, "Installing APK packages in session " + session.sessionId);
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100506 installApksInSession(session);
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100507 } catch (PackageManagerException e) {
508 session.setStagedSessionFailed(e.error, e.getMessage());
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000509 abortCheckpoint();
Nikita Ioffe39a6a5b2019-02-26 15:36:39 +0000510
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +0000511 // If checkpoint is not supported, we have to handle failure for one staged session.
Nikita Ioffe39a6a5b2019-02-26 15:36:39 +0000512 if (!hasApex) {
513 return;
514 }
515
Mohammad Samiul Islam1a96eb62019-11-21 10:38:06 +0000516 if (!mApexManager.revertActiveSessions()) {
Nikita Ioffe39a6a5b2019-02-26 15:36:39 +0000517 Slog.e(TAG, "Failed to abort APEXd session");
518 } else {
519 Slog.e(TAG,
520 "Successfully aborted apexd session. Rebooting device in order to revert "
521 + "to the previous state of APEXd.");
522 mPowerManager.reboot(null);
523 }
Dario Freni61b3e252019-01-11 23:05:39 +0000524 return;
525 }
Nikita Ioffea820bd92019-02-15 14:22:44 +0000526
Nikita Ioffef2c2e012019-07-23 17:48:38 +0100527 Slog.d(TAG, "Marking session " + session.sessionId + " as applied");
Dario Freni4b572c02019-01-29 09:40:31 +0000528 session.setStagedSessionApplied();
Nikita Ioffea820bd92019-02-15 14:22:44 +0000529 if (hasApex) {
530 mApexManager.markStagedSessionSuccessful(session.sessionId);
531 }
Dario Freni4b572c02019-01-29 09:40:31 +0000532 }
533
Dario Freni815bd212019-02-20 14:09:36 +0000534 private List<String> findAPKsInDir(File stageDir) {
535 List<String> ret = new ArrayList<>();
Dario Freni4b572c02019-01-29 09:40:31 +0000536 if (stageDir != null && stageDir.exists()) {
537 for (File file : stageDir.listFiles()) {
538 if (file.getAbsolutePath().toLowerCase().endsWith(".apk")) {
Dario Freni815bd212019-02-20 14:09:36 +0000539 ret.add(file.getAbsolutePath());
Dario Freni4b572c02019-01-29 09:40:31 +0000540 }
541 }
Dario Freni61b3e252019-01-11 23:05:39 +0000542 }
Dario Freni815bd212019-02-20 14:09:36 +0000543 return ret;
Dario Freni4b572c02019-01-29 09:40:31 +0000544 }
545
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100546 @NonNull
Dario Freni4b572c02019-01-29 09:40:31 +0000547 private PackageInstallerSession createAndWriteApkSession(
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100548 @NonNull PackageInstallerSession originalSession, boolean preReboot)
549 throws PackageManagerException {
550 final int errorCode = preReboot ? SessionInfo.STAGED_SESSION_VERIFICATION_FAILED
551 : SessionInfo.STAGED_SESSION_ACTIVATION_FAILED;
Dario Freni4b572c02019-01-29 09:40:31 +0000552 if (originalSession.stageDir == null) {
553 Slog.wtf(TAG, "Attempting to install a staged APK session with no staging dir");
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100554 throw new PackageManagerException(errorCode,
555 "Attempting to install a staged APK session with no staging dir");
Dario Freni1473bcb2019-01-25 14:27:13 +0000556 }
Dario Freni815bd212019-02-20 14:09:36 +0000557 List<String> apkFilePaths = findAPKsInDir(originalSession.stageDir);
558 if (apkFilePaths.isEmpty()) {
Dario Freni4b572c02019-01-29 09:40:31 +0000559 Slog.w(TAG, "Can't find staged APK in " + originalSession.stageDir.getAbsolutePath());
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100560 throw new PackageManagerException(errorCode,
561 "Can't find staged APK in " + originalSession.stageDir.getAbsolutePath());
Dario Freni61b3e252019-01-11 23:05:39 +0000562 }
Dario Freni4b572c02019-01-29 09:40:31 +0000563
564 PackageInstaller.SessionParams params = originalSession.params.copy();
565 params.isStaged = false;
Dario Frenia2afebf2019-05-01 16:03:17 +0100566 params.installFlags |= PackageManager.INSTALL_STAGED;
Dario Frenic3e68ea2019-04-02 11:45:13 +0100567 // TODO(b/129744602): use the userid from the original session.
Dario Frenif141aab2019-02-04 14:48:30 +0000568 if (preReboot) {
569 params.installFlags &= ~PackageManager.INSTALL_ENABLE_ROLLBACK;
570 params.installFlags |= PackageManager.INSTALL_DRY_RUN;
571 } else {
572 params.installFlags |= PackageManager.INSTALL_DISABLE_VERIFICATION;
573 }
Dario Freni4b572c02019-01-29 09:40:31 +0000574 try {
Mohammad Samiul Islam8ce32fe2020-01-14 18:33:39 +0000575 int apkSessionId = mPi.createSession(
576 params, originalSession.getInstallerPackageName(),
577 0 /* UserHandle.SYSTEM */);
578 PackageInstallerSession apkSession = mPi.getSession(apkSessionId);
Dario Freni4b572c02019-01-29 09:40:31 +0000579 apkSession.open();
Dario Freni815bd212019-02-20 14:09:36 +0000580 for (String apkFilePath : apkFilePaths) {
581 File apkFile = new File(apkFilePath);
582 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(apkFile,
583 ParcelFileDescriptor.MODE_READ_ONLY);
Mohammad Samiul Islam8ce32fe2020-01-14 18:33:39 +0000584 long sizeBytes = (pfd == null) ? -1 : pfd.getStatSize();
Dario Freni815bd212019-02-20 14:09:36 +0000585 if (sizeBytes < 0) {
586 Slog.e(TAG, "Unable to get size of: " + apkFilePath);
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100587 throw new PackageManagerException(errorCode,
588 "Unable to get size of: " + apkFilePath);
Dario Freni815bd212019-02-20 14:09:36 +0000589 }
590 apkSession.write(apkFile.getName(), 0, sizeBytes, pfd);
Dario Freni4b572c02019-01-29 09:40:31 +0000591 }
Mohammad Samiul Islam8ce32fe2020-01-14 18:33:39 +0000592 return apkSession;
593 } catch (IOException | ParcelableException e) {
Dario Freni4b572c02019-01-29 09:40:31 +0000594 Slog.e(TAG, "Failure to install APK staged session " + originalSession.sessionId, e);
Mohammad Samiul Islam8ce32fe2020-01-14 18:33:39 +0000595 throw new PackageManagerException(errorCode, "Failed to create/write APK session", e);
Dario Freni4b572c02019-01-29 09:40:31 +0000596 }
Dario Freni4b572c02019-01-29 09:40:31 +0000597 }
598
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100599 /**
600 * Extract apks in the given session into a new session. Returns {@code null} if there is no
601 * apks in the given session. Only parent session is returned for multi-package session.
602 */
603 @Nullable
604 private PackageInstallerSession extractApksInSession(PackageInstallerSession session,
605 boolean preReboot) throws PackageManagerException {
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100606 final int errorCode = preReboot ? SessionInfo.STAGED_SESSION_VERIFICATION_FAILED
607 : SessionInfo.STAGED_SESSION_ACTIVATION_FAILED;
Dario Freni4b572c02019-01-29 09:40:31 +0000608 if (!session.isMultiPackage() && !isApexSession(session)) {
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100609 return createAndWriteApkSession(session, preReboot);
Dario Freni4b572c02019-01-29 09:40:31 +0000610 } else if (session.isMultiPackage()) {
611 // For multi-package staged sessions containing APKs, we identify which child sessions
612 // contain an APK, and with those then create a new multi-package group of sessions,
613 // carrying over all the session parameters and unmarking them as staged. On commit the
614 // sessions will be installed atomically.
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100615 final List<PackageInstallerSession> childSessions;
Dario Freni4b572c02019-01-29 09:40:31 +0000616 synchronized (mStagedSessions) {
617 childSessions =
618 Arrays.stream(session.getChildSessionIds())
619 // Retrieve cached sessions matching ids.
620 .mapToObj(i -> mStagedSessions.get(i))
Mohammad Samiul Islam3fcecfc2019-12-20 17:46:01 +0000621 // Filter only the ones containing APKs.
Dario Freni4b572c02019-01-29 09:40:31 +0000622 .filter(childSession -> !isApexSession(childSession))
623 .collect(Collectors.toList());
624 }
625 if (childSessions.isEmpty()) {
626 // APEX-only multi-package staged session, nothing to do.
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100627 return null;
Dario Freni4b572c02019-01-29 09:40:31 +0000628 }
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100629 final PackageInstaller.SessionParams params = session.params.copy();
Dario Freni4b572c02019-01-29 09:40:31 +0000630 params.isStaged = false;
Dario Frenif141aab2019-02-04 14:48:30 +0000631 if (preReboot) {
632 params.installFlags &= ~PackageManager.INSTALL_ENABLE_ROLLBACK;
633 }
Dario Frenic3e68ea2019-04-02 11:45:13 +0100634 // TODO(b/129744602): use the userid from the original session.
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100635 final int apkParentSessionId = mPi.createSession(
Dario Frenic3e68ea2019-04-02 11:45:13 +0100636 params, session.getInstallerPackageName(),
637 0 /* UserHandle.SYSTEM */);
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100638 final PackageInstallerSession apkParentSession = mPi.getSession(apkParentSessionId);
Dario Freni4b572c02019-01-29 09:40:31 +0000639 try {
640 apkParentSession.open();
641 } catch (IOException e) {
642 Slog.e(TAG, "Unable to prepare multi-package session for staged session "
643 + session.sessionId);
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100644 throw new PackageManagerException(errorCode,
645 "Unable to prepare multi-package session for staged session");
Dario Freni4b572c02019-01-29 09:40:31 +0000646 }
647
648 for (PackageInstallerSession sessionToClone : childSessions) {
Dario Frenif141aab2019-02-04 14:48:30 +0000649 PackageInstallerSession apkChildSession =
650 createAndWriteApkSession(sessionToClone, preReboot);
Dario Freni09660472019-02-18 16:44:23 +0000651 try {
652 apkParentSession.addChildSessionId(apkChildSession.sessionId);
Patrick Baumann00321b72019-04-09 15:07:25 -0700653 } catch (IllegalStateException e) {
Dario Freni09660472019-02-18 16:44:23 +0000654 Slog.e(TAG, "Failed to add a child session for installing the APK files", e);
Nikita Ioffec2b87a92019-07-15 13:28:11 +0100655 throw new PackageManagerException(errorCode,
656 "Failed to add a child session " + apkChildSession.sessionId);
Dario Freni09660472019-02-18 16:44:23 +0000657 }
Dario Freni4b572c02019-01-29 09:40:31 +0000658 }
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100659 return apkParentSession;
Dario Freni4b572c02019-01-29 09:40:31 +0000660 }
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +0100661 return null;
662 }
663
664 private void verifyApksInSession(PackageInstallerSession session)
665 throws PackageManagerException {
666
667 final PackageInstallerSession apksToVerify = extractApksInSession(
668 session, /* preReboot */ true);
669 if (apksToVerify == null) {
670 return;
671 }
672
673 final LocalIntentReceiverAsync receiver = new LocalIntentReceiverAsync(
674 (Intent result) -> {
675 int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
676 PackageInstaller.STATUS_FAILURE);
677 if (status != PackageInstaller.STATUS_SUCCESS) {
678 final String errorMessage = result.getStringExtra(
679 PackageInstaller.EXTRA_STATUS_MESSAGE);
680 Slog.e(TAG, "Failure to verify APK staged session "
681 + session.sessionId + " [" + errorMessage + "]");
682 session.setStagedSessionFailed(
683 SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, errorMessage);
684 return;
685 }
686 mPreRebootVerificationHandler.notifyPreRebootVerification_Apk_Complete(
687 session.sessionId);
688 });
689
690 apksToVerify.commit(receiver.getIntentSender(), false);
691 }
692
693 private void installApksInSession(@NonNull PackageInstallerSession session)
694 throws PackageManagerException {
695
696 final PackageInstallerSession apksToInstall = extractApksInSession(
697 session, /* preReboot */ false);
698 if (apksToInstall == null) {
699 return;
700 }
701
702 if ((apksToInstall.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
703 // If rollback is available for this session, notify the rollback
704 // manager of the apk session so it can properly enable rollback.
705 final IRollbackManager rm = IRollbackManager.Stub.asInterface(
706 ServiceManager.getService(Context.ROLLBACK_SERVICE));
707 try {
708 rm.notifyStagedApkSession(session.sessionId, apksToInstall.sessionId);
709 } catch (RemoteException re) {
710 Slog.e(TAG, "Failed to notifyStagedApkSession for session: "
711 + session.sessionId, re);
712 }
713 }
714
715 final LocalIntentReceiverSync receiver = new LocalIntentReceiverSync();
716 apksToInstall.commit(receiver.getIntentSender(), false);
717 final Intent result = receiver.getResult();
718 final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
719 PackageInstaller.STATUS_FAILURE);
720 if (status != PackageInstaller.STATUS_SUCCESS) {
721 final String errorMessage = result.getStringExtra(
722 PackageInstaller.EXTRA_STATUS_MESSAGE);
723 Slog.e(TAG, "Failure to install APK staged session "
724 + session.sessionId + " [" + errorMessage + "]");
725 throw new PackageManagerException(
726 SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, errorMessage);
727 }
Dario Freni61b3e252019-01-11 23:05:39 +0000728 }
729
Nikita Ioffe8e5b703c2019-03-14 18:40:17 +0000730 void commitSession(@NonNull PackageInstallerSession session) {
Dario Freni276cd072019-01-09 11:13:44 +0000731 updateStoredSession(session);
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +0100732 mPreRebootVerificationHandler.startPreRebootVerification(session.sessionId);
Dario Frenibe98c3f2018-12-22 15:25:27 +0000733 }
734
Mohammad Samiul Islamcc4c7d82019-11-05 18:18:28 +0000735 private int parentOrOwnSessionId(PackageInstallerSession session) {
736 return session.hasParentSessionId() ? session.getParentSessionId() : session.sessionId;
737 }
738
Mohammad Samiul Islamda004972019-10-09 11:29:26 +0100739 /**
740 * <p> Check if the session provided is non-overlapping with the active staged sessions.
741 *
742 * <p> A session is non-overlapping if it meets one of the following conditions: </p>
743 * <ul>
744 * <li>It is a parent session</li>
745 * <li>It is already one of the active sessions</li>
746 * <li>Its package name is not same as any of the active sessions</li>
747 * </ul>
748 * @throws PackageManagerException if session fails the check
749 */
750 void checkNonOverlappingWithStagedSessions(@NonNull PackageInstallerSession session)
751 throws PackageManagerException {
752 if (session.isMultiPackage()) {
753 // We cannot say a parent session overlaps until we process its children
754 return;
755 }
756 if (session.getPackageName() == null) {
757 throw new PackageManagerException(PackageManager.INSTALL_FAILED_INVALID_APK,
758 "Cannot stage session " + session.sessionId + " with package name null");
759 }
760
Mohammad Samiul Islamcc4c7d82019-11-05 18:18:28 +0000761 boolean supportsCheckpoint = ((StorageManager) mContext.getSystemService(
762 Context.STORAGE_SERVICE)).isCheckpointSupported();
763
Nikita Ioffeda998cf2019-03-04 22:54:30 +0000764 synchronized (mStagedSessions) {
765 for (int i = 0; i < mStagedSessions.size(); i++) {
Mohammad Samiul Islamda004972019-10-09 11:29:26 +0100766 final PackageInstallerSession stagedSession = mStagedSessions.valueAt(i);
767 if (!stagedSession.isCommitted() || stagedSession.isStagedAndInTerminalState()) {
Nikita Ioffeda998cf2019-03-04 22:54:30 +0000768 continue;
769 }
Mohammad Samiul Islamda004972019-10-09 11:29:26 +0100770 if (stagedSession.isMultiPackage()) {
771 // This active parent staged session is useless as it doesn't have a package
772 // name and the session we are checking is not a parent session either.
Nikita Ioffeda998cf2019-03-04 22:54:30 +0000773 continue;
774 }
Mohammad Samiul Islamda004972019-10-09 11:29:26 +0100775
776 // From here on, stagedSession is a non-parent active staged session
777
778 // Check if stagedSession has an active parent session or not
779 if (stagedSession.hasParentSessionId()) {
780 int parentId = stagedSession.getParentSessionId();
781 PackageInstallerSession parentSession = mStagedSessions.get(parentId);
782 if (parentSession == null || parentSession.isStagedAndInTerminalState()) {
783 // Parent session has been abandoned or terminated already
784 continue;
785 }
786 }
787
788 // Check if session is one of the active sessions
789 if (session.sessionId == stagedSession.sessionId) {
790 Slog.w(TAG, "Session " + session.sessionId + " is already staged");
791 continue;
792 }
793
794 // If session is not among the active sessions, then it cannot have same package
795 // name as any of the active sessions.
796 if (session.getPackageName().equals(stagedSession.getPackageName())) {
797 throw new PackageManagerException(
798 PackageManager.INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS,
799 "Package: " + session.getPackageName() + " in session: "
800 + session.sessionId + " has been staged already by session: "
801 + stagedSession.sessionId, null);
802 }
803
Mohammad Samiul Islamcc4c7d82019-11-05 18:18:28 +0000804 // Staging multiple root sessions is not allowed if device doesn't support
805 // checkpoint. If session and stagedSession do not have common ancestor, they are
806 // from two different root sessions.
807 if (!supportsCheckpoint
808 && parentOrOwnSessionId(session) != parentOrOwnSessionId(stagedSession)) {
809 throw new PackageManagerException(
810 PackageManager.INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS,
811 "Cannot stage multiple sessions without checkpoint support", null);
812 }
Nikita Ioffeda998cf2019-03-04 22:54:30 +0000813 }
814 }
Nikita Ioffeda998cf2019-03-04 22:54:30 +0000815 }
816
Dario Frenibe98c3f2018-12-22 15:25:27 +0000817 void createSession(@NonNull PackageInstallerSession sessionInfo) {
818 synchronized (mStagedSessions) {
819 mStagedSessions.append(sessionInfo.sessionId, sessionInfo);
820 }
821 }
822
Dario Freni015f9352019-01-14 21:56:17 +0000823 void abortSession(@NonNull PackageInstallerSession session) {
Dario Frenibe98c3f2018-12-22 15:25:27 +0000824 synchronized (mStagedSessions) {
Dario Freni015f9352019-01-14 21:56:17 +0000825 mStagedSessions.remove(session.sessionId);
Oli Lanc72b0bb2019-12-02 14:03:55 +0000826 mSessionRollbackIds.delete(session.sessionId);
Dario Frenibe98c3f2018-12-22 15:25:27 +0000827 }
828 }
Dario Freni8e7d0ec2019-01-10 15:21:40 +0000829
shafik07205e32019-02-07 20:12:33 +0000830 void abortCommittedSession(@NonNull PackageInstallerSession session) {
Nikita Ioffe1e523b52019-03-13 16:07:48 +0000831 if (session.isStagedSessionApplied()) {
832 Slog.w(TAG, "Cannot abort applied session : " + session.sessionId);
shafik07205e32019-02-07 20:12:33 +0000833 return;
834 }
shafik07205e32019-02-07 20:12:33 +0000835 abortSession(session);
Nikita Ioffee2d52f52019-03-11 14:29:23 +0000836
837 boolean hasApex = sessionContainsApex(session);
838 if (hasApex) {
839 ApexSessionInfo apexSession = mApexManager.getStagedSessionInfo(session.sessionId);
840 if (apexSession == null || isApexSessionFinalized(apexSession)) {
841 Slog.w(TAG,
Mohammad Samiul Islamda004972019-10-09 11:29:26 +0100842 "Cannot abort session " + session.sessionId
843 + " because it is not active or APEXD is not reachable");
Nikita Ioffee2d52f52019-03-11 14:29:23 +0000844 return;
845 }
Mohammad Samiul Islam44ad95b2019-11-20 15:14:36 +0000846 try {
847 mApexManager.abortStagedSession(session.sessionId);
848 } catch (Exception ignore) {
849 }
Nikita Ioffee2d52f52019-03-11 14:29:23 +0000850 }
shafik07205e32019-02-07 20:12:33 +0000851 }
852
Nikita Ioffee2d52f52019-03-11 14:29:23 +0000853 private boolean isApexSessionFinalized(ApexSessionInfo session) {
shafik07205e32019-02-07 20:12:33 +0000854 /* checking if the session is in a final state, i.e., not active anymore */
Nikita Ioffe82742222019-02-26 12:14:04 +0000855 return session.isUnknown || session.isActivationFailed || session.isSuccess
Mohammad Samiul Islam2dfc86a2019-11-20 13:53:07 +0000856 || session.isReverted;
Nikita Ioffe82742222019-02-26 12:14:04 +0000857 }
858
859 private static boolean isApexSessionFailed(ApexSessionInfo apexSessionInfo) {
Mohammad Samiul Islam2dfc86a2019-11-20 13:53:07 +0000860 // isRevertInProgress is included to cover the scenario, when a device is rebooted
861 // during the revert, and apexd fails to resume the revert after reboot.
Nikita Ioffe82742222019-02-26 12:14:04 +0000862 return apexSessionInfo.isActivationFailed || apexSessionInfo.isUnknown
Mohammad Samiul Islam2dfc86a2019-11-20 13:53:07 +0000863 || apexSessionInfo.isReverted || apexSessionInfo.isRevertInProgress
864 || apexSessionInfo.isRevertFailed;
shafik07205e32019-02-07 20:12:33 +0000865 }
866
Dario Freni015f9352019-01-14 21:56:17 +0000867 @GuardedBy("mStagedSessions")
868 private boolean isMultiPackageSessionComplete(@NonNull PackageInstallerSession session) {
869 // This method assumes that the argument is either a parent session of a multi-package
870 // i.e. isMultiPackage() returns true, or that it is a child session, i.e.
871 // hasParentSessionId() returns true.
872 if (session.isMultiPackage()) {
873 // Parent session of a multi-package group. Check that we restored all the children.
874 for (int childSession : session.getChildSessionIds()) {
875 if (mStagedSessions.get(childSession) == null) {
876 return false;
877 }
878 }
879 return true;
880 }
881 if (session.hasParentSessionId()) {
882 PackageInstallerSession parent = mStagedSessions.get(session.getParentSessionId());
883 if (parent == null) {
884 return false;
885 }
886 return isMultiPackageSessionComplete(parent);
887 }
888 Slog.wtf(TAG, "Attempting to restore an invalid multi-package session.");
889 return false;
890 }
891
Rhed Jao1fc8b362020-01-16 18:38:17 +0800892 void restoreSession(@NonNull PackageInstallerSession session, boolean isDeviceUpgrading) {
Dario Freni015f9352019-01-14 21:56:17 +0000893 PackageInstallerSession sessionToResume = session;
894 synchronized (mStagedSessions) {
895 mStagedSessions.append(session.sessionId, session);
896 // For multi-package sessions, we don't know in which order they will be restored. We
897 // need to wait until we have restored all the session in a group before restoring them.
898 if (session.isMultiPackage() || session.hasParentSessionId()) {
899 if (!isMultiPackageSessionComplete(session)) {
900 // Still haven't recovered all sessions of the group, return.
901 return;
902 }
903 // Group recovered, find the parent if necessary and resume the installation.
904 if (session.hasParentSessionId()) {
905 sessionToResume = mStagedSessions.get(session.getParentSessionId());
906 }
907 }
908 }
Rhed Jao1fc8b362020-01-16 18:38:17 +0800909 // The preconditions used during pre-reboot verification might have changed when device
910 // is upgrading. Updated staged sessions to activation failed before we resume the session.
911 if (isDeviceUpgrading && !sessionToResume.isStagedAndInTerminalState()) {
912 sessionToResume.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
913 "Build fingerprint has changed");
914 return;
915 }
Dario Freni015f9352019-01-14 21:56:17 +0000916 checkStateAndResume(sessionToResume);
917 }
918
919 private void checkStateAndResume(@NonNull PackageInstallerSession session) {
Dario Freni47799f42019-03-13 18:06:24 +0000920 if (!session.isCommitted()) {
921 // Session hasn't been committed yet, ignore.
922 return;
923 }
Dario Freni61b3e252019-01-11 23:05:39 +0000924 // Check the state of the session and decide what to do next.
925 if (session.isStagedSessionFailed() || session.isStagedSessionApplied()) {
926 // Final states, nothing to do.
927 return;
928 }
929 if (!session.isStagedSessionReady()) {
930 // The framework got restarted before the pre-reboot verification could complete,
931 // restart the verification.
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +0100932 mPreRebootVerificationHandler.startPreRebootVerification(session.sessionId);
Dario Freni61b3e252019-01-11 23:05:39 +0000933 } else {
934 // Session had already being marked ready. Start the checks to verify if there is any
935 // follow-up work.
Dario Freni4df010e2019-01-31 19:34:46 +0000936 resumeSession(session);
Dario Freni61b3e252019-01-11 23:05:39 +0000937 }
Dario Freni8e7d0ec2019-01-10 15:21:40 +0000938 }
Dario Freni4b572c02019-01-29 09:40:31 +0000939
Rhed Jaob793c212019-10-30 20:54:37 +0800940 void systemReady() {
941 // Register the receiver of boot completed intent for staging manager.
942 mContext.registerReceiver(new BroadcastReceiver() {
943 @Override
944 public void onReceive(Context ctx, Intent intent) {
945 mPreRebootVerificationHandler.readyToStart();
946 ctx.unregisterReceiver(this);
947 }
948 }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED));
949 }
950
Mohammad Samiul Islamef400272019-08-15 16:44:57 +0100951 private static class LocalIntentReceiverAsync {
952 final Consumer<Intent> mConsumer;
953
954 LocalIntentReceiverAsync(Consumer<Intent> consumer) {
955 mConsumer = consumer;
956 }
957
958 private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
959 @Override
960 public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
961 IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
962 mConsumer.accept(intent);
963 }
964 };
965
966 public IntentSender getIntentSender() {
967 return new IntentSender((IIntentSender) mLocalSender);
968 }
969 }
970
971 private static class LocalIntentReceiverSync {
Dario Freni4b572c02019-01-29 09:40:31 +0000972 private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
973
974 private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
975 @Override
976 public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
Mohammad Samiul Islamef400272019-08-15 16:44:57 +0100977 IIntentReceiver finishedReceiver, String requiredPermission,
978 Bundle options) {
Dario Freni4b572c02019-01-29 09:40:31 +0000979 try {
980 mResult.offer(intent, 5, TimeUnit.SECONDS);
981 } catch (InterruptedException e) {
982 throw new RuntimeException(e);
983 }
984 }
985 };
986
987 public IntentSender getIntentSender() {
988 return new IntentSender((IIntentSender) mLocalSender);
989 }
990
991 public Intent getResult() {
992 try {
993 return mResult.take();
994 } catch (InterruptedException e) {
995 throw new RuntimeException(e);
996 }
997 }
998 }
Mohammad Samiul Islamef400272019-08-15 16:44:57 +0100999
1000 private final class PreRebootVerificationHandler extends Handler {
Rhed Jaob793c212019-10-30 20:54:37 +08001001 // Hold session ids before handler gets ready to do the verification.
1002 private IntArray mPendingSessionIds;
1003 private boolean mIsReady;
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001004
1005 PreRebootVerificationHandler(Looper looper) {
1006 super(looper);
1007 }
1008
1009 /**
1010 * Handler for states of pre reboot verification. The states are arranged linearly (shown
1011 * below) with each state either calling the next state, or calling some other method that
1012 * eventually calls the next state.
1013 *
1014 * <p><ul>
1015 * <li>MSG_PRE_REBOOT_VERIFICATION_START</li>
1016 * <li>MSG_PRE_REBOOT_VERIFICATION_APEX</li>
1017 * <li>MSG_PRE_REBOOT_VERIFICATION_APK</li>
1018 * <li>MSG_PRE_REBOOT_VERIFICATION_END</li>
1019 * </ul></p>
1020 *
1021 * Details about each of state can be found in corresponding handler of node.
1022 */
1023 private static final int MSG_PRE_REBOOT_VERIFICATION_START = 1;
1024 private static final int MSG_PRE_REBOOT_VERIFICATION_APEX = 2;
1025 private static final int MSG_PRE_REBOOT_VERIFICATION_APK = 3;
1026 private static final int MSG_PRE_REBOOT_VERIFICATION_END = 4;
1027
1028 @Override
1029 public void handleMessage(Message msg) {
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001030 final int sessionId = msg.arg1;
1031 final PackageInstallerSession session;
1032 synchronized (mStagedSessions) {
1033 session = mStagedSessions.get(sessionId);
1034 }
1035 // Maybe session was aborted before pre-reboot verification was complete
1036 if (session == null) {
1037 Slog.d(TAG, "Stopping pre-reboot verification for sessionId: " + sessionId);
1038 return;
1039 }
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001040 switch (msg.what) {
1041 case MSG_PRE_REBOOT_VERIFICATION_START:
1042 handlePreRebootVerification_Start(session);
1043 break;
1044 case MSG_PRE_REBOOT_VERIFICATION_APEX:
1045 handlePreRebootVerification_Apex(session);
1046 break;
1047 case MSG_PRE_REBOOT_VERIFICATION_APK:
1048 handlePreRebootVerification_Apk(session);
1049 break;
1050 case MSG_PRE_REBOOT_VERIFICATION_END:
1051 handlePreRebootVerification_End(session);
1052 break;
1053 }
1054 }
1055
Rhed Jaob793c212019-10-30 20:54:37 +08001056 // Notify the handler that system is ready, and reschedule the pre-reboot verifications.
1057 private synchronized void readyToStart() {
1058 mIsReady = true;
1059 if (mPendingSessionIds != null) {
1060 for (int i = 0; i < mPendingSessionIds.size(); i++) {
1061 startPreRebootVerification(mPendingSessionIds.get(i));
1062 }
1063 mPendingSessionIds = null;
1064 }
1065 }
1066
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001067 // Method for starting the pre-reboot verification
Rhed Jaob793c212019-10-30 20:54:37 +08001068 private synchronized void startPreRebootVerification(int sessionId) {
1069 if (!mIsReady) {
1070 if (mPendingSessionIds == null) {
1071 mPendingSessionIds = new IntArray();
1072 }
1073 mPendingSessionIds.add(sessionId);
1074 return;
1075 }
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001076 obtainMessage(MSG_PRE_REBOOT_VERIFICATION_START, sessionId, 0).sendToTarget();
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001077 }
1078
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001079 private void notifyPreRebootVerification_Start_Complete(int sessionId) {
1080 obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APEX, sessionId, 0).sendToTarget();
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001081 }
1082
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001083 private void notifyPreRebootVerification_Apex_Complete(int sessionId) {
1084 obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APK, sessionId, 0).sendToTarget();
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001085 }
1086
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001087 private void notifyPreRebootVerification_Apk_Complete(int sessionId) {
1088 obtainMessage(MSG_PRE_REBOOT_VERIFICATION_END, sessionId, 0).sendToTarget();
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001089 }
1090
1091 /**
1092 * A dummy state for starting the pre reboot verification.
1093 *
1094 * See {@link PreRebootVerificationHandler} to see all nodes of pre reboot verification
1095 */
1096 private void handlePreRebootVerification_Start(@NonNull PackageInstallerSession session) {
1097 Slog.d(TAG, "Starting preRebootVerification for session " + session.sessionId);
Oli Lanc72b0bb2019-12-02 14:03:55 +00001098
1099 if ((session.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
1100 // If rollback is enabled for this session, we call through to the RollbackManager
1101 // with the list of sessions it must enable rollback for. Note that
1102 // notifyStagedSession is a synchronous operation.
1103 final IRollbackManager rm = IRollbackManager.Stub.asInterface(
1104 ServiceManager.getService(Context.ROLLBACK_SERVICE));
1105 try {
1106 // NOTE: To stay consistent with the non-staged install flow, we don't fail the
1107 // entire install if rollbacks can't be enabled.
1108 int rollbackId = rm.notifyStagedSession(session.sessionId);
1109 if (rollbackId != -1) {
1110 synchronized (mStagedSessions) {
1111 mSessionRollbackIds.put(session.sessionId, rollbackId);
1112 }
1113 }
1114 } catch (RemoteException re) {
1115 Slog.e(TAG, "Failed to notifyStagedSession for session: "
1116 + session.sessionId, re);
1117 }
1118 }
1119
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001120 notifyPreRebootVerification_Start_Complete(session.sessionId);
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001121 }
1122
1123 /**
1124 * Pre-reboot verification state for apex files:
1125 *
1126 * <p><ul>
1127 * <li>submits session to apex service</li>
1128 * <li>validates signatures of apex files</li>
1129 * </ul></p>
1130 */
1131 private void handlePreRebootVerification_Apex(@NonNull PackageInstallerSession session) {
1132 final boolean hasApex = sessionContainsApex(session);
1133
1134 // APEX checks. For single-package sessions, check if they contain an APEX. For
1135 // multi-package sessions, find all the child sessions that contain an APEX.
1136 if (hasApex) {
1137 try {
1138 final List<PackageInfo> apexPackages =
1139 submitSessionToApexService(session);
1140 for (PackageInfo apexPackage : apexPackages) {
Mohammad Samiul Islamd7472d02019-11-11 17:53:47 +00001141 validateApexSignature(apexPackage);
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001142 }
1143 } catch (PackageManagerException e) {
1144 session.setStagedSessionFailed(e.error, e.getMessage());
1145 return;
1146 }
1147 }
1148
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001149 notifyPreRebootVerification_Apex_Complete(session.sessionId);
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001150 }
1151
1152 /**
1153 * Pre-reboot verification state for apk files:
1154 * <p><ul>
1155 * <li>performs a dry-run install of apk</li>
1156 * </ul></p>
1157 */
1158 private void handlePreRebootVerification_Apk(@NonNull PackageInstallerSession session) {
1159 if (!sessionContainsApk(session)) {
Mohammad Samiul Islam6dc98892019-09-13 11:10:12 +01001160 notifyPreRebootVerification_Apk_Complete(session.sessionId);
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001161 return;
1162 }
1163
1164 try {
1165 Slog.d(TAG, "Running a pre-reboot verification for APKs in session "
1166 + session.sessionId + " by performing a dry-run install");
1167
Mohammad Samiul Islam8c041df2019-09-10 16:26:26 +01001168 // verifyApksInSession will notify the handler when APK verification is complete
1169 verifyApksInSession(session);
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001170 // TODO(b/118865310): abort the session on apexd.
1171 } catch (PackageManagerException e) {
1172 session.setStagedSessionFailed(e.error, e.getMessage());
1173 }
1174 }
1175
1176 /**
1177 * Pre-reboot verification state for wrapping up:
1178 * <p><ul>
1179 * <li>enables rollback if required</li>
1180 * <li>marks session as ready</li>
1181 * </ul></p>
1182 */
1183 private void handlePreRebootVerification_End(@NonNull PackageInstallerSession session) {
Mohammad Samiul Islam7e9fdb02019-11-28 18:14:40 +00001184 // Before marking the session as ready, start checkpoint service if available
1185 try {
1186 IStorageManager storageManager = PackageHelper.getStorageManager();
1187 if (storageManager.supportsCheckpoint()) {
1188 storageManager.startCheckpoint(1);
1189 }
1190 } catch (Exception e) {
1191 // Failed to get hold of StorageManager
1192 Slog.e(TAG, "Failed to get hold of StorageManager", e);
1193 session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_UNKNOWN,
1194 "Failed to get hold of StorageManager");
1195 return;
1196 }
1197
Mohammad Samiul Islamef400272019-08-15 16:44:57 +01001198 // Proactively mark session as ready before calling apexd. Although this call order
1199 // looks counter-intuitive, this is the easiest way to ensure that session won't end up
1200 // in the inconsistent state:
1201 // - If device gets rebooted right before call to apexd, then apexd will never activate
1202 // apex files of this staged session. This will result in StagingManager failing
1203 // the session.
1204 // On the other hand, if the order of the calls was inverted (first call apexd, then
1205 // mark session as ready), then if a device gets rebooted right after the call to apexd,
1206 // only apex part of the train will be applied, leaving device in an inconsistent state.
1207 Slog.d(TAG, "Marking session " + session.sessionId + " as ready");
1208 session.setStagedSessionReady();
1209 final boolean hasApex = sessionContainsApex(session);
1210 if (!hasApex) {
1211 // Session doesn't contain apex, nothing to do.
1212 return;
1213 }
1214 try {
1215 mApexManager.markStagedSessionReady(session.sessionId);
1216 } catch (PackageManagerException e) {
1217 session.setStagedSessionFailed(e.error, e.getMessage());
1218 }
1219 }
1220 }
Dario Frenibe98c3f2018-12-22 15:25:27 +00001221}