/*
* Copyright (c) 2015 OpenSilk Productions LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 program. If not, see <http://www.gnu.org/licenses/>.
*/
package syncthing.android.service;
import android.annotation.TargetApi;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.RemoteException;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.opensilk.common.core.dagger2.ForApplication;
import org.opensilk.common.core.util.BundleHelper;
import org.opensilk.common.core.util.VersionUtils;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* Created by drew on 3/21/15.
*/
public class ServiceSettings {
public static final String ENABLED = "local_instance_enabled";
public static final String INITIALISED = "local_instance_initialised";
public static final String RUN_WHEN = "run_when";
public static final String ALWAYS = "always";
public static final String SCHEDULED = "scheduled";
public static final String RANGE_START = "scheduled_start";
public static final String RANGE_END = "scheduled_end";
public static final String ONLY_WIFI = "only_on_wifi";
public static final String WIFI_NETWORKS = "TRANSIENT_wifi_networks";
public static final String ONLY_CHARGING = "only_when_charging";
private final Context appContext;
private final ConnectivityManager cm;
private final WifiManager wm;
private final Uri callUri;
private final ReceiverHelper receiverHelper;
private final Object clientLock = new Object();
private ContentProviderClient client;
private boolean cached;
@Inject
public ServiceSettings(
@ForApplication Context appContext,
ConnectivityManager cm,
WifiManager wm,
@Named("settingsAuthority") String authority,
ReceiverHelper receiverHelper
) {
this.appContext = appContext;
this.cm = cm;
this.wm = wm;
this.callUri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT).authority(authority).build();
this.receiverHelper = receiverHelper;
}
private Bundle getCall(String pref, Bundle extras) {
return makeCall("get_settings", pref, extras, true);
}
private Bundle putCall(String pref, Bundle extras) {
return makeCall("put_settings", pref, extras, true);
}
private Bundle makeCall(String method, String pref, Bundle extras, boolean retry) {
if (cached && VersionUtils.hasApi17()) {
try {
return makeCallApi17(method, pref, extras);
} catch (RemoteException e) {
release();
if (retry) {
return makeCall(method, pref, extras, false);
} else {
return extras;//return defaults
}
}
} else {
return appContext.getContentResolver().call(callUri, method, pref, extras);
}
}
@TargetApi(17)
private Bundle makeCallApi17(String method, String pref, Bundle extras) throws RemoteException {
synchronized (clientLock) {
if (client == null) {
//clients dramatically improve performance and is what
//content resolver does under the hood anyway.
client = appContext.getContentResolver()
.acquireUnstableContentProviderClient(callUri);
if (client == null) {
throw new RuntimeException("Unable to connect to our *own* content provider !!!!");
}
}
return client.call(method, pref, extras);
}
}
public void release() {
synchronized (clientLock) {
if (client != null) {
client.release();
client = null;
}
}
}
public void setCached(boolean cached) {
this.cached = cached;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (client != null) {
Timber.e("Settings were not released!!!");
release();
}
}
public boolean isDisabled() {
return !isEnabled();
}
public boolean isEnabled() {
Bundle reply = getCall(ENABLED, BundleHelper.b().putInt(0).get());
return BundleHelper.getInt(reply) == 1;
}
public void setEnabled(boolean enabled) {
Bundle reply = putCall(ENABLED, BundleHelper.b().putInt(enabled ? 1 : 0).get());
if (StringUtils.equals(BundleHelper.getString(reply), "ok")) {
receiverHelper.setBootReceiverEnabled(enabled);
if (enabled) {
receiverHelper.setConnectivityReceiverEnabled(onlyOnWifi());
receiverHelper.setChargingReceiverEnabled(onlyWhenCharging());
} else {
receiverHelper.setConnectivityReceiverEnabled(false);
receiverHelper.setChargingReceiverEnabled(false);
}
}
}
public boolean isInitialised() {
Bundle reply = getCall(INITIALISED, BundleHelper.b().putInt(0).get());
return BundleHelper.getInt(reply) == 1;
}
public void setInitialized(boolean initialized) {
Bundle reply = putCall(INITIALISED, BundleHelper.b().putInt(initialized ? 1 : 0).get());
if (initialized && "ok".equals(BundleHelper.getString(reply))) {
appContext.getContentResolver().notifyChange(getInitializedUri(), null);
}
}
public String runWhen() {
Bundle reply = getCall(RUN_WHEN, BundleHelper.b().putString(ALWAYS).get());
return BundleHelper.getString(reply);
}
public void setRunWhen(String runWhen) {
Bundle reply = putCall(RUN_WHEN, BundleHelper.b().putString(runWhen).get());
}
public boolean onlyWhenCharging() {
Bundle reply = getCall(ONLY_CHARGING, BundleHelper.b().putInt(0).get());
return BundleHelper.getInt(reply) == 1;
}
public void setOnlyWhenCharging(boolean onlyWhenCharging) {
Bundle reply = putCall(ONLY_CHARGING, BundleHelper.b().putInt(onlyWhenCharging ? 1 : 0).get());
if (StringUtils.equals(BundleHelper.getString(reply), "ok")) {
receiverHelper.setChargingReceiverEnabled(onlyWhenCharging);
}
}
public String getScheduledStartTime() {
Bundle reply = getCall(RANGE_START, BundleHelper.b().putString("00:00").get());
return BundleHelper.getString(reply);
}
public void setScheduledStartTime(String time) {
Bundle reply = putCall(RANGE_START, BundleHelper.b().putString(time).get());
}
public String getScheduledEndTime() {
Bundle reply = getCall(RANGE_END, BundleHelper.b().putString("00:00").get());
return BundleHelper.getString(reply);
}
public void setScheduledEndTime(String time) {
Bundle reply = putCall(RANGE_END, BundleHelper.b().putString(time).get());
}
public boolean onlyOnWifi() {
Bundle reply = getCall(ONLY_WIFI, BundleHelper.b().putInt(0).get());
return BundleHelper.getInt(reply) == 1;
}
public void setOnlyOnWifi(boolean onlyOnWifi) {
Bundle reply = putCall(ONLY_WIFI, BundleHelper.b().putInt(onlyOnWifi ? 1 : 0).get());
if (StringUtils.equals(BundleHelper.getString(reply), "ok")) {
receiverHelper.setConnectivityReceiverEnabled(onlyOnWifi);
}
}
public Set<String> allowedWifiNetworks() {
Bundle reply = getCall(WIFI_NETWORKS, null);
if (reply != null) {
return unrollWifiNetworks(BundleHelper.getString(reply));
} else {
return Collections.emptySet();
}
}
public void setAllowedWifiNetworks(Set<String> networks) {
Bundle reply = putCall(WIFI_NETWORKS, BundleHelper.b().putString(rollWifiNetworks(networks)).get());
}
public Uri getInitializedUri() {
return callUri.buildUpon().appendPath("instanceInitialized").build();
}
boolean isAllowedToRun() {
if (isDisabled()) {
Timber.d("isAllowedToRun(): SyncthingInstance disabled");
return false;
}
if (!isInitialised()) {
Timber.d("isAllowedToRun(): SyncthingInstance initiating credentials");
return true;
}
if (onlyWhenCharging() && !isCharging()) {
Timber.d("isAllowedToRun(): chargingOnly=true and not charging... rejecting");
return false;
}
if (!hasSuitableConnection()) {
Timber.d("isAllowedToRun(): No suitable network... rejecting");
return false;
}
switch (runWhen()) {
case SCHEDULED:
long start = SyncthingUtils.parseTime(getScheduledStartTime());
long end = SyncthingUtils.parseTime(getScheduledEndTime());
boolean can = SyncthingUtils.isNowBetweenRange(start, end);
Timber.d("isAllowedToRun(): is now a good time? %s", can);
return can;
case ALWAYS:
default:
Timber.d("isAllowedToRun(): Always mate!");
return true;
}
}
boolean isCharging() {
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent battChanged = appContext.registerReceiver(null, filter);//stickies returned by register
int status = battChanged != null ? battChanged.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) : 0;
return status != 0;
}
boolean hasSuitableConnection() {
NetworkInfo info = cm.getActiveNetworkInfo();
if (onlyOnWifi()) {
if (info != null && info.isConnectedOrConnecting()) {
if (!isWifiOrEthernet(info.getType())) {
Timber.d("Connection is not wifi network");
return false;
} else if (isWifi(info.getType()) && !isConnectedToWhitelistedNetwork()) {
Timber.d("Connected wifi network not in whitelist");
return false;
} else {
Timber.d("Connected to wifi or ethernet");
return true;
}
} else {
Timber.d("No wifi or ethernet connection");
return false;
}
} else if (info != null && info.isConnectedOrConnecting()) {
Timber.d("Connected to network");
return true;
} else {
Timber.d("Not connected to any networks... running anyway");
//TODO add setting to only run when connected
return true;
}
}
static boolean isWifi(int type) {
return type == ConnectivityManager.TYPE_WIFI;
}
static boolean isWifiOrEthernet(int type) {
return isWifi(type) || type == ConnectivityManager.TYPE_ETHERNET;
}
boolean isConnectedToWhitelistedNetwork() {
Set<String> whitelist = allowedWifiNetworks();
if (whitelist == null || whitelist.isEmpty()) {
Timber.d("No whitelist found");
return true;
}
WifiInfo info = wm.getConnectionInfo();
if (info == null) {
Timber.w("WifiInfo was null");
return true;
}
String ssid = info.getSSID();
if (ssid == null) {
Timber.w("SSID was null");
return true;
}
if (whitelist.contains(ssid)) {
Timber.d("Found %s in whitelist", ssid);
return true;
} else {
return false;
}
}
boolean isOnSchedule() {
return SCHEDULED.equals(runWhen());
}
long getNextScheduledEndTime() {
long start = SyncthingUtils.parseTime(getScheduledStartTime());
long end = SyncthingUtils.parseTime(getScheduledEndTime());
DateTime now = DateTime.now();
Interval interval = SyncthingUtils.getIntervalForRange(now, start, end);
if (interval.contains(now)) {
//With scheduled range
return interval.getEndMillis();
} else {
//Outside scheduled range, shutdown asap
return now.getMillis() + AlarmManagerHelper.KEEP_ALIVE;
}
}
long getNextScheduledStartTime() {
long start = SyncthingUtils.parseTime(getScheduledStartTime());
long end = SyncthingUtils.parseTime(getScheduledEndTime());
return getNextNextStartTimeFor(start, end);
}
static long getNextNextStartTimeFor(long start, long end) {
DateTime now = DateTime.now();
Interval interval = SyncthingUtils.getIntervalForRange(now, start, end);
if (interval.isAfter(now)) {
//Interval hasnt started yet
return interval.getStartMillis();
} else {
//were either inside the interval or past it, get the next days start
//XXX we count inside interval as next day so if user
// explicitly shuts us down we dont just start again
return interval.getStart().plusDays(1).getMillis();
}
}
static String rollWifiNetworks(Set<String> networks) {
if (networks == null || networks.isEmpty()) {
return "";
}
return StringUtils.join(networks, sep);
}
static Set<String> unrollWifiNetworks(String networks) {
Set<String> set = new HashSet<>();
String[] n = StringUtils.split(networks, sep);
if (n != null && n.length > 0) {
Collections.addAll(set, n);
}
return set;
}
private static final char sep = '★';//commas are boring
}