| /* |
| * 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.google.common.annotations.VisibleForTesting; |
| import com.android.ddmlib.*; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.Sets; |
| 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.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| public class InstalledApkCache implements Disposable { |
| private final DeviceStateCache<CacheData> myCache; |
| |
| /** Diagnostic output set by {@link #getLastUpdateTime(com.android.ddmlib.IDevice, String)} */ |
| private String myDiagnosticOutput; |
| |
| public InstalledApkCache() { |
| myCache = new DeviceStateCache<CacheData>(this); |
| } |
| |
| @Override |
| public void dispose() { |
| } |
| |
| public boolean isInstalled( |
| @NotNull IDevice device, |
| @NotNull File apk, |
| @NotNull String pkgName, |
| @Nullable Integer userId) throws IOException { |
| CacheData state = myCache.get(device, pkgName); |
| if (state == null) { |
| return false; |
| } |
| |
| InstallState currentState = getInstallState(device, pkgName); |
| return currentState != null && |
| state.installState.lastUpdateTime.equals(currentState.lastUpdateTime) && |
| state.hash.equals(hash(apk)) && |
| (userId == null || currentState.users.contains(userId)); |
| } |
| |
| public void setInstalled(@NotNull IDevice device, @NotNull File apk, @NotNull String pkgName) throws IOException { |
| InstallState installState = getInstallState(device, pkgName); |
| if (installState == 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(InstalledApkCache.class).warn(msg); |
| return; |
| } |
| |
| myCache.put(device, pkgName, new CacheData(installState, hash(apk))); |
| } |
| |
| @NotNull |
| private static HashCode hash(@NotNull File apk) throws IOException { |
| return Files.hash(apk, Hashing.goodFastHash(32)); |
| } |
| |
| @VisibleForTesting |
| void deviceDisconnected(IDevice device) { |
| myCache.deviceDisconnected(device); |
| } |
| |
| /** |
| * Returns the lastUpdateTime and set of installed users from dumpsys package's output from the given device for the given package. |
| * A null return value indicates that the package was not found. |
| */ |
| @Nullable |
| public InstallState getInstallState(@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; |
| } |
| |
| String lastUpdateTime = ""; |
| Set<Integer> users = Sets.newHashSet(); |
| for (String line : lines) { |
| line = line.trim(); |
| if (line.startsWith("lastUpdateTime")) { |
| lastUpdateTime = line; |
| } |
| if (line.startsWith("User ") && line.contains("installed=true")) { |
| int endIndex = line.indexOf(':'); |
| try { |
| users.add(Integer.parseInt(line.substring("User ".length(), endIndex))); |
| } catch (NumberFormatException e) { |
| // ignore and move on to next line |
| } |
| } |
| } |
| |
| return new InstallState(lastUpdateTime, users); |
| } |
| |
| 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(); |
| } |
| |
| public static class InstallState { |
| @NotNull public final String lastUpdateTime; |
| @NotNull public final Set<Integer> users; |
| |
| public InstallState(@NotNull String lastUpdateTime, @NotNull Set<Integer> users) { |
| this.lastUpdateTime = lastUpdateTime; |
| this.users = users; |
| } |
| } |
| |
| private static class CacheData { |
| @NotNull private final InstallState installState; |
| @NotNull private final HashCode hash; |
| |
| private CacheData(@NotNull InstallState installState, @NotNull HashCode hash) { |
| this.installState = installState; |
| this.hash = hash; |
| } |
| } |
| } |