blob: dc83115ddc1c6f03f69fedfdfb5b64326ca824f3 [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.ddmlib;
import com.android.annotations.NonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.SettableFuture;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Fetches and caches 'getprop' values from device.
*/
class PropertyFetcher {
/** the amount of time to wait between unsuccessful prop fetch attempts */
private static final String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$
private static final Pattern GETPROP_PATTERN = Pattern.compile("^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"); //$NON-NLS-1$
private static final int GETPROP_TIMEOUT_SEC = 2;
private static final int EXPECTED_PROP_COUNT = 150;
private enum CacheState {
UNPOPULATED, FETCHING, POPULATED
}
/**
* Shell output parser for a getprop command
*/
@VisibleForTesting
static class GetPropReceiver extends MultiLineReceiver {
private final Map<String, String> mCollectedProperties =
Maps.newHashMapWithExpectedSize(EXPECTED_PROP_COUNT);
@Override
public void processNewLines(String[] lines) {
// We receive an array of lines. We're expecting
// to have the build info in the first line, and the build
// date in the 2nd line. There seems to be an empty line
// after all that.
for (String line : lines) {
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
Matcher m = GETPROP_PATTERN.matcher(line);
if (m.matches()) {
String label = m.group(1);
String value = m.group(2);
if (!label.isEmpty()) {
mCollectedProperties.put(label, value);
}
}
}
}
@Override
public boolean isCancelled() {
return false;
}
Map<String, String> getCollectedProperties() {
return mCollectedProperties;
}
}
private final Map<String, String> mProperties = Maps.newHashMapWithExpectedSize(
EXPECTED_PROP_COUNT);
private final IDevice mDevice;
private CacheState mCacheState = CacheState.UNPOPULATED;
private final Map<String, SettableFuture<String>> mPendingRequests =
Maps.newHashMapWithExpectedSize(4);
public PropertyFetcher(IDevice device) {
mDevice = device;
}
/**
* Returns the full list of cached properties.
*/
public synchronized Map<String, String> getProperties() {
return mProperties;
}
/**
* Make a possibly asynchronous request for a system property value.
*
* @param name the property name to retrieve
* @return a {@link Future} that can be used to retrieve the prop value
*/
@NonNull
public synchronized Future<String> getProperty(@NonNull String name) {
SettableFuture<String> result;
if (mCacheState.equals(CacheState.FETCHING)) {
result = addPendingRequest(name);
} else if (mDevice.isOnline() && mCacheState.equals(CacheState.UNPOPULATED) || !isRoProp(name)) {
// cache is empty, or this is a volatile prop that requires a query
result = addPendingRequest(name);
mCacheState = CacheState.FETCHING;
initiatePropertiesQuery();
} else {
result = SettableFuture.create();
// cache is populated and this is a ro prop
result.set(mProperties.get(name));
}
return result;
}
private SettableFuture<String> addPendingRequest(String name) {
SettableFuture<String> future = mPendingRequests.get(name);
if (future == null) {
future = SettableFuture.create();
mPendingRequests.put(name, future);
}
return future;
}
private void initiatePropertiesQuery() {
String threadName = String.format("query-prop-%s", mDevice.getSerialNumber());
Thread propThread = new Thread(threadName) {
@Override
public void run() {
try {
GetPropReceiver propReceiver = new GetPropReceiver();
mDevice.executeShellCommand(GETPROP_COMMAND, propReceiver, GETPROP_TIMEOUT_SEC,
TimeUnit.SECONDS);
populateCache(propReceiver.getCollectedProperties());
} catch (Exception e) {
handleException(e);
}
}
};
propThread.setDaemon(true);
propThread.start();
}
private synchronized void populateCache(@NonNull Map<String, String> props) {
mCacheState = props.isEmpty() ? CacheState.UNPOPULATED : CacheState.POPULATED;
if (!props.isEmpty()) {
mProperties.putAll(props);
}
for (Map.Entry<String, SettableFuture<String>> entry : mPendingRequests.entrySet()) {
entry.getValue().set(mProperties.get(entry.getKey()));
}
mPendingRequests.clear();
}
private synchronized void handleException(Exception e) {
mCacheState = CacheState.UNPOPULATED;
Log.w("PropertyFetcher",
String.format("%s getting properties for device %s: %s",
e.getClass().getSimpleName(), mDevice.getSerialNumber(),
e.getMessage()));
for (Map.Entry<String, SettableFuture<String>> entry : mPendingRequests.entrySet()) {
entry.getValue().setException(e);
}
mPendingRequests.clear();
}
/**
* Return true if cache is populated.
*
* @deprecated implementation detail
*/
@Deprecated
public synchronized boolean arePropertiesSet() {
return CacheState.POPULATED.equals(mCacheState);
}
private static boolean isRoProp(@NonNull String propName) {
return propName.startsWith("ro.");
}
}