blob: 6a5e963064bbe07b6ef20765c5710ac1c4a758c8 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.incremental;
import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.DataLoaderParams;
import android.content.pm.InstallationFile;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.ShellCommand;
import android.util.Slog;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Defines actions to handle adb commands like "adb abb incremental ...".
*/
public final class IncrementalManagerShellCommand extends ShellCommand {
private static final String TAG = "IncrementalShellCommand";
// Assuming the adb data loader is always installed on the device
private static final String LOADER_PACKAGE_NAME = "com.android.incremental.nativeadb";
private static final String LOADER_CLASS_NAME =
LOADER_PACKAGE_NAME + ".NativeAdbDataLoaderService";
private final @NonNull Context mContext;
private static final int ERROR_INVALID_ARGUMENTS = -1;
private static final int ERROR_DATA_LOADER_INIT = -2;
private static final int ERROR_COMMAND_EXECUTION = -3;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ERROR_INVALID_ARGUMENTS, ERROR_DATA_LOADER_INIT, ERROR_COMMAND_EXECUTION})
public @interface IncrementalShellCommandErrorCode {
}
IncrementalManagerShellCommand(@NonNull Context context) {
mContext = context;
}
@Override
public int onCommand(@Nullable String cmd) {
if (cmd == null) {
return handleDefaultCommands(null);
}
switch (cmd) {
case "install-start":
return runInstallStart();
case "install-finish":
return runInstallFinish();
default:
return handleDefaultCommands(cmd);
}
}
@Override
public void onHelp() {
PrintWriter pw = getOutPrintWriter();
pw.println("Incremental Service Commands:");
pw.println(" help");
pw.println(" Print this help text.");
pw.println(" install-start");
pw.println(" Opens an installation session");
pw.println(" install-finish SESSION_ID --file NAME:SIZE:INDEX --file NAME:SIZE:INDEX ...");
pw.println(" Commits an installation session specified by session ID for an APK ");
pw.println(" or a bundle of splits. Configures lib dirs or OBB files if specified.");
}
private int runInstallStart() {
final PrintWriter pw = getOutPrintWriter();
final PackageInstaller packageInstaller =
mContext.getPackageManager().getPackageInstaller();
if (packageInstaller == null) {
pw.println("Failed to get PackageInstaller.");
return ERROR_COMMAND_EXECUTION;
}
final Map<String, ParcelFileDescriptor> fds = getShellFileDescriptors();
if (fds == null) {
pw.println("File names and sizes don't match.");
return ERROR_DATA_LOADER_INIT;
}
// dup FDs before closing them
final Map<String, ParcelFileDescriptor> dataLoaderDynamicArgs = new HashMap<>();
for (Map.Entry<String, ParcelFileDescriptor> nfd : fds.entrySet()) {
try {
dataLoaderDynamicArgs.put(nfd.getKey(), nfd.getValue().dup());
} catch (IOException ignored) {
pw.println("Failed to dup shell file descriptor");
return ERROR_DATA_LOADER_INIT;
} finally {
try {
nfd.getValue().close();
} catch (IOException ignored) {
}
}
}
final DataLoaderParams params = DataLoaderParams.forIncremental(
new ComponentName(LOADER_PACKAGE_NAME, LOADER_CLASS_NAME), "",
dataLoaderDynamicArgs);
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
sessionParams.installFlags |= PackageManager.INSTALL_ALL_USERS;
// Replace existing if same package is already installed
sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
sessionParams.setDataLoaderParams(params);
try {
int sessionId = packageInstaller.createSession(sessionParams);
pw.println("Successfully opened session: sessionId = " + sessionId);
} catch (IOException ex) {
pw.println("Failed to create session.");
return ERROR_COMMAND_EXECUTION;
}
return 0;
}
private int runInstallFinish() {
final PrintWriter pw = getOutPrintWriter();
final int sessionId = parseInt(getNextArgRequired());
final List<InstallationFile> installationFiles = parseFileArgs(pw);
if (installationFiles == null) {
pw.println("Must specify at least one file to install.");
return ERROR_INVALID_ARGUMENTS;
}
final int numFiles = installationFiles.size();
if (numFiles == 0) {
pw.println("Must specify at least one file to install.");
return ERROR_INVALID_ARGUMENTS;
}
final PackageInstaller packageInstaller = mContext.getPackageManager()
.getPackageInstaller();
if (packageInstaller == null) {
pw.println("Failed to get PackageInstaller.");
return ERROR_COMMAND_EXECUTION;
}
final LocalIntentReceiver localReceiver = new LocalIntentReceiver();
boolean success = false;
PackageInstaller.Session session = null;
try {
session = packageInstaller.openSession(sessionId);
for (int i = 0; i < numFiles; i++) {
InstallationFile file = installationFiles.get(i);
session.addFile(file.getLocation(), file.getName(), file.getLengthBytes(),
file.getMetadata(), file.getSignature());
}
session.commit(localReceiver.getIntentSender());
final Intent result = localReceiver.getResult();
final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
if (status == PackageInstaller.STATUS_SUCCESS) {
success = true;
pw.println("Success");
return 0;
} else {
pw.println("Failure ["
+ result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + "]");
return ERROR_COMMAND_EXECUTION;
}
} catch (Exception e) {
e.printStackTrace(pw);
return ERROR_COMMAND_EXECUTION;
} finally {
if (!success) {
try {
if (session != null) {
session.abandon();
}
} catch (Exception ignore) {
}
}
}
}
private static class LocalIntentReceiver {
private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
@Override
public void send(int code, Intent intent, String resolvedType,
IBinder whitelistToken,
IIntentReceiver finishedReceiver, String requiredPermission,
Bundle options) {
try {
mResult.offer(intent, 5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
public IntentSender getIntentSender() {
return new IntentSender((IIntentSender) mLocalSender);
}
public Intent getResult() {
try {
return mResult.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/** Helpers. */
private Map<String, ParcelFileDescriptor> getShellFileDescriptors() {
Map<String, ParcelFileDescriptor> fds = new HashMap<>();
final FileDescriptor outFd = getOutFileDescriptor();
final FileDescriptor inFd = getInFileDescriptor();
try {
fds.put("inFd", ParcelFileDescriptor.dup(inFd));
fds.put("outFd", ParcelFileDescriptor.dup(outFd));
return fds;
} catch (Exception ex) {
Slog.e(TAG, "Failed to dup FDs");
return null;
}
}
private long parseLong(String arg) {
long result = -1;
try {
result = Long.parseLong(arg);
} catch (NumberFormatException e) {
}
return result;
}
private int parseInt(String arg) {
int result = -1;
try {
result = Integer.parseInt(arg);
} catch (NumberFormatException e) {
}
return result;
}
private List<InstallationFile> parseFileArgs(PrintWriter pw) {
List<InstallationFile> fileList = new ArrayList<>();
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "--file": {
final String fileArgs = getNextArgRequired();
final String[] args = fileArgs.split(":");
if (args.length != 3) {
pw.println("Invalid file args: " + fileArgs);
return null;
}
final String name = args[0];
final long size = parseLong(args[1]);
if (size < 0) {
pw.println("Invalid file size in: " + fileArgs);
return null;
}
final long index = parseLong(args[2]);
if (index < 0) {
pw.println("Invalid file index in: " + fileArgs);
return null;
}
final byte[] metadata = String.valueOf(index).getBytes(
StandardCharsets.UTF_8);
fileList.add(
new InstallationFile(LOCATION_DATA_APP, name, size, metadata, null));
break;
}
default:
break;
}
}
return fileList;
}
}