blob: e7e697ffd49284bb0e3c1d08801124e1c5194be3 [file] [log] [blame]
/*
* Copyright (C) 2014 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.tools.idea.run;
import com.android.annotations.VisibleForTesting;
import com.android.ddmlib.*;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class InstalledApks implements AndroidDebugBridge.IDeviceChangeListener, Disposable {
/**
* A map from device serial -> package name -> install state.
* The install state provides the hash of the apk that was installed, and the last update time as obtained from the device.
*/
private final Map<String, Map<String, InstallState>> myCache = Maps.newHashMap();
/** Diagnostic output set by {@link #getLastUpdateTime(com.android.ddmlib.IDevice, String)} */
private String myDiagnosticOutput;
public InstalledApks() {
AndroidDebugBridge.addDeviceChangeListener(this);
}
@Override
public void dispose() {
AndroidDebugBridge.removeDeviceChangeListener(this);
}
public boolean isInstalled(@NotNull IDevice device, @NotNull File apk, @NotNull String pkgName) throws IOException {
String serial = device.getSerialNumber();
Map<String, InstallState> cache = myCache.get(serial);
if (cache == null) {
return false;
}
InstallState state = cache.get(pkgName);
if (state == null) {
return false;
}
String lastUpdateTime = getLastUpdateTime(device, pkgName);
return lastUpdateTime != null && lastUpdateTime.equals(state.lastUpdateTime) && state.hash.equals(hash(apk));
}
public void setInstalled(@NotNull IDevice device, @NotNull File apk, @NotNull String pkgName) throws IOException {
String serial = device.getSerialNumber();
Map<String, InstallState> cache = myCache.get(serial);
if (cache == null) {
cache = Maps.newHashMap();
myCache.put(serial, cache);
}
String lastUpdateTime = getLastUpdateTime(device, pkgName);
if (lastUpdateTime == null) {
// set installed should be called only after the package has been installed
// If this error happens, look at the output of "dumpsys package <name>", and see why the parser did not identify the install state.
String msg = String.format("Unexpected error: package manager reports that package %1$s has not been installed: %2$s", pkgName,
StringUtil.notNullize(myDiagnosticOutput));
// We used to log an error, but see https://code.google.com/p/android/issues/detail?id=79778 for a case where this doesn't work
// on custom Android systems. So we just log a warning: the impact is that these users won't have any benefits of caching - the apk
// will always be uploaded
Logger.getInstance(InstalledApks.class).warn(msg);
return;
}
cache.put(pkgName, new InstallState(hash(apk), lastUpdateTime));
}
@NotNull
private static HashCode hash(@NotNull File apk) throws IOException {
return Files.hash(apk, Hashing.goodFastHash(32));
}
@Override
public void deviceConnected(IDevice device) {
}
@Override
public void deviceDisconnected(IDevice device) {
myCache.remove(device.getSerialNumber());
}
@Override
public void deviceChanged(IDevice device, int changeMask) {
}
/**
* Returns the lastUpdateTime field from dumpsys package's output from the given device for the given package.
* A null return value indicates that the package was not found, and an empty string indicates that the package was installed, but the
* last update time could not be determined.
*/
@Nullable
@VisibleForTesting
String getLastUpdateTime(@NotNull IDevice device, @NotNull String pkgName) {
boolean deviceHasPackage = false;
myDiagnosticOutput = null;
String output;
try {
output = executeShellCommand(device, "dumpsys package " + pkgName, 500, TimeUnit.MILLISECONDS);
}
catch (Exception e) {
myDiagnosticOutput = String.format("Error executing 'dumpsys package %1$s:\n%2$s'", pkgName, e.getMessage());
return null;
}
// The follow code assumes that the output of "dumpsys package <pkgname>" has at least the following line:
// Package [pkgName]
// Optionally, if it also has a line of form:
// lastUpdateTime=2014-09-29 11:58:19
// then that line is saved as is as the last updated time
Iterable<String> lines = Splitter.on("\n").split(output);
for (String line : lines) {
line = line.trim();
if (line.startsWith("Package [")) {
int startIndex = line.indexOf('[');
int endIndex = line.indexOf(']');
if (startIndex > 0 && endIndex > startIndex) {
deviceHasPackage = pkgName.equals(line.substring(startIndex + 1, endIndex));
}
break;
}
}
if (!deviceHasPackage) {
myDiagnosticOutput = String.format("Expected string 'Package [%1$s]' not found in output: %2$s", pkgName, output);
return null;
}
for (String line : lines) {
line = line.trim();
if (line.startsWith("lastUpdateTime")) {
return line;
}
}
return "";
}
protected String executeShellCommand(@NotNull IDevice device, @NotNull String cmd, long timeout, @NotNull TimeUnit timeUnit)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException, InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch);
device.executeShellCommand(cmd, receiver);
latch.await(timeout, timeUnit);
return receiver.getOutput();
}
private static class InstallState {
@NotNull public final HashCode hash;
@Nullable public final String lastUpdateTime;
public InstallState(@NotNull HashCode hash, @Nullable String lastUpdateTime) {
this.hash = hash;
this.lastUpdateTime = lastUpdateTime;
}
}
}