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