com.android.tools.idea.run.InstalledApkCache.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.run.InstalledApkCache.java

Source

/*
 * 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.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;
        }
    }
}