/*
* AndFHEM - Open Source Android application to control a FHEM home automation
* server.
*
* Copyright (c) 2011, Matthias Klass or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GENERAL PUBLIC LICENSE
* for more details.
*
* You should have received a copy of the GNU GENERAL PUBLIC LICENSE
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package li.klass.fhem.service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import com.google.common.base.Optional;
import com.google.common.io.CharStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import javax.inject.Inject;
import javax.inject.Singleton;
import li.klass.fhem.constants.Actions;
import li.klass.fhem.fhem.DataConnectionSwitch;
import li.klass.fhem.fhem.FHEMConnection;
import li.klass.fhem.fhem.FHEMWEBConnection;
import li.klass.fhem.fhem.RequestResult;
import li.klass.fhem.fhem.RequestResultError;
import li.klass.fhem.util.ApplicationProperties;
import li.klass.fhem.util.Cache;
import li.klass.fhem.util.CloseableUtil;
import static java.util.concurrent.TimeUnit.SECONDS;
import static li.klass.fhem.constants.Actions.DISMISS_EXECUTING_DIALOG;
import static li.klass.fhem.constants.Actions.SHOW_EXECUTING_DIALOG;
import static li.klass.fhem.constants.PreferenceKeys.COMMAND_EXECUTION_RETRIES;
import static li.klass.fhem.fhem.RequestResultError.CONNECTION_TIMEOUT;
import static li.klass.fhem.fhem.RequestResultError.HOST_CONNECTION_ERROR;
@Singleton
public class CommandExecutionService extends AbstractService {
public static final int DEFAULT_NUMBER_OF_RETRIES = 3;
private static final int IMAGE_CACHE_SIZE = 20;
private static final Logger LOG = LoggerFactory.getLogger(CommandExecutionService.class);
private static final ResultListener DO_NOTHING = new SuccessfulResultListener() {
@Override
public void onResult(String result) {
}
};
@Inject
DataConnectionSwitch dataConnectionSwitch;
@Inject
ApplicationProperties applicationProperties;
private transient ScheduledExecutorService scheduledExecutorService = null;
private transient Command lastFailedCommand = null;
private transient Cache<Bitmap> imageCache = getImageCache();
@Inject
public CommandExecutionService() {
}
public void resendLastFailedCommand(Context context) {
if (lastFailedCommand != null) {
Command command = lastFailedCommand;
lastFailedCommand = null;
executeSafely(command, context, DO_NOTHING);
}
}
public String executeSync(Command command, Context context) {
SyncResultListener resultListener = new SyncResultListener();
executeSafely(command, 0, context, resultListener);
return resultListener.getResult();
}
public void executeSafely(Command command, Context context, ResultListener resultListener) {
executeSafely(command, 0, context, resultListener);
}
public void executeSafely(Command command, int delay, Context context, ResultListener resultListener) {
LOG.info("executeSafely(command={}, delay={})", command, delay);
if (delay == 0) {
executeImmediately(command, 0, context, resultListener);
} else {
executeDelayed(command, delay, context, resultListener);
}
}
private void executeDelayed(Command command, int delay, Context context, ResultListener callback) {
schedule(delay, new ResendCommand(command, 0, context, callback));
}
private void executeImmediately(Command command, int currentTry, Context context, ResultListener resultListener) {
showExecutingDialog(context);
RequestResult<String> result = execute(command, currentTry, context, resultListener);
if (result.handleErrors()) {
lastFailedCommand = command;
resultListener.onError();
} else {
resultListener.onResult(result.content);
}
}
private void showExecutingDialog(Context context) {
context.sendBroadcast(new Intent(SHOW_EXECUTING_DIALOG));
}
private RequestResult<String> execute(Command command, int currentTry, Context context, ResultListener resultListener) {
Optional<FHEMConnection> currentProvider = dataConnectionSwitch.getProviderFor(context, command.connectionId);
if (!currentProvider.isPresent()) {
return new RequestResult<>(RequestResultError.HOST_CONNECTION_ERROR);
}
RequestResult<String> result = currentProvider.get().executeCommand(command.command, context);
LOG.info("execute() - executing command={}, try={}", command, currentTry);
try {
if (result.error == null) {
sendBroadcastWithAction(Actions.CONNECTION_ERROR_HIDE, context);
} else if (shouldTryResend(command.command, result, currentTry, context)) {
int timeoutForNextTry = secondsForTry(currentTry);
ResendCommand resendCommand = new ResendCommand(command, currentTry + 1, context, resultListener);
schedule(timeoutForNextTry, resendCommand);
}
} finally {
if (!command.command.equalsIgnoreCase("xmllist")) {
hideExecutingDialog(context);
}
}
return result;
}
public ScheduledFuture<?> schedule(int timeoutForNextTry, ResendCommand resendCommand) {
LOG.info("schedule() - schedule {} in {} seconds", resendCommand, timeoutForNextTry);
return getScheduledExecutorService().schedule(resendCommand, timeoutForNextTry, SECONDS);
}
private boolean shouldTryResend(String command, RequestResult<?> result, int currentTry, Context context) {
if (!command.startsWith("set") && !command.startsWith("attr")) return false;
if (result.error == null) return false;
if (result.error != CONNECTION_TIMEOUT &&
result.error != HOST_CONNECTION_ERROR) return false;
if (currentTry > getNumberOfRetries(context)) return false;
return true;
}
public static int secondsForTry(int executionTry) {
return (int) Math.pow(3, executionTry);
}
private ScheduledExecutorService getScheduledExecutorService() {
if (scheduledExecutorService == null) {
scheduledExecutorService = Executors.newScheduledThreadPool(1);
}
return scheduledExecutorService;
}
private void hideExecutingDialog(Context context) {
context.sendBroadcast(new Intent(DISMISS_EXECUTING_DIALOG));
}
private int getNumberOfRetries(Context context) {
return applicationProperties.getIntegerSharedPreference(
COMMAND_EXECUTION_RETRIES, DEFAULT_NUMBER_OF_RETRIES,
context);
}
public Command getLastFailedCommand() {
return lastFailedCommand;
}
public Bitmap getBitmap(String relativePath, Context context) {
try {
Cache<Bitmap> cache = getImageCache();
if (cache.containsKey(relativePath)) {
return cache.get(relativePath);
} else {
showExecutingDialog(context);
FHEMConnection provider = dataConnectionSwitch.getProviderFor(context);
RequestResult<Bitmap> result = provider.requestBitmap(relativePath);
if (result.handleErrors()) return null;
Bitmap bitmap = result.content;
cache.put(relativePath, bitmap);
return bitmap;
}
} finally {
hideExecutingDialog(context);
}
}
public Optional<String> executeRequest(String relativPath, Context context) {
FHEMConnection provider = dataConnectionSwitch.getProviderFor(context);
if (!(provider instanceof FHEMWEBConnection)) {
return Optional.absent();
}
RequestResult<InputStream> result = ((FHEMWEBConnection) provider).executeRequest(relativPath);
if (result.handleErrors()) {
return Optional.absent();
}
try {
return Optional.of(CharStreams.toString(new InputStreamReader(result.content)));
} catch (IOException e) {
LOG.error("executeRequest() - cannot read stream", e);
return Optional.absent();
} finally {
CloseableUtil.close(result.content);
}
}
private Cache<Bitmap> getImageCache() {
if (imageCache == null) {
imageCache = new Cache<>(IMAGE_CACHE_SIZE);
}
return imageCache;
}
private static class SyncResultListener extends SuccessfulResultListener {
private String result;
@Override
public void onResult(String result) {
this.result = result;
}
public String getResult() {
return result != null ? result.trim() : null;
}
}
private class ResendCommand implements Runnable {
private final Context context;
private ResultListener resultListener;
int currentTry;
Command command;
ResendCommand(Command command, int currentTry, Context context, ResultListener resultListener) {
this.command = command;
this.currentTry = currentTry;
this.context = context;
this.resultListener = resultListener;
}
@Override
public void run() {
executeImmediately(command, currentTry, context, resultListener);
}
@Override
public String toString() {
return "ResendCommand{" +
", context=" + context +
", currentTry=" + currentTry +
", command='" + command + '\'' +
'}';
}
}
public interface ResultListener {
void onResult(String result);
void onError();
}
public static abstract class SuccessfulResultListener implements ResultListener {
@Override
public void onError() {
}
}
}