Refactor ConfigXml (#135)
Changelog: - "Use default folder path given in config.xml" (#101) - "IllegalStateException: Fragment already added" (#108) - "Enhancement request for per-folder(device) sync conditions" (#110) - "NPE crash after key and config regeneration" (#141) - "Adjust the folder icon to show if it's send/receive or both" (#143) - "CPU percentage is not shown on the status tab" (#144) - "Always make individual sync conditions UI available" (#145) - "IntroducedBy deviceID lost on config change through wrapper UI" (#146) - "Wrapper doesn't use the same syntax as syncthing core's web UI for device addresses" (#147) - "Syncthing wrapper "emergency" shutdown on native binary crash doesn't work" (#148) Commits: * WIP * WIP * Get folder list and paused setting when syncthing is not running Preparation to solve #110 * Fix NPE in DeviceListFragment#DEVICES_COMPARATOR * Remove blank line * Add ConfigXml#getDevices and comparator Make ConfigXml#saveChanges public * SyncthingService evaluates per folder/device sync conditions when syncthing is not running via ConfigXml * Fix typos and add stubs * Fix build errors * DEBUG - Always run syncthing binary * Fix NPE at RunConditionMonitor pointer * Add setFolderPause, setDevicePause to ConfigXml * Improve logging * Remove test mode * Better log levels * Make ConfigXml#updateIfNeeded private * Remove SyncthingService#mStartupTask AsyncTask no longer needed * Update model/Options (fixes #101) * Fix NPE after config regeneration (fixes #140) * Refactor key and config generation Refactor ConfigXml public functions to allow checking if a valid config exists and trigger key and config (re)genration if something is corrupted. * Fix crash on export/import (fixes #142) * ApiRequest - Disable verbose log in release builds * ConfigXml#updateIfNeeded - Disable "startBrowser" because it applies to desktop environments and cannot start a mobile browser app * MainActivity - Always show all tabs * Show folder/device tab contents from config.xml if syncthing is not running * Update ConfigXml#getDevices return model - compression - introducer * Device tab - Hide in/out rate if syncthing is not running or if the device is paused * Update device item layout * MainActivity/Devices - Prevent showing outdated status after syncthing core transitioned from "active" to "disabled" * MainActivity/Folders - Prevent showing outdated status after syncthing core transitioned from "active" to "disabled" * Add ConfigRouter class Provides a transparent access to the config if ... a) Syncthing is running and REST API is available. b) Syncthing is NOT running and config.xml is accessed. * Add pref - Cache local device ID * Allow excluding self in ConfigRouter#getDevices * Allow excluding self in ConfigRouter#getDevices (2) * Update Folder model default values * Update Folder model defaults (2) - copiers - hashers * WIP - ConfigXml - FolderActivity Remove unused pref inject code Cache local device ID in pref Reduce verbose logging in release builds Extend ConfigXml#getFolders Extend ConfigXml#getDevices Fix ConfigXml#setDevicePause ToDo ConfigXml#getFolderIgnoreList needs to be implemented * Implemented ConfigXml#getFolderIgnoreList * Extend ConfigXml#getDevices - device.addresses * WIP - DeviceActivity Make it available when syncthing is not running * Fix unsuccessful API bumps while syncthing is starting * Fix space * Adjust the folder icon to show if it's send/receive or both (fixes #143) * Fix lint - item_device_list * Preserve active tab when syncthing core transitions between running and not running * Add xmlns:android to item_folder_list * Remove unused reference from item_folder_list * Add device icon to device tab * Fix CPU percentage not showing (fixes #144) * SyncthingService - Polish iterator code * Fix MainActivity#updateViewPager (fixes #108) * Add ConfigXml#updateFolder, updateDevice (1) * Add ConfigRouter#updateFolder, updateDevice * Add missing "final" to ConfigXml#updateDevice * WIP - FolderActivity - Update updateFolder via ConfigRouter ToDo: Implement ConfigRouter here. * ConfigRouter - Fix missing return * DeviceActivity - Update device via ConfigRouter * Always make individual sync conditions UI available (fixes #145) regardless if syncthing core is running or not. Remove SyncthingService dependency from SyncConditionsActivity * Fix incorrect folder type icon shown when syncthing core is not running * Add "introducedBy" to folder and device model (fixes #146) * Add Folder#getDevices to model * ConfigXml#updateFolder - Writeback devices sharing the folder Support preserving the "introducedBy" model field of Folder.java (fixes #146) * Add ConfigXml#updateFolder - Versioning * Remove SyncthingService dependency from FolderPickerActivity because it is no longer required. * Update ToDo remarks * Add ConfigXml#updateDevice - Addresses * Fix DeviceActivity#persistableAddresses to be more graceful (fixes #147) and accept the same address syntax as syncthing core web UI does. * Add ConfigXml#removeFolder, removeDevice * Add ConfigXml#addDevice, addFolder - Add ConfigXml#isDeviceIdValid - Do not allow adding empty folder labels or empty device names. - Update model Folder.java so ConfigXml can handle the ignorePerms XML attribute * Fix Syncthing wrapper "emergency" shutdown on native binary crash (fixes #148) * Update translation de * Add ConfigXml#postFolderIgnoreList * Update APK version to 0.14.54.3 / 4182 * Revert DEBUG - Always run syncthing binary * Update whatsnew
|
@ -37,8 +37,8 @@ android {
|
|||
applicationId "com.github.catfriend1.syncthingandroid"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 26
|
||||
versionCode 4181
|
||||
versionName "0.14.54.2"
|
||||
versionCode 4182
|
||||
versionName "0.14.54.3"
|
||||
testApplicationId 'com.github.catfriend1.syncthingandroid.test'
|
||||
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
|
||||
playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.nutomic.syncthingandroid.service.RestApi;
|
|||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
import com.nutomic.syncthingandroid.SyncthingApp;
|
||||
import com.nutomic.syncthingandroid.util.Compression;
|
||||
import com.nutomic.syncthingandroid.util.ConfigRouter;
|
||||
import com.nutomic.syncthingandroid.util.TextWatcherAdapter;
|
||||
import com.nutomic.syncthingandroid.util.Util;
|
||||
|
||||
|
@ -68,13 +69,15 @@ public class DeviceActivity extends SyncthingActivity
|
|||
public static final String EXTRA_IS_CREATE =
|
||||
"com.nutomic.syncthingandroid.activities.DeviceActivity.IS_CREATE";
|
||||
|
||||
private static final String TAG = "DeviceSettingsFragment";
|
||||
private static final String TAG = "DeviceActivity";
|
||||
private static final String IS_SHOWING_DISCARD_DIALOG = "DISCARD_FOLDER_DIALOG_STATE";
|
||||
private static final String IS_SHOWING_COMPRESSION_DIALOG = "COMPRESSION_FOLDER_DIALOG_STATE";
|
||||
private static final String IS_SHOWING_DELETE_DIALOG = "DELETE_FOLDER_DIALOG_STATE";
|
||||
|
||||
private static final List<String> DYNAMIC_ADDRESS = Collections.singletonList("dynamic");
|
||||
|
||||
private ConfigRouter mConfig;
|
||||
|
||||
private Device mDevice;
|
||||
|
||||
private View mIdContainer;
|
||||
|
@ -186,6 +189,8 @@ public class DeviceActivity extends SyncthingActivity
|
|||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
mConfig = new ConfigRouter(DeviceActivity.this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
((SyncthingApp) getApplication()).component().inject(this);
|
||||
setContentView(R.layout.fragment_device);
|
||||
|
@ -338,14 +343,9 @@ public class DeviceActivity extends SyncthingActivity
|
|||
|
||||
@Override
|
||||
public void onServiceStateChange(SyncthingService.State currentState) {
|
||||
if (currentState != ACTIVE) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mIsCreateMode) {
|
||||
RestApi restApi = getApi(); // restApi != null because of State.ACTIVE
|
||||
List<Device> devices = restApi.getDevices(false);
|
||||
RestApi restApi = getApi();
|
||||
List<Device> devices = mConfig.getDevices(restApi, false);
|
||||
String passedId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
|
||||
mDevice = null;
|
||||
for (Device currentDevice : devices) {
|
||||
|
@ -427,8 +427,16 @@ public class DeviceActivity extends SyncthingActivity
|
|||
.show();
|
||||
return true;
|
||||
}
|
||||
getApi().addDevice(mDevice, error ->
|
||||
Toast.makeText(this, error, Toast.LENGTH_LONG).show());
|
||||
if (isEmpty(mDevice.name)) {
|
||||
Toast.makeText(this, R.string.device_name_required, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
mConfig.addDevice(
|
||||
getApi(),
|
||||
mDevice,
|
||||
error -> Toast.makeText(this, error, Toast.LENGTH_LONG).show()
|
||||
);
|
||||
finish();
|
||||
return true;
|
||||
case R.id.share_device_id:
|
||||
|
@ -455,7 +463,8 @@ public class DeviceActivity extends SyncthingActivity
|
|||
return new android.app.AlertDialog.Builder(this)
|
||||
.setMessage(R.string.remove_device_confirm)
|
||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> {
|
||||
getApi().removeDevice(mDevice.deviceID);
|
||||
mConfig.removeDevice(getApi(), mDevice.deviceID);
|
||||
mDeviceNeedsToUpdate = false;
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
|
@ -474,6 +483,9 @@ public class DeviceActivity extends SyncthingActivity
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in mIsCreateMode.
|
||||
*/
|
||||
private void initDevice() {
|
||||
mDevice = new Device();
|
||||
mDevice.name = getIntent().getStringExtra(EXTRA_DEVICE_NAME);
|
||||
|
@ -482,6 +494,7 @@ public class DeviceActivity extends SyncthingActivity
|
|||
mDevice.compression = METADATA.getValue(this);
|
||||
mDevice.introducer = false;
|
||||
mDevice.paused = false;
|
||||
mDevice.introducedBy = "";
|
||||
}
|
||||
|
||||
private void prepareEditMode() {
|
||||
|
@ -501,13 +514,14 @@ public class DeviceActivity extends SyncthingActivity
|
|||
*/
|
||||
private void updateDevice() {
|
||||
if (mIsCreateMode) {
|
||||
// If we are about to create this folder, we cannot update via restApi.
|
||||
// If we are about to create this device, we cannot update via restApi.
|
||||
return;
|
||||
}
|
||||
if (mDevice == null) {
|
||||
Log.e(TAG, "updateDevice: mDevice == null");
|
||||
return;
|
||||
}
|
||||
// Log.v(TAG, "deviceID=" + mDevice.deviceID + ", introducedBy=" + mDevice.introducedBy);
|
||||
|
||||
// Save device specific preferences.
|
||||
Log.v(TAG, "updateDevice: mDevice.deviceID = \'" + mDevice.deviceID + "\'");
|
||||
|
@ -518,26 +532,42 @@ public class DeviceActivity extends SyncthingActivity
|
|||
);
|
||||
editor.apply();
|
||||
|
||||
// Update device via restApi and send the config to REST endpoint.
|
||||
RestApi restApi = getApi();
|
||||
if (restApi == null) {
|
||||
Log.e(TAG, "updateDevice: restApi == null");
|
||||
return;
|
||||
}
|
||||
restApi.updateDevice(mDevice);
|
||||
// Update device using RestApi or ConfigXml.
|
||||
mConfig.updateDevice(getApi(), mDevice);
|
||||
}
|
||||
|
||||
private List<String> persistableAddresses(CharSequence userInput) {
|
||||
return isEmpty(userInput)
|
||||
? DYNAMIC_ADDRESS
|
||||
: Arrays.asList(userInput.toString().split(" "));
|
||||
if (isEmpty(userInput)) {
|
||||
return DYNAMIC_ADDRESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Be fault-tolerant here.
|
||||
* The user can write like this:
|
||||
* tcp4://192.168.1.67:2222, dynamic
|
||||
* tcp4://192.168.1.67:2222; dynamic
|
||||
* tcp4://192.168.1.67:2222,dynamic
|
||||
* tcp4://192.168.1.67:2222;dynamic
|
||||
* tcp4://192.168.1.67:2222 dynamic
|
||||
*/
|
||||
String input = userInput.toString();
|
||||
input = input.replace(",", " ");
|
||||
input = input.replace(";", " ");
|
||||
input = input.replaceAll("\\s+", ", ");
|
||||
// Log.v(TAG, "persistableAddresses: Cleaned user input=" + input);
|
||||
|
||||
// Split and return the addresses as String[].
|
||||
return Arrays.asList(input.split(", "));
|
||||
}
|
||||
|
||||
private String displayableAddresses() {
|
||||
if (mDevice.addresses == null) {
|
||||
return "";
|
||||
}
|
||||
List<String> list = DYNAMIC_ADDRESS.equals(mDevice.addresses)
|
||||
? DYNAMIC_ADDRESS
|
||||
: mDevice.addresses;
|
||||
return TextUtils.join(" ", list);
|
||||
return TextUtils.join(", ", list);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -93,6 +93,21 @@ public class FirstStartActivity extends Activity {
|
|||
super.onCreate(savedInstanceState);
|
||||
((SyncthingApp) getApplication()).component().inject(this);
|
||||
|
||||
/**
|
||||
* Check if a valid config exists that can be read and parsed.
|
||||
*/
|
||||
Boolean configParseable = false;
|
||||
Boolean configExists = Constants.getConfigFile(this).exists();
|
||||
if (configExists) {
|
||||
ConfigXml configParseTest = new ConfigXml(this);
|
||||
try {
|
||||
configParseTest.loadConfig();
|
||||
configParseable = true;
|
||||
} catch (ConfigXml.OpenConfigException e) {
|
||||
Log.d(TAG, "Failed to parse existing config. Will show key generation slide ...");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prerequisites to run the app are still in place.
|
||||
* If anything mandatory is missing, the according welcome slide(s) will be shown.
|
||||
|
@ -100,7 +115,7 @@ public class FirstStartActivity extends Activity {
|
|||
Boolean showSlideStoragePermission = !haveStoragePermission();
|
||||
Boolean showSlideIgnoreDozePermission = !haveIgnoreDozePermission();
|
||||
Boolean showSlideLocationPermission = !haveLocationPermission();
|
||||
Boolean showSlideKeyGeneration = !Constants.getConfigFile(this).exists();
|
||||
Boolean showSlideKeyGeneration = !configExists || !configParseable;
|
||||
|
||||
/**
|
||||
* If we don't have to show slides for mandatory prerequisites,
|
||||
|
@ -480,8 +495,10 @@ public class FirstStartActivity extends Activity {
|
|||
cancel(true);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
configXml = new ConfigXml(firstStartActivity);
|
||||
try {
|
||||
// Create new secure keys and config.
|
||||
configXml.generateConfig();
|
||||
} catch (ExecutableNotFoundException e) {
|
||||
publishProgress(firstStartActivity.getString(R.string.executable_not_found, e.getMessage()));
|
||||
cancel(true);
|
||||
|
|
|
@ -36,6 +36,7 @@ import com.nutomic.syncthingandroid.service.Constants;
|
|||
import com.nutomic.syncthingandroid.service.RestApi;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
import com.nutomic.syncthingandroid.SyncthingApp;
|
||||
import com.nutomic.syncthingandroid.util.ConfigRouter;
|
||||
import com.nutomic.syncthingandroid.util.FileUtils;
|
||||
import com.nutomic.syncthingandroid.util.TextWatcherAdapter;
|
||||
import com.nutomic.syncthingandroid.util.Util;
|
||||
|
@ -85,6 +86,7 @@ public class FolderActivity extends SyncthingActivity
|
|||
private static final String FOLDER_MARKER_NAME = ".stfolder";
|
||||
// private static final String IGNORE_FILE_NAME = ".stignore";
|
||||
|
||||
private ConfigRouter mConfig;
|
||||
private Folder mFolder;
|
||||
// Contains SAF readwrite access URI on API level >= Build.VERSION_CODES.LOLLIPOP (21)
|
||||
private Uri mFolderUri = null;
|
||||
|
@ -154,7 +156,7 @@ public class FolderActivity extends SyncthingActivity
|
|||
case R.id.device_toggle:
|
||||
Device device = (Device) view.getTag();
|
||||
if (isChecked) {
|
||||
mFolder.addDevice(device.deviceID);
|
||||
mFolder.addDevice(device);
|
||||
} else {
|
||||
mFolder.removeDevice(device.deviceID);
|
||||
}
|
||||
|
@ -166,6 +168,8 @@ public class FolderActivity extends SyncthingActivity
|
|||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
mConfig = new ConfigRouter(FolderActivity.this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
((SyncthingApp) getApplication()).component().inject(this);
|
||||
setContentView(R.layout.fragment_folder);
|
||||
|
@ -384,14 +388,9 @@ public class FolderActivity extends SyncthingActivity
|
|||
|
||||
@Override
|
||||
public void onServiceStateChange(SyncthingService.State currentState) {
|
||||
if (currentState != ACTIVE) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mIsCreateMode) {
|
||||
RestApi restApi = getApi(); // restApi != null because of State.ACTIVE
|
||||
List<Folder> folders = restApi.getFolders();
|
||||
RestApi restApi = getApi();
|
||||
List<Folder> folders = mConfig.getFolders(restApi);
|
||||
String passedId = getIntent().getStringExtra(EXTRA_FOLDER_ID);
|
||||
mFolder = null;
|
||||
for (Folder currentFolder : folders) {
|
||||
|
@ -405,11 +404,15 @@ public class FolderActivity extends SyncthingActivity
|
|||
finish();
|
||||
return;
|
||||
}
|
||||
restApi.getFolderIgnoreList(mFolder.id, this::onReceiveFolderIgnoreList);
|
||||
mConfig.getFolderIgnoreList(restApi, mFolder, this::onReceiveFolderIgnoreList);
|
||||
checkWriteAndUpdateUI();
|
||||
}
|
||||
|
||||
// If the extra is set, we should automatically share the current folder with the given device.
|
||||
if (getIntent().hasExtra(EXTRA_DEVICE_ID)) {
|
||||
mFolder.addDevice(getIntent().getStringExtra(EXTRA_DEVICE_ID));
|
||||
Device device = new Device();
|
||||
device.deviceID = getIntent().getStringExtra(EXTRA_DEVICE_ID);
|
||||
mFolder.addDevice(device);
|
||||
mFolderNeedsToUpdate = true;
|
||||
}
|
||||
|
||||
|
@ -471,13 +474,14 @@ public class FolderActivity extends SyncthingActivity
|
|||
mCustomSyncConditionsDialog.setEnabled(mCustomSyncConditionsSwitch.isChecked());
|
||||
|
||||
// Populate devicesList.
|
||||
List<Device> devicesList = getApi().getDevices(false);
|
||||
RestApi restApi = getApi();
|
||||
List<Device> devicesList = mConfig.getDevices(restApi, false);
|
||||
mDevicesContainer.removeAllViews();
|
||||
if (devicesList.isEmpty()) {
|
||||
addEmptyDeviceListView();
|
||||
} else {
|
||||
for (Device n : devicesList) {
|
||||
addDeviceViewAndSetListener(n, getLayoutInflater());
|
||||
for (Device device : devicesList) {
|
||||
addDeviceViewAndSetListener(device, getLayoutInflater());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -511,6 +515,11 @@ public class FolderActivity extends SyncthingActivity
|
|||
.show();
|
||||
return true;
|
||||
}
|
||||
if (TextUtils.isEmpty(mFolder.label)) {
|
||||
Toast.makeText(this, R.string.folder_label_required, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
if (TextUtils.isEmpty(mFolder.path)) {
|
||||
Toast.makeText(this, R.string.folder_path_required, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
|
@ -531,7 +540,7 @@ public class FolderActivity extends SyncthingActivity
|
|||
dfFolder.createDirectory(FOLDER_MARKER_NAME);
|
||||
}
|
||||
}
|
||||
getApi().createFolder(mFolder);
|
||||
mConfig.addFolder(getApi(), mFolder);
|
||||
finish();
|
||||
return true;
|
||||
case R.id.remove:
|
||||
|
@ -554,10 +563,7 @@ public class FolderActivity extends SyncthingActivity
|
|||
return new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.remove_folder_confirm)
|
||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> {
|
||||
RestApi restApi = getApi();
|
||||
if (restApi != null) {
|
||||
restApi.removeFolder(mFolder.id);
|
||||
}
|
||||
mConfig.removeFolder(getApi(), mFolder.id);
|
||||
mFolderNeedsToUpdate = false;
|
||||
finish();
|
||||
})
|
||||
|
@ -731,17 +737,13 @@ public class FolderActivity extends SyncthingActivity
|
|||
|
||||
// Update folder via restApi and send the config to REST endpoint.
|
||||
RestApi restApi = getApi();
|
||||
if (restApi == null) {
|
||||
Log.e(TAG, "updateFolder: restApi == null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update ignore list.
|
||||
String[] ignore = mEditIgnoreListContent.getText().toString().split("\n");
|
||||
restApi.postFolderIgnoreList(mFolder.id, ignore);
|
||||
mConfig.postFolderIgnoreList(restApi, mFolder, ignore);
|
||||
|
||||
// Update model and send the config to REST endpoint.
|
||||
restApi.updateFolder(mFolder);
|
||||
// Update folder using RestApi or ConfigXml.
|
||||
mConfig.updateFolder(restApi, mFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.content.Intent;
|
|||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
@ -31,8 +30,6 @@ import com.google.common.collect.Sets;
|
|||
import com.nutomic.syncthingandroid.R;
|
||||
import com.nutomic.syncthingandroid.SyncthingApp;
|
||||
import com.nutomic.syncthingandroid.service.Constants;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingServiceBinder;
|
||||
import com.nutomic.syncthingandroid.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -45,7 +42,7 @@ import java.util.Iterator;
|
|||
* Activity that allows selecting a directory in the local file system.
|
||||
*/
|
||||
public class FolderPickerActivity extends SyncthingActivity
|
||||
implements AdapterView.OnItemClickListener, SyncthingService.OnServiceStateChangeListener {
|
||||
implements AdapterView.OnItemClickListener {
|
||||
|
||||
private static final String EXTRA_INITIAL_DIRECTORY =
|
||||
"com.nutomic.syncthingandroid.activities.FolderPickerActivity.INITIAL_DIRECTORY";
|
||||
|
@ -151,22 +148,6 @@ public class FolderPickerActivity extends SyncthingActivity
|
|||
mRootsAdapter.addAll(Sets.newTreeSet(roots));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
|
||||
super.onServiceConnected(componentName, iBinder);
|
||||
SyncthingServiceBinder syncthingServiceBinder = (SyncthingServiceBinder) iBinder;
|
||||
syncthingServiceBinder.getService().registerOnServiceStateChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
SyncthingService syncthingService = getService();
|
||||
if (syncthingService != null) {
|
||||
syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
if (mListView.getAdapter() == mRootsAdapter)
|
||||
|
@ -324,14 +305,6 @@ public class FolderPickerActivity extends SyncthingActivity
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceStateChange(SyncthingService.State currentState) {
|
||||
if (!isFinishing() && currentState != SyncthingService.State.ACTIVE) {
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a list of all available roots, or if there is only one root, the
|
||||
* contents of that folder.
|
||||
|
|
|
@ -97,6 +97,9 @@ public class MainActivity extends SyncthingActivity
|
|||
|
||||
private ActionBarDrawerToggle mDrawerToggle;
|
||||
private DrawerLayout mDrawerLayout;
|
||||
|
||||
private Boolean oneTimeShot = true;
|
||||
|
||||
@Inject SharedPreferences mPreferences;
|
||||
|
||||
/**
|
||||
|
@ -104,9 +107,10 @@ public class MainActivity extends SyncthingActivity
|
|||
*/
|
||||
@Override
|
||||
public void onServiceStateChange(SyncthingService.State currentState) {
|
||||
if (currentState != mSyncthingServiceState) {
|
||||
mSyncthingServiceState = currentState;
|
||||
if (oneTimeShot) {
|
||||
updateViewPager();
|
||||
oneTimeShot = false;
|
||||
}
|
||||
|
||||
switch (currentState) {
|
||||
|
@ -178,15 +182,12 @@ public class MainActivity extends SyncthingActivity
|
|||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mViewPager.setCurrentItem(savedInstanceState.getInt("currentTab"));
|
||||
if (savedInstanceState.getBoolean(IS_SHOWING_RESTART_DIALOG)){
|
||||
showRestartDialog();
|
||||
}
|
||||
if(savedInstanceState.getBoolean(IS_QRCODE_DIALOG_DISPLAYED)) {
|
||||
if (savedInstanceState.getBoolean(IS_QRCODE_DIALOG_DISPLAYED)) {
|
||||
showQrCodeDialog(savedInstanceState.getString(DEVICEID_KEY), savedInstanceState.getParcelable(QRCODE_BITMAP_KEY));
|
||||
}
|
||||
} else {
|
||||
updateViewPager();
|
||||
}
|
||||
|
||||
fm.beginTransaction().replace(R.id.drawer, mDrawerFragment).commit();
|
||||
|
@ -211,14 +212,12 @@ public class MainActivity extends SyncthingActivity
|
|||
* Updates the ViewPager to show tabs depending on the service state.
|
||||
*/
|
||||
private void updateViewPager() {
|
||||
Boolean isServiceActive = mSyncthingServiceState == SyncthingService.State.ACTIVE;
|
||||
final int numPages = (isServiceActive ? 3 : 1);
|
||||
final int numPages = 3;
|
||||
FragmentStatePagerAdapter mSectionsPagerAdapter =
|
||||
new FragmentStatePagerAdapter(getSupportFragmentManager()) {
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
if (isServiceActive) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return mFolderListFragment;
|
||||
|
@ -229,14 +228,6 @@ public class MainActivity extends SyncthingActivity
|
|||
default:
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return mStatusFragment;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -251,7 +242,6 @@ public class MainActivity extends SyncthingActivity
|
|||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
if (isServiceActive) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getResources().getString(R.string.folders_fragment_title);
|
||||
|
@ -262,14 +252,6 @@ public class MainActivity extends SyncthingActivity
|
|||
default:
|
||||
return String.valueOf(position);
|
||||
}
|
||||
} else {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getResources().getString(R.string.status_fragment_title);
|
||||
default:
|
||||
return String.valueOf(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
|
@ -316,9 +298,9 @@ public class MainActivity extends SyncthingActivity
|
|||
SyncthingService mSyncthingService = getService();
|
||||
if (mSyncthingService != null) {
|
||||
mSyncthingService.unregisterOnServiceStateChangeListener(this);
|
||||
mSyncthingService.unregisterOnServiceStateChangeListener(mDrawerFragment);
|
||||
mSyncthingService.unregisterOnServiceStateChangeListener(mFolderListFragment);
|
||||
mSyncthingService.unregisterOnServiceStateChangeListener(mDeviceListFragment);
|
||||
mSyncthingService.unregisterOnServiceStateChangeListener(mDrawerFragment);
|
||||
mSyncthingService.unregisterOnServiceStateChangeListener(mStatusFragment);
|
||||
}
|
||||
}
|
||||
|
@ -329,9 +311,9 @@ public class MainActivity extends SyncthingActivity
|
|||
SyncthingServiceBinder syncthingServiceBinder = (SyncthingServiceBinder) iBinder;
|
||||
SyncthingService syncthingService = syncthingServiceBinder.getService();
|
||||
syncthingService.registerOnServiceStateChangeListener(this);
|
||||
syncthingService.registerOnServiceStateChangeListener(mDrawerFragment);
|
||||
syncthingService.registerOnServiceStateChangeListener(mFolderListFragment);
|
||||
syncthingService.registerOnServiceStateChangeListener(mDeviceListFragment);
|
||||
syncthingService.registerOnServiceStateChangeListener(mDrawerFragment);
|
||||
syncthingService.registerOnServiceStateChangeListener(mStatusFragment);
|
||||
}
|
||||
|
||||
|
@ -351,11 +333,9 @@ public class MainActivity extends SyncthingActivity
|
|||
putFragment.accept(mFolderListFragment);
|
||||
putFragment.accept(mDeviceListFragment);
|
||||
putFragment.accept(mStatusFragment);
|
||||
putFragment.accept(mDrawerFragment);
|
||||
|
||||
outState.putInt("currentTab", mViewPager.getCurrentItem());
|
||||
outState.putBoolean(IS_SHOWING_RESTART_DIALOG, mRestartDialog != null && mRestartDialog.isShowing());
|
||||
if(mQrCodeDialog != null && mQrCodeDialog.isShowing()) {
|
||||
if (mQrCodeDialog != null && mQrCodeDialog.isShowing()) {
|
||||
outState.putBoolean(IS_QRCODE_DIALOG_DISPLAYED, true);
|
||||
ImageView qrCode = mQrCodeDialog.findViewById(R.id.qrcode_image_view);
|
||||
TextView deviceID = mQrCodeDialog.findViewById(R.id.device_id);
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.support.v7.widget.SwitchCompat;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
|
@ -23,8 +22,6 @@ import com.google.common.collect.Sets;
|
|||
import com.nutomic.syncthingandroid.R;
|
||||
import com.nutomic.syncthingandroid.SyncthingApp;
|
||||
import com.nutomic.syncthingandroid.service.Constants;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingServiceBinder;
|
||||
import com.nutomic.syncthingandroid.util.Util;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
@ -41,8 +38,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
|||
/**
|
||||
* Activity that allows selecting a directory in the local file system.
|
||||
*/
|
||||
public class SyncConditionsActivity extends SyncthingActivity
|
||||
implements SyncthingService.OnServiceStateChangeListener {
|
||||
public class SyncConditionsActivity extends SyncthingActivity {
|
||||
|
||||
private static final String TAG = "SyncConditionsActivity";
|
||||
|
||||
|
@ -215,20 +211,9 @@ public class SyncConditionsActivity extends SyncthingActivity
|
|||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
|
||||
super.onServiceConnected(componentName, iBinder);
|
||||
SyncthingServiceBinder syncthingServiceBinder = (SyncthingServiceBinder) iBinder;
|
||||
syncthingServiceBinder.getService().registerOnServiceStateChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
SyncthingService syncthingService = getService();
|
||||
if (syncthingService != null) {
|
||||
syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -284,12 +269,4 @@ public class SyncConditionsActivity extends SyncthingActivity
|
|||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceStateChange(SyncthingService.State currentState) {
|
||||
if (!isFinishing() && currentState != SyncthingService.State.ACTIVE) {
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -133,9 +133,10 @@ public class WebGuiActivity extends SyncthingActivity
|
|||
setContentView(R.layout.activity_web_gui);
|
||||
|
||||
mLoadingView = findViewById(R.id.loading);
|
||||
try {
|
||||
mConfig = new ConfigXml(this);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
mConfig.loadConfig();
|
||||
} catch (ConfigXml.OpenConfigException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
loadCaCert();
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.nutomic.syncthingandroid.model.Device;
|
|||
import com.nutomic.syncthingandroid.service.Constants;
|
||||
import com.nutomic.syncthingandroid.service.RestApi;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
import com.nutomic.syncthingandroid.util.ConfigXml;
|
||||
import com.nutomic.syncthingandroid.views.DevicesAdapter;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -34,7 +35,14 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
|||
|
||||
private final static String TAG = "DeviceListFragment";
|
||||
|
||||
private final static Comparator<Device> DEVICES_COMPARATOR = (lhs, rhs) -> lhs.name.compareTo(rhs.name);
|
||||
/**
|
||||
* Compares devices by name, uses the device ID as fallback if the name is empty
|
||||
*/
|
||||
private final static Comparator<Device> DEVICES_COMPARATOR = (lhs, rhs) -> {
|
||||
String lhsName = lhs.name != null && !lhs.name.isEmpty() ? lhs.name : lhs.deviceID;
|
||||
String rhsName = rhs.name != null && !rhs.name.isEmpty() ? rhs.name : rhs.deviceID;
|
||||
return lhsName.compareTo(rhsName);
|
||||
};
|
||||
|
||||
private Runnable mUpdateListRunnable = new Runnable() {
|
||||
@Override
|
||||
|
@ -107,9 +115,6 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
|||
* while the user is looking at the current tab.
|
||||
*/
|
||||
private void onTimerEvent() {
|
||||
if (mServiceState != SyncthingService.State.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
MainActivity mainActivity = (MainActivity) getActivity();
|
||||
if (mainActivity == null) {
|
||||
return;
|
||||
|
@ -117,10 +122,6 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
|||
if (mainActivity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
RestApi restApi = mainActivity.getApi();
|
||||
if (restApi == null) {
|
||||
return;
|
||||
}
|
||||
Log.v(TAG, "Invoking updateList on UI thread");
|
||||
mainActivity.runOnUiThread(DeviceListFragment.this::updateList);
|
||||
}
|
||||
|
@ -135,11 +136,19 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
|||
if (activity == null || getView() == null || activity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
List<Device> devices;
|
||||
RestApi restApi = activity.getApi();
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
return;
|
||||
if (restApi == null ||
|
||||
!restApi.isConfigLoaded() ||
|
||||
mServiceState != SyncthingService.State.ACTIVE) {
|
||||
// Syncthing is not running or REST API is not available yet.
|
||||
ConfigXml configXml = new ConfigXml(activity);
|
||||
configXml.loadConfig();
|
||||
devices = configXml.getDevices(false);
|
||||
} else {
|
||||
// Syncthing is running and REST API is available.
|
||||
devices = restApi.getDevices(false);
|
||||
}
|
||||
List<Device> devices = restApi.getDevices(false);
|
||||
if (devices == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -153,7 +162,7 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
|||
mAdapter.clear();
|
||||
Collections.sort(devices, DEVICES_COMPARATOR);
|
||||
mAdapter.addAll(devices);
|
||||
mAdapter.updateConnections(restApi);
|
||||
mAdapter.updateDeviceStatus(restApi);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
setListShown(true);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import com.nutomic.syncthingandroid.model.Folder;
|
|||
import com.nutomic.syncthingandroid.service.Constants;
|
||||
import com.nutomic.syncthingandroid.service.RestApi;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
import com.nutomic.syncthingandroid.util.ConfigXml;
|
||||
import com.nutomic.syncthingandroid.views.FoldersAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -102,9 +103,6 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
|||
* while the user is looking at the current tab.
|
||||
*/
|
||||
private void onTimerEvent() {
|
||||
if (mServiceState != SyncthingService.State.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
MainActivity mainActivity = (MainActivity) getActivity();
|
||||
if (mainActivity == null) {
|
||||
return;
|
||||
|
@ -112,10 +110,6 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
|||
if (mainActivity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
RestApi restApi = mainActivity.getApi();
|
||||
if (restApi == null) {
|
||||
return;
|
||||
}
|
||||
Log.v(TAG, "Invoking updateList on UI thread");
|
||||
mainActivity.runOnUiThread(FolderListFragment.this::updateList);
|
||||
}
|
||||
|
@ -130,11 +124,19 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
|||
if (activity == null || getView() == null || activity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
List<Folder> folders;
|
||||
RestApi restApi = activity.getApi();
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
return;
|
||||
if (restApi == null ||
|
||||
!restApi.isConfigLoaded() ||
|
||||
mServiceState != SyncthingService.State.ACTIVE) {
|
||||
// Syncthing is not running or REST API is not available yet.
|
||||
ConfigXml configXml = new ConfigXml(activity);
|
||||
configXml.loadConfig();
|
||||
folders = configXml.getFolders();
|
||||
} else {
|
||||
// Syncthing is running and REST API is available.
|
||||
folders = restApi.getFolders();
|
||||
}
|
||||
List<Folder> folders = restApi.getFolders();
|
||||
if (folders == null) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -260,7 +260,7 @@ public class StatusFragment extends ListFragment implements SyncthingService.OnS
|
|||
int announceConnected =
|
||||
announceTotal - Optional.fromNullable(systemStatus.discoveryErrors).transform(Map::size).or(0);
|
||||
synchronized (mStatusHolderLock) {
|
||||
mCpuUsage = (systemStatus.cpuPercent / 100 < 1) ? "" : percentFormat.format(systemStatus.cpuPercent / 100);
|
||||
mCpuUsage = (systemStatus.cpuPercent < 5) ? "" : percentFormat.format(systemStatus.cpuPercent / 100);
|
||||
mRamUsage = Util.readableFileSize(mActivity, systemStatus.sys);
|
||||
mAnnounceServer = (announceTotal == 0) ?
|
||||
"" :
|
||||
|
|
|
@ -38,6 +38,8 @@ public abstract class ApiRequest {
|
|||
|
||||
private static final String TAG = "ApiRequest";
|
||||
|
||||
private static final Boolean ENABLE_VERBOSE_LOG = false;
|
||||
|
||||
/**
|
||||
* The name of the HTTP header used for the syncthing API key.
|
||||
*/
|
||||
|
@ -92,7 +94,9 @@ public abstract class ApiRequest {
|
|||
*/
|
||||
void connect(int requestMethod, Uri uri, @Nullable String requestBody,
|
||||
@Nullable OnSuccessListener listener, @Nullable OnErrorListener errorListener) {
|
||||
if (ENABLE_VERBOSE_LOG) {
|
||||
Log.v(TAG, "Performing request to " + uri.toString());
|
||||
}
|
||||
StringRequest request = new StringRequest(requestMethod, uri.toString(), reply -> {
|
||||
if (listener != null) {
|
||||
listener.onSuccess(reply);
|
||||
|
|
|
@ -10,17 +10,29 @@ public class Device {
|
|||
public List<String> addresses;
|
||||
public String compression;
|
||||
public String certName;
|
||||
public boolean introducer;
|
||||
public String introducedBy = "";
|
||||
public boolean introducer = false;
|
||||
public boolean paused;
|
||||
public List<PendingFolder> pendingFolders;
|
||||
public List<IgnoredFolder> ignoredFolders;
|
||||
|
||||
/**
|
||||
* Relevant fields for Folder.List<Device> "shared-with-device" model,
|
||||
* handled by {@link ConfigRouter#updateFolder and ConfigXml#updateFolder}
|
||||
* deviceID
|
||||
* introducedBy
|
||||
* Example Tag
|
||||
* <folder ...>
|
||||
* <device id="[DEVICE_SHARING_THAT_FOLDER_WITH_US]" introducedBy="[INTRODUCER_DEVICE_THAT_TOLD_US_ABOUT_THE_FOLDER_OR_EMPTY_STRING]"></device>
|
||||
* </folder>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the device name, or the first characters of the ID if the name is empty.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return (TextUtils.isEmpty(name))
|
||||
? deviceID.substring(0, 7)
|
||||
? (TextUtils.isEmpty(deviceID) ? "" : deviceID.substring(0, 7))
|
||||
: name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,15 +21,15 @@ public class Folder {
|
|||
public boolean fsWatcherEnabled = true;
|
||||
public int fsWatcherDelayS = 10;
|
||||
private List<Device> devices = new ArrayList<>();
|
||||
public int rescanIntervalS;
|
||||
public final boolean ignorePerms = true;
|
||||
public int rescanIntervalS = 3600;
|
||||
public boolean ignorePerms = true;
|
||||
public boolean autoNormalize = true;
|
||||
public MinDiskFree minDiskFree;
|
||||
public Versioning versioning;
|
||||
public int copiers;
|
||||
public int copiers = 0;
|
||||
public int pullerMaxPendingKiB;
|
||||
public int hashers;
|
||||
public String order;
|
||||
public int hashers = 0;
|
||||
public String order = "random";
|
||||
public boolean ignoreDelete;
|
||||
public int scanProgressIntervalS;
|
||||
public int pullerPauseS;
|
||||
|
@ -52,12 +52,17 @@ public class Folder {
|
|||
public String unit;
|
||||
}
|
||||
|
||||
public void addDevice(String deviceId) {
|
||||
public void addDevice(final Device device) {
|
||||
Device d = new Device();
|
||||
d.deviceID = deviceId;
|
||||
d.deviceID = device.deviceID;
|
||||
d.introducedBy = device.introducedBy;
|
||||
devices.add(d);
|
||||
}
|
||||
|
||||
public List<Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public Device getDevice(String deviceId) {
|
||||
for (Device d : devices) {
|
||||
if (d.deviceID.equals(deviceId)) {
|
||||
|
@ -78,6 +83,8 @@ public class Folder {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return !TextUtils.isEmpty(label) ? label : id;
|
||||
return (TextUtils.isEmpty(label))
|
||||
? (TextUtils.isEmpty(id) ? "" : id)
|
||||
: label;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ public class Options {
|
|||
public String[] alwaysLocalNets;
|
||||
public boolean overwriteRemoteDeviceNamesOnConnect;
|
||||
public int tempIndexMinBlocks;
|
||||
public String defaultFolderPath;
|
||||
|
||||
public static final int USAGE_REPORTING_UNDECIDED = 0;
|
||||
public static final int USAGE_REPORTING_DENIED = -1;
|
||||
|
|
|
@ -68,6 +68,7 @@ public class Constants {
|
|||
* Cached information which is not available on SettingsActivity.
|
||||
*/
|
||||
public static final String PREF_LAST_BINARY_VERSION = "lastBinaryVersion";
|
||||
public static final String PREF_LOCAL_DEVICE_ID = "localDeviceID";
|
||||
|
||||
/**
|
||||
* {@link EventProcessor}
|
||||
|
|
|
@ -71,7 +71,6 @@ public class RestApi {
|
|||
private final static Comparator<Folder> FOLDERS_COMPARATOR = (lhs, rhs) -> {
|
||||
String lhsLabel = lhs.label != null && !lhs.label.isEmpty() ? lhs.label : lhs.id;
|
||||
String rhsLabel = rhs.label != null && !rhs.label.isEmpty() ? rhs.label : rhs.id;
|
||||
|
||||
return lhsLabel.compareTo(rhsLabel);
|
||||
};
|
||||
|
||||
|
@ -453,7 +452,7 @@ public class RestApi {
|
|||
/**
|
||||
* This is only used for new folder creation, see {@link FolderActivity}.
|
||||
*/
|
||||
public void createFolder(Folder folder) {
|
||||
public void addFolder(Folder folder) {
|
||||
synchronized (mConfigLock) {
|
||||
// Add the new folder to the model.
|
||||
mConfig.folders.add(folder);
|
||||
|
@ -500,7 +499,7 @@ public class RestApi {
|
|||
*
|
||||
* @param includeLocal True if the local device should be included in the result.
|
||||
*/
|
||||
public List<Device> getDevices(boolean includeLocal) {
|
||||
public List<Device> getDevices(Boolean includeLocal) {
|
||||
List<Device> devices;
|
||||
synchronized (mConfigLock) {
|
||||
devices = deepCopy(mConfig.devices, new TypeToken<List<Device>>(){}.getType());
|
||||
|
@ -850,6 +849,7 @@ public class RestApi {
|
|||
Log.v(TAG, "onSyncPreconditionChanged: syncFolder(" + folder.id + ")=" + (syncConditionsMet ? "1" : "0"));
|
||||
if (folder.paused != !syncConditionsMet) {
|
||||
folder.paused = !syncConditionsMet;
|
||||
Log.d(TAG, "onSyncPreconditionChanged: syncFolder(" + folder.id + ")=" + (syncConditionsMet ? ">1" : ">0"));
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
|
@ -872,6 +872,7 @@ public class RestApi {
|
|||
Log.v(TAG, "onSyncPreconditionChanged: syncDevice(" + device.deviceID + ")=" + (syncConditionsMet ? "1" : "0"));
|
||||
if (device.paused != !syncConditionsMet) {
|
||||
device.paused = !syncConditionsMet;
|
||||
Log.d(TAG, "onSyncPreconditionChanged: syncDevice(" + device.deviceID + ")=" + (syncConditionsMet ? ">1" : ">0"));
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ public class RunConditionMonitor {
|
|||
}
|
||||
|
||||
public interface OnSyncPreconditionChangedListener {
|
||||
void onSyncPreconditionChanged();
|
||||
void onSyncPreconditionChanged(RunConditionMonitor runConditionMonitor);
|
||||
}
|
||||
|
||||
private class SyncConditionResult {
|
||||
|
@ -189,7 +189,7 @@ public class RunConditionMonitor {
|
|||
|
||||
// Notify about changed preconditions.
|
||||
if (mOnSyncPreconditionChangedListener != null) {
|
||||
mOnSyncPreconditionChangedListener.onSyncPreconditionChanged();
|
||||
mOnSyncPreconditionChangedListener.onSyncPreconditionChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@ import javax.inject.Inject;
|
|||
|
||||
import eu.chainfire.libsuperuser.Shell;
|
||||
|
||||
import static com.nutomic.syncthingandroid.service.SyncthingService.EXTRA_STOP_AFTER_CRASHED_NATIVE;
|
||||
|
||||
/**
|
||||
* Runs the syncthing binary from command line, and prints its output to logcat.
|
||||
*
|
||||
|
@ -258,8 +260,10 @@ public class SyncthingRunnable implements Runnable {
|
|||
|
||||
// Notify {@link SyncthingService} that service state State.ACTIVE is no longer valid.
|
||||
if (!returnStdOut && sendStopToService) {
|
||||
mContext.startService(new Intent(mContext, SyncthingService.class)
|
||||
.setAction(SyncthingService.ACTION_STOP));
|
||||
Intent intent = new Intent(mContext, SyncthingService.class);
|
||||
intent.setAction(SyncthingService.ACTION_STOP);
|
||||
intent.putExtra(EXTRA_STOP_AFTER_CRASHED_NATIVE, true);
|
||||
mContext.startService(intent);
|
||||
}
|
||||
|
||||
// Return captured command line output.
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.Manifest;
|
|||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
@ -20,6 +21,7 @@ import com.google.common.io.Files;
|
|||
import com.nutomic.syncthingandroid.R;
|
||||
import com.nutomic.syncthingandroid.SyncthingApp;
|
||||
import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask;
|
||||
import com.nutomic.syncthingandroid.model.Device;
|
||||
import com.nutomic.syncthingandroid.model.Folder;
|
||||
import com.nutomic.syncthingandroid.util.ConfigXml;
|
||||
import com.nutomic.syncthingandroid.util.FileUtils;
|
||||
|
@ -37,6 +39,7 @@ import java.nio.file.Paths;
|
|||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -112,6 +115,13 @@ public class SyncthingService extends Service {
|
|||
public static final String EXTRA_FOLDER_ID =
|
||||
"com.nutomic.syncthingandroid.service.SyncthingService.EXTRA_FOLDER_ID";
|
||||
|
||||
/**
|
||||
* Extra used together with ACTION_STOP.
|
||||
*/
|
||||
public static final String EXTRA_STOP_AFTER_CRASHED_NATIVE =
|
||||
"com.nutomic.syncthingandroid.service.SyncthingService.EXTRA_STOP_AFTER_CRASHED_NATIVE";
|
||||
|
||||
|
||||
public interface OnSyncthingKilled {
|
||||
void onKilled();
|
||||
}
|
||||
|
@ -155,7 +165,6 @@ public class SyncthingService extends Service {
|
|||
*/
|
||||
private State mCurrentState = State.DISABLED;
|
||||
private ConfigXml mConfig;
|
||||
private StartupTask mStartupTask = null;
|
||||
private Thread mSyncthingRunnableThread = null;
|
||||
private Handler mHandler;
|
||||
|
||||
|
@ -276,9 +285,23 @@ public class SyncthingService extends Service {
|
|||
|
||||
if (ACTION_RESTART.equals(intent.getAction()) && mCurrentState == State.ACTIVE) {
|
||||
shutdown(State.INIT, () -> launchStartupTask(SyncthingRunnable.Command.main));
|
||||
} else if (ACTION_STOP.equals(intent.getAction()) && mCurrentState == State.ACTIVE) {
|
||||
shutdown(State.DISABLED, () -> {
|
||||
});
|
||||
} else if (ACTION_STOP.equals(intent.getAction())) {
|
||||
if (intent.getBooleanExtra(EXTRA_STOP_AFTER_CRASHED_NATIVE, false)) {
|
||||
/**
|
||||
* We were requested to stop the service because the syncthing native binary crashed.
|
||||
* Changing mCurrentState prevents the "defer until syncthing is started" routine we normally
|
||||
* use for clean shutdown to take place. Instead, we will immediately shutdown the crashed
|
||||
* instance forcefully.
|
||||
*/
|
||||
mCurrentState = State.ERROR;
|
||||
shutdown(State.DISABLED, () -> {});
|
||||
} else {
|
||||
// Graceful shutdown.
|
||||
if (mCurrentState == State.STARTING ||
|
||||
mCurrentState == State.ACTIVE) {
|
||||
shutdown(State.DISABLED, () -> {});
|
||||
}
|
||||
}
|
||||
} else if (ACTION_RESET_DATABASE.equals(intent.getAction())) {
|
||||
Log.i(TAG, "Invoking reset of database");
|
||||
shutdown(State.INIT, () -> {
|
||||
|
@ -348,10 +371,84 @@ public class SyncthingService extends Service {
|
|||
* After sync preconditions changed, we need to inform {@link RestApi} to pause or
|
||||
* unpause devices and folders as defined in per-object sync preferences.
|
||||
*/
|
||||
private void onSyncPreconditionChanged() {
|
||||
if (mRestApi != null) {
|
||||
// Forward event.
|
||||
mRestApi.onSyncPreconditionChanged(mRunConditionMonitor);
|
||||
private void onSyncPreconditionChanged(RunConditionMonitor runConditionMonitor) {
|
||||
synchronized (mStateLock) {
|
||||
if (mRestApi != null && mCurrentState == State.ACTIVE) {
|
||||
// Forward event because syncthing is running.
|
||||
mRestApi.onSyncPreconditionChanged(runConditionMonitor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.v(TAG, "onSyncPreconditionChanged: Event fired while syncthing is not running.");
|
||||
Boolean configChanged = false;
|
||||
ConfigXml configXml;
|
||||
|
||||
// Read and parse the config from disk.
|
||||
configXml = new ConfigXml(this);
|
||||
try {
|
||||
configXml.loadConfig();
|
||||
} catch (ConfigXml.OpenConfigException e) {
|
||||
mNotificationHandler.showCrashedNotification(R.string.config_read_failed, "onSyncPreconditionChanged:ConfigXml.OpenConfigException");
|
||||
synchronized (mStateLock) {
|
||||
onServiceStateChange(State.ERROR);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the folders are available from config.
|
||||
List<Folder> folders = configXml.getFolders();
|
||||
if (folders != null) {
|
||||
for (Folder folder : folders) {
|
||||
// Log.v(TAG, "onSyncPreconditionChanged: Processing config of folder.id=" + folder.id);
|
||||
Boolean folderCustomSyncConditionsEnabled = mPreferences.getBoolean(
|
||||
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_FOLDER + folder.id), false
|
||||
);
|
||||
if (folderCustomSyncConditionsEnabled) {
|
||||
Boolean syncConditionsMet = runConditionMonitor.checkObjectSyncConditions(
|
||||
Constants.PREF_OBJECT_PREFIX_FOLDER + folder.id
|
||||
);
|
||||
Log.v(TAG, "onSyncPreconditionChanged: syncFolder(" + folder.id + ")=" + (syncConditionsMet ? "1" : "0"));
|
||||
if (folder.paused != !syncConditionsMet) {
|
||||
configXml.setFolderPause(folder.id, !syncConditionsMet);
|
||||
Log.d(TAG, "onSyncPreconditionChanged: syncFolder(" + folder.id + ")=" + (syncConditionsMet ? ">1" : ">0"));
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "onSyncPreconditionChanged: folders == null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the devices are available from config.
|
||||
List<Device> devices = configXml.getDevices(false);
|
||||
if (devices != null) {
|
||||
for (Device device : devices) {
|
||||
// Log.v(TAG, "onSyncPreconditionChanged: Processing config of device.id=" + device.deviceID);
|
||||
Boolean deviceCustomSyncConditionsEnabled = mPreferences.getBoolean(
|
||||
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_DEVICE + device.deviceID), false
|
||||
);
|
||||
if (deviceCustomSyncConditionsEnabled) {
|
||||
Boolean syncConditionsMet = runConditionMonitor.checkObjectSyncConditions(
|
||||
Constants.PREF_OBJECT_PREFIX_DEVICE + device.deviceID
|
||||
);
|
||||
Log.v(TAG, "onSyncPreconditionChanged: syncDevice(" + device.deviceID + ")=" + (syncConditionsMet ? "1" : "0"));
|
||||
if (device.paused != !syncConditionsMet) {
|
||||
configXml.setDevicePause(device.deviceID, !syncConditionsMet);
|
||||
Log.d(TAG, "onSyncPreconditionChanged: syncDevice(" + device.deviceID + ")=" + (syncConditionsMet ? ">1" : ">0"));
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "onSyncPreconditionChanged: devices == null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
Log.v(TAG, "onSyncPreconditionChanged: Saving changed config to disk ...");
|
||||
configXml.saveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,77 +456,25 @@ public class SyncthingService extends Service {
|
|||
* Prepares to launch the syncthing binary.
|
||||
*/
|
||||
private void launchStartupTask(SyncthingRunnable.Command srCommand) {
|
||||
Log.v(TAG, "Starting syncthing");
|
||||
synchronized (mStateLock) {
|
||||
if (mCurrentState != State.DISABLED && mCurrentState != State.INIT) {
|
||||
Log.e(TAG, "launchStartupTask: Wrong state " + mCurrentState + " detected. Cancelling.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: Log warning if a previously launched startup task did not finish properly.
|
||||
if (mStartupTask != null && (mStartupTask.getStatus() == AsyncTask.Status.RUNNING)) {
|
||||
Log.w(TAG, "launchStartupTask: StartupTask is still running. Skipped starting it twice.");
|
||||
Log.v(TAG, "Starting syncthing");
|
||||
onServiceStateChange(State.STARTING);
|
||||
mConfig = new ConfigXml(this);
|
||||
try {
|
||||
mConfig.loadConfig();
|
||||
} catch (ConfigXml.OpenConfigException e) {
|
||||
mNotificationHandler.showCrashedNotification(R.string.config_read_failed, "ConfigXml.OpenConfigException");
|
||||
synchronized (mStateLock) {
|
||||
onServiceStateChange(State.ERROR);
|
||||
}
|
||||
return;
|
||||
}
|
||||
onServiceStateChange(State.STARTING);
|
||||
mStartupTask = new StartupTask(this, srCommand);
|
||||
mStartupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the initial configuration, and updates the config when coming from an old
|
||||
* version.
|
||||
*/
|
||||
private static class StartupTask extends AsyncTask<Void, Void, Void> {
|
||||
private WeakReference<SyncthingService> refSyncthingService;
|
||||
private SyncthingRunnable.Command srCommand;
|
||||
|
||||
StartupTask(SyncthingService context, SyncthingRunnable.Command srCommand) {
|
||||
refSyncthingService = new WeakReference<>(context);
|
||||
this.srCommand = srCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
SyncthingService syncthingService = refSyncthingService.get();
|
||||
if (syncthingService == null) {
|
||||
cancel(true);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
syncthingService.mConfig = new ConfigXml(syncthingService);
|
||||
syncthingService.mConfig.updateIfNeeded();
|
||||
} catch (SyncthingRunnable.ExecutableNotFoundException e) {
|
||||
syncthingService.mNotificationHandler.showCrashedNotification(R.string.config_read_failed, "SycnthingRunnable.ExecutableNotFoundException");
|
||||
synchronized (syncthingService.mStateLock) {
|
||||
syncthingService.onServiceStateChange(State.ERROR);
|
||||
}
|
||||
cancel(true);
|
||||
} catch (ConfigXml.OpenConfigException e) {
|
||||
syncthingService.mNotificationHandler.showCrashedNotification(R.string.config_read_failed, "ConfigXml.OpenConfigException");
|
||||
synchronized (syncthingService.mStateLock) {
|
||||
syncthingService.onServiceStateChange(State.ERROR);
|
||||
}
|
||||
cancel(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
// Get a reference to the service if it is still there.
|
||||
SyncthingService syncthingService = refSyncthingService.get();
|
||||
if (syncthingService != null) {
|
||||
syncthingService.onStartupTaskCompleteListener(srCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback on {@link StartupTask#onPostExecute}.
|
||||
*/
|
||||
private void onStartupTaskCompleteListener(SyncthingRunnable.Command srCommand) {
|
||||
if (mRestApi == null) {
|
||||
mRestApi = new RestApi(this, mConfig.getWebGuiUrl(), mConfig.getApiKey(),
|
||||
this::onApiAvailable, () -> onServiceStateChange(mCurrentState));
|
||||
|
@ -652,13 +697,13 @@ public class SyncthingService extends Service {
|
|||
mCurrentState = newState;
|
||||
mHandler.post(() -> {
|
||||
mNotificationHandler.updatePersistentNotification(this);
|
||||
for (Iterator<OnServiceStateChangeListener> i = mOnServiceStateChangeListeners.iterator();
|
||||
i.hasNext(); ) {
|
||||
OnServiceStateChangeListener listener = i.next();
|
||||
Iterator<OnServiceStateChangeListener> it = mOnServiceStateChangeListeners.iterator();
|
||||
while (it.hasNext()) {
|
||||
OnServiceStateChangeListener listener = it.next();
|
||||
if (listener != null) {
|
||||
listener.onServiceStateChange(mCurrentState);
|
||||
} else {
|
||||
i.remove();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -773,8 +818,15 @@ public class SyncthingService extends Service {
|
|||
|
||||
// Start syncthing after export if run conditions apply.
|
||||
if (mLastDeterminedShouldRun) {
|
||||
Handler mainLooper = new Handler(Looper.getMainLooper());
|
||||
Runnable launchStartupTaskRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
launchStartupTask(SyncthingRunnable.Command.main);
|
||||
}
|
||||
};
|
||||
mainLooper.post(launchStartupTaskRunnable);
|
||||
}
|
||||
return failSuccess;
|
||||
}
|
||||
|
||||
|
@ -844,6 +896,7 @@ public class SyncthingService extends Service {
|
|||
case Constants.PREF_DEBUG_FACILITIES_AVAILABLE:
|
||||
case Constants.PREF_EVENT_PROCESSOR_LAST_SYNC_ID:
|
||||
case Constants.PREF_LAST_BINARY_VERSION:
|
||||
case Constants.PREF_LOCAL_DEVICE_ID:
|
||||
Log.v(TAG, "importConfig: Ignoring cache pref \"" + prefKey + "\".");
|
||||
break;
|
||||
default:
|
||||
|
@ -929,8 +982,15 @@ public class SyncthingService extends Service {
|
|||
|
||||
// Start syncthing after import if run conditions apply.
|
||||
if (mLastDeterminedShouldRun) {
|
||||
Handler mainLooper = new Handler(Looper.getMainLooper());
|
||||
Runnable launchStartupTaskRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
launchStartupTask(SyncthingRunnable.Command.main);
|
||||
}
|
||||
};
|
||||
mainLooper.post(launchStartupTaskRunnable);
|
||||
}
|
||||
return failSuccess;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
package com.nutomic.syncthingandroid.util;
|
||||
|
||||
import android.content.Context;
|
||||
// import android.util.Log;
|
||||
|
||||
import com.nutomic.syncthingandroid.model.Device;
|
||||
import com.nutomic.syncthingandroid.model.Folder;
|
||||
import com.nutomic.syncthingandroid.model.FolderIgnoreList;
|
||||
import com.nutomic.syncthingandroid.service.RestApi;
|
||||
import com.nutomic.syncthingandroid.util.ConfigXml;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides a transparent access to the config if ...
|
||||
* a) Syncthing is running and REST API is available.
|
||||
* b) Syncthing is NOT running and config.xml is accessed.
|
||||
*/
|
||||
public class ConfigRouter {
|
||||
|
||||
private static final String TAG = "ConfigRouter";
|
||||
|
||||
public interface OnResultListener1<T> {
|
||||
void onResult(T t);
|
||||
}
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private ConfigXml configXml;
|
||||
|
||||
public ConfigRouter(Context context) {
|
||||
mContext = context;
|
||||
configXml = new ConfigXml(mContext);
|
||||
}
|
||||
|
||||
public List<Folder> getFolders(RestApi restApi) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
return configXml.getFolders();
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
return restApi.getFolders();
|
||||
}
|
||||
|
||||
public void addFolder(RestApi restApi, Folder folder) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.addFolder(folder);
|
||||
configXml.saveChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.addFolder(folder); // This will send the config afterwards.
|
||||
}
|
||||
|
||||
public void updateFolder(RestApi restApi, final Folder folder) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.updateFolder(folder);
|
||||
configXml.saveChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.updateFolder(folder); // This will send the config afterwards.
|
||||
}
|
||||
|
||||
public void removeFolder(RestApi restApi, final String folderId) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.removeFolder(folderId);
|
||||
configXml.saveChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.removeFolder(folderId); // This will send the config afterwards.
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets ignore list for given folder.
|
||||
*/
|
||||
public void getFolderIgnoreList(RestApi restApi, Folder folder, OnResultListener1<FolderIgnoreList> listener) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.getFolderIgnoreList(folder, folderIgnoreList -> listener.onResult(folderIgnoreList));
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.getFolderIgnoreList(folder.id, folderIgnoreList -> listener.onResult(folderIgnoreList));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores ignore list for given folder.
|
||||
*/
|
||||
public void postFolderIgnoreList(RestApi restApi, Folder folder, String[] ignore) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.postFolderIgnoreList(folder, ignore);
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.postFolderIgnoreList(folder.id, ignore);
|
||||
}
|
||||
|
||||
public List<Device> getDevices(RestApi restApi, Boolean includeLocal) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
return configXml.getDevices(includeLocal);
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
return restApi.getDevices(includeLocal);
|
||||
}
|
||||
|
||||
public void addDevice(RestApi restApi, Device device, OnResultListener1<String> errorListener) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.addDevice(device, error -> errorListener.onResult(error));
|
||||
configXml.saveChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.addDevice(device, error -> errorListener.onResult(error)); // This will send the config afterwards.
|
||||
}
|
||||
|
||||
public void updateDevice(RestApi restApi, final Device device) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.updateDevice(device);
|
||||
configXml.saveChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.updateDevice(device); // This will send the config afterwards.
|
||||
}
|
||||
|
||||
public void removeDevice(RestApi restApi, final String deviceID) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running or REST API is not (yet) available.
|
||||
configXml.loadConfig();
|
||||
configXml.removeDevice(deviceID);
|
||||
configXml.saveChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
// Syncthing is running and REST API is available.
|
||||
restApi.removeDevice(deviceID); // This will send the config afterwards.
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
package com.nutomic.syncthingandroid.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nutomic.syncthingandroid.model.Device;
|
||||
import com.nutomic.syncthingandroid.model.Folder;
|
||||
import com.nutomic.syncthingandroid.model.FolderIgnoreList;
|
||||
import com.nutomic.syncthingandroid.R;
|
||||
import com.nutomic.syncthingandroid.service.Constants;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingRunnable;
|
||||
|
@ -23,12 +25,16 @@ import java.io.OutputStreamWriter;
|
|||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
@ -53,42 +59,72 @@ import org.xml.sax.InputSource;
|
|||
*/
|
||||
public class ConfigXml {
|
||||
|
||||
private static final String TAG = "ConfigXml";
|
||||
|
||||
private static final Boolean ENABLE_VERBOSE_LOG = false;
|
||||
|
||||
public class OpenConfigException extends RuntimeException {
|
||||
}
|
||||
|
||||
private static final String TAG = "ConfigXml";
|
||||
/**
|
||||
* Compares devices by name, uses the device ID as fallback if the name is empty
|
||||
*/
|
||||
private final static Comparator<Device> DEVICES_COMPARATOR = (lhs, rhs) -> {
|
||||
String lhsName = lhs.name != null && !lhs.name.isEmpty() ? lhs.name : lhs.deviceID;
|
||||
String rhsName = rhs.name != null && !rhs.name.isEmpty() ? rhs.name : rhs.deviceID;
|
||||
return lhsName.compareTo(rhsName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares folders by labels, uses the folder ID as fallback if the label is empty
|
||||
*/
|
||||
private final static Comparator<Folder> FOLDERS_COMPARATOR = (lhs, rhs) -> {
|
||||
String lhsLabel = lhs.label != null && !lhs.label.isEmpty() ? lhs.label : lhs.id;
|
||||
String rhsLabel = rhs.label != null && !rhs.label.isEmpty() ? rhs.label : rhs.id;
|
||||
return lhsLabel.compareTo(rhsLabel);
|
||||
};
|
||||
|
||||
public interface OnResultListener1<T> {
|
||||
void onResult(T t);
|
||||
}
|
||||
|
||||
private static final int FOLDER_ID_APPENDIX_LENGTH = 4;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
@Inject
|
||||
SharedPreferences mPreferences;
|
||||
|
||||
private final File mConfigFile;
|
||||
|
||||
private Document mConfig;
|
||||
|
||||
public ConfigXml(Context context) throws OpenConfigException, SyncthingRunnable.ExecutableNotFoundException {
|
||||
public ConfigXml(Context context) {
|
||||
mContext = context;
|
||||
mConfigFile = Constants.getConfigFile(mContext);
|
||||
boolean isFirstStart = !mConfigFile.exists();
|
||||
if (isFirstStart) {
|
||||
Log.i(TAG, "App started for the first time. Generating keys and config.");
|
||||
new SyncthingRunnable(context, SyncthingRunnable.Command.generate).run(true);
|
||||
}
|
||||
|
||||
readConfig();
|
||||
public void loadConfig() throws OpenConfigException {
|
||||
parseConfig();
|
||||
updateIfNeeded();
|
||||
}
|
||||
|
||||
if (isFirstStart) {
|
||||
boolean changed = false;
|
||||
/**
|
||||
* This should run within an AsyncTask as it can cause a full CPU load
|
||||
* for more than 30 seconds on older phone hardware.
|
||||
*/
|
||||
public void generateConfig() throws OpenConfigException, SyncthingRunnable.ExecutableNotFoundException {
|
||||
// Create new secret keys and config.
|
||||
Log.i(TAG, "(Re)Generating keys and config.");
|
||||
new SyncthingRunnable(mContext, SyncthingRunnable.Command.generate).run(true);
|
||||
parseConfig();
|
||||
Boolean changed = false;
|
||||
|
||||
// Set local device name.
|
||||
Log.i(TAG, "Starting syncthing to retrieve local device id.");
|
||||
String logOutput = new SyncthingRunnable(context, SyncthingRunnable.Command.deviceid).run(true);
|
||||
String localDeviceID = logOutput.replace("\n", "");
|
||||
// Verify local device ID is correctly formatted.
|
||||
if (localDeviceID.matches("^([A-Z0-9]{7}-){7}[A-Z0-9]{7}$")) {
|
||||
String localDeviceID = getLocalDeviceIDandStoreToPref();
|
||||
if (!TextUtils.isEmpty(localDeviceID)) {
|
||||
changed = changeLocalDeviceName(localDeviceID) || changed;
|
||||
}
|
||||
|
||||
// Set default folder to the "camera" folder: path and name
|
||||
changed = changeDefaultFolder() || changed;
|
||||
|
||||
// Save changes if we made any.
|
||||
|
@ -96,10 +132,44 @@ public class ConfigXml {
|
|||
saveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private String getLocalDeviceIDfromPref() {
|
||||
String localDeviceID = PreferenceManager.getDefaultSharedPreferences(mContext).getString(Constants.PREF_LOCAL_DEVICE_ID, "");
|
||||
if (TextUtils.isEmpty(localDeviceID)) {
|
||||
Log.d(TAG, "getLocalDeviceIDfromPref: Local device ID unavailable, trying to retrieve it from syncthing ...");
|
||||
try {
|
||||
localDeviceID = getLocalDeviceIDandStoreToPref();
|
||||
} catch (SyncthingRunnable.ExecutableNotFoundException e) {
|
||||
Log.e(TAG, "getLocalDeviceIDfromPref: Failed to execute syncthing core");
|
||||
}
|
||||
if (TextUtils.isEmpty(localDeviceID)) {
|
||||
Log.e(TAG, "getLocalDeviceIDfromPref: Local device ID unavailable");
|
||||
}
|
||||
}
|
||||
return localDeviceID;
|
||||
}
|
||||
|
||||
private void readConfig() {
|
||||
private String getLocalDeviceIDandStoreToPref() throws SyncthingRunnable.ExecutableNotFoundException {
|
||||
String logOutput = new SyncthingRunnable(mContext, SyncthingRunnable.Command.deviceid).run(true);
|
||||
String localDeviceID = logOutput.replace("\n", "");
|
||||
|
||||
// Verify that local device ID is correctly formatted.
|
||||
if (!isDeviceIdValid(localDeviceID)) {
|
||||
Log.w(TAG, "getLocalDeviceIDandStoreToPref: Syncthing core returned a bad formatted device ID \"" + localDeviceID + "\"");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Store local device ID to pref. This saves us expensive calls to the syncthing binary if we need it later.
|
||||
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
|
||||
.putString(Constants.PREF_LOCAL_DEVICE_ID, localDeviceID)
|
||||
.apply();
|
||||
Log.v(TAG, "getLocalDeviceIDandStoreToPref: Cached local device ID \"" + localDeviceID + "\"");
|
||||
return localDeviceID;
|
||||
}
|
||||
|
||||
private void parseConfig() {
|
||||
if (!mConfigFile.canRead() && !Util.fixAppDataPermissions(mContext)) {
|
||||
Log.w(TAG, "Failed to open config file '" + mConfigFile + "'");
|
||||
throw new OpenConfigException();
|
||||
}
|
||||
try {
|
||||
|
@ -107,11 +177,16 @@ public class ConfigXml {
|
|||
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
|
||||
InputSource inputSource = new InputSource(inputStreamReader);
|
||||
inputSource.setEncoding("UTF-8");
|
||||
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
Log.d(TAG, "Parsing config file '" + mConfigFile + "'");
|
||||
DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = dbfactory.newDocumentBuilder();
|
||||
if (ENABLE_VERBOSE_LOG) {
|
||||
Log.v(TAG, "Parsing config file '" + mConfigFile + "'");
|
||||
}
|
||||
mConfig = db.parse(inputSource);
|
||||
inputStream.close();
|
||||
Log.i(TAG, "Successfully parsed config file");
|
||||
if (ENABLE_VERBOSE_LOG) {
|
||||
Log.v(TAG, "Successfully parsed config file");
|
||||
}
|
||||
} catch (SAXException | ParserConfigurationException | IOException e) {
|
||||
Log.w(TAG, "Failed to parse config file '" + mConfigFile + "'", e);
|
||||
throw new OpenConfigException();
|
||||
|
@ -141,7 +216,7 @@ public class ConfigXml {
|
|||
* username/password, and disables weak hash checking.
|
||||
*/
|
||||
@SuppressWarnings("SdCardPath")
|
||||
public void updateIfNeeded() {
|
||||
private void updateIfNeeded() {
|
||||
boolean changed = false;
|
||||
|
||||
/* Perform one-time migration tasks on syncthing's config file when coming from an older config version. */
|
||||
|
@ -173,7 +248,7 @@ public class ConfigXml {
|
|||
Boolean forceHttps = Constants.osSupportsTLS12();
|
||||
if (!gui.hasAttribute("tls") ||
|
||||
Boolean.parseBoolean(gui.getAttribute("tls")) != forceHttps) {
|
||||
gui.setAttribute("tls", forceHttps ? "true" : "false");
|
||||
gui.setAttribute("tls", Boolean.toString(forceHttps));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
@ -222,6 +297,9 @@ public class ConfigXml {
|
|||
}
|
||||
}
|
||||
|
||||
// Disable "startBrowser" because it applies to desktop environments and cannot start a mobile browser app.
|
||||
changed = setConfigElement(options, "startBrowser", "false") || changed;
|
||||
|
||||
// Save changes if we made any.
|
||||
if (changed) {
|
||||
saveChanges();
|
||||
|
@ -238,7 +316,9 @@ public class ConfigXml {
|
|||
/* Read existing config version */
|
||||
int iConfigVersion = Integer.parseInt(mConfig.getDocumentElement().getAttribute("version"));
|
||||
int iOldConfigVersion = iConfigVersion;
|
||||
Log.i(TAG, "Found existing config version " + Integer.toString(iConfigVersion));
|
||||
if (ENABLE_VERBOSE_LOG) {
|
||||
Log.v(TAG, "Found existing config version " + Integer.toString(iConfigVersion));
|
||||
}
|
||||
|
||||
/* Check if we have to do manual migration from version X to Y */
|
||||
if (iConfigVersion == 27) {
|
||||
|
@ -273,6 +353,438 @@ public class ConfigXml {
|
|||
}
|
||||
}
|
||||
|
||||
private Boolean getAttributeOrDefault(final Element element, String attribute, Boolean defaultValue) {
|
||||
return element.hasAttribute(attribute) ? Boolean.parseBoolean(element.getAttribute(attribute)) : defaultValue;
|
||||
}
|
||||
|
||||
private Integer getAttributeOrDefault(final Element element, String attribute, Integer defaultValue) {
|
||||
return element.hasAttribute(attribute) ? Integer.parseInt(element.getAttribute(attribute)) : defaultValue;
|
||||
}
|
||||
|
||||
private String getAttributeOrDefault(final Element element, String attribute, String defaultValue) {
|
||||
return element.hasAttribute(attribute) ? element.getAttribute(attribute) : defaultValue;
|
||||
}
|
||||
|
||||
private Boolean getContentOrDefault(final Node node, Boolean defaultValue) {
|
||||
return (node == null) ? defaultValue : Boolean.parseBoolean(node.getTextContent());
|
||||
}
|
||||
|
||||
private Integer getContentOrDefault(final Node node, Integer defaultValue) {
|
||||
return (node == null) ? defaultValue : Integer.parseInt(node.getTextContent());
|
||||
}
|
||||
|
||||
private String getContentOrDefault(final Node node, String defaultValue) {
|
||||
return (node == null) ? defaultValue : node.getTextContent();
|
||||
}
|
||||
|
||||
public List<Folder> getFolders() {
|
||||
String localDeviceID = getLocalDeviceIDfromPref();
|
||||
List<Folder> folders = new ArrayList<>();
|
||||
NodeList nodeFolders = mConfig.getDocumentElement().getElementsByTagName("folder");
|
||||
for (int i = 0; i < nodeFolders.getLength(); i++) {
|
||||
Element r = (Element) nodeFolders.item(i);
|
||||
Folder folder = new Folder();
|
||||
folder.id = getAttributeOrDefault(r, "id", "");
|
||||
folder.label = getAttributeOrDefault(r, "label", "");
|
||||
folder.path = getAttributeOrDefault(r, "path", "");
|
||||
folder.type = getAttributeOrDefault(r, "type", Constants.FOLDER_TYPE_SEND_RECEIVE);
|
||||
folder.autoNormalize = getAttributeOrDefault(r, "autoNormalize", true);
|
||||
folder.fsWatcherDelayS =getAttributeOrDefault(r, "fsWatcherDelayS", 10);
|
||||
folder.fsWatcherEnabled = getAttributeOrDefault(r, "fsWatcherEnabled", true);
|
||||
folder.ignorePerms = getAttributeOrDefault(r, "ignorePerms", true);
|
||||
folder.rescanIntervalS = getAttributeOrDefault(r, "rescanIntervalS", 3600);
|
||||
|
||||
folder.copiers = getContentOrDefault(r.getElementsByTagName("copiers").item(0), 0);
|
||||
folder.hashers = getContentOrDefault(r.getElementsByTagName("hashers").item(0), 0);
|
||||
folder.order = getContentOrDefault(r.getElementsByTagName("order").item(0), "random");
|
||||
folder.paused = getContentOrDefault(r.getElementsByTagName("paused").item(0), false);
|
||||
|
||||
// Devices
|
||||
/*
|
||||
<device id="[DEVICE_ID]" introducedBy=""/>
|
||||
*/
|
||||
NodeList nodeDevices = r.getElementsByTagName("device");
|
||||
for (int j = 0; j < nodeDevices.getLength(); j++) {
|
||||
Element elementDevice = (Element) nodeDevices.item(j);
|
||||
Device device = new Device();
|
||||
device.deviceID = getAttributeOrDefault(elementDevice, "id", "");
|
||||
|
||||
// Exclude self.
|
||||
if (!TextUtils.isEmpty(device.deviceID) && !device.deviceID.equals(localDeviceID)) {
|
||||
device.introducedBy = getAttributeOrDefault(elementDevice, "introducedBy", "");
|
||||
// Log.v(TAG, "getFolders: deviceID=" + device.deviceID + ", introducedBy=" + device.introducedBy);
|
||||
folder.addDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
// Versioning
|
||||
/*
|
||||
<versioning></versioning>
|
||||
<versioning type="trashcan">
|
||||
<param key="cleanoutDays" val="90"></param>
|
||||
</versioning>
|
||||
*/
|
||||
folder.versioning = new Folder.Versioning();
|
||||
Element elementVersioning = (Element) r.getElementsByTagName("versioning").item(0);
|
||||
folder.versioning.type = getAttributeOrDefault(elementVersioning, "type", "");
|
||||
NodeList nodeVersioningParam = elementVersioning.getElementsByTagName("param");
|
||||
for (int j = 0; j < nodeVersioningParam.getLength(); j++) {
|
||||
Element elementVersioningParam = (Element) nodeVersioningParam.item(j);
|
||||
folder.versioning.params.put(
|
||||
getAttributeOrDefault(elementVersioningParam, "key", ""),
|
||||
getAttributeOrDefault(elementVersioningParam, "val", "")
|
||||
);
|
||||
/*
|
||||
Log.v(TAG, "folder.versioning.type=" + folder.versioning.type +
|
||||
", key=" + getAttributeOrDefault(elementVersioningParam, "key", "") +
|
||||
", val=" + getAttributeOrDefault(elementVersioningParam, "val", "")
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
// For testing purposes only.
|
||||
// Log.v(TAG, "folder.label=" + folder.label + "/" +"folder.type=" + folder.type + "/" + "folder.paused=" + folder.paused);
|
||||
folders.add(folder);
|
||||
}
|
||||
Collections.sort(folders, FOLDERS_COMPARATOR);
|
||||
return folders;
|
||||
}
|
||||
|
||||
public void addFolder(final Folder folder) {
|
||||
Log.v(TAG, "addFolder: folder.id=" + folder.id);
|
||||
Node nodeConfig = mConfig.getDocumentElement();
|
||||
Node nodeFolder = mConfig.createElement("folder");
|
||||
nodeConfig.appendChild(nodeFolder);
|
||||
Element elementFolder = (Element) nodeFolder;
|
||||
elementFolder.setAttribute("id", folder.id);
|
||||
updateFolder(folder);
|
||||
}
|
||||
|
||||
public void updateFolder(final Folder folder) {
|
||||
String localDeviceID = getLocalDeviceIDfromPref();
|
||||
NodeList nodeFolders = mConfig.getDocumentElement().getElementsByTagName("folder");
|
||||
for (int i = 0; i < nodeFolders.getLength(); i++) {
|
||||
Element r = (Element) nodeFolders.item(i);
|
||||
if (folder.id.equals(getAttributeOrDefault(r, "id", ""))) {
|
||||
// Found folder node to update.
|
||||
r.setAttribute("label", folder.label);
|
||||
r.setAttribute("path", folder.path);
|
||||
r.setAttribute("type", folder.type);
|
||||
r.setAttribute("autoNormalize", Boolean.toString(folder.autoNormalize));
|
||||
r.setAttribute("fsWatcherDelayS", Integer.toString(folder.fsWatcherDelayS));
|
||||
r.setAttribute("fsWatcherEnabled", Boolean.toString(folder.fsWatcherEnabled));
|
||||
r.setAttribute("ignorePerms", Boolean.toString(folder.ignorePerms));
|
||||
r.setAttribute("rescanIntervalS", Integer.toString(folder.rescanIntervalS));
|
||||
|
||||
setConfigElement(r, "copiers", Integer.toString(folder.copiers));
|
||||
setConfigElement(r, "hashers", Integer.toString(folder.hashers));
|
||||
setConfigElement(r, "order", folder.order);
|
||||
setConfigElement(r, "paused", Boolean.toString(folder.paused));
|
||||
|
||||
// Update devices that share this folder.
|
||||
// Pass 1: Remove all devices below that folder in XML except the local device.
|
||||
NodeList nodeDevices = r.getElementsByTagName("device");
|
||||
for (int j = nodeDevices.getLength() - 1; j >= 0; j--) {
|
||||
Element elementDevice = (Element) nodeDevices.item(j);
|
||||
if (!getAttributeOrDefault(elementDevice, "id", "").equals(localDeviceID)) {
|
||||
Log.v(TAG, "updateFolder: nodeDevices: Removing deviceID=" + getAttributeOrDefault(elementDevice, "id", ""));
|
||||
removeChildElementFromTextNode(r, elementDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: Add devices below that folder from the POJO model.
|
||||
final List<Device> devices = folder.getDevices();
|
||||
for (Device device : devices) {
|
||||
Log.v(TAG, "updateFolder: nodeDevices: Adding deviceID=" + device.deviceID);
|
||||
Node nodeDevice = mConfig.createElement("device");
|
||||
r.appendChild(nodeDevice);
|
||||
Element elementDevice = (Element) nodeDevice;
|
||||
elementDevice.setAttribute("id", device.deviceID);
|
||||
elementDevice.setAttribute("introducedBy", device.introducedBy);
|
||||
}
|
||||
|
||||
// Versioning
|
||||
// Pass 1: Remove all versioning nodes in XML (usually one)
|
||||
/*
|
||||
NodeList nlVersioning = r.getElementsByTagName("versioning");
|
||||
for (int j = nlVersioning.getLength() - 1; j >= 0; j--) {
|
||||
Log.v(TAG, "updateFolder: nodeVersioning: Removing versioning node");
|
||||
removeChildElementFromTextNode(r, (Element) nlVersioning.item(j));
|
||||
}
|
||||
*/
|
||||
Element elementVersioning = (Element) r.getElementsByTagName("versioning").item(0);
|
||||
if (elementVersioning != null) {
|
||||
Log.v(TAG, "updateFolder: nodeVersioning: Removing versioning node");
|
||||
removeChildElementFromTextNode(r, elementVersioning);
|
||||
}
|
||||
|
||||
// Pass 2: Add versioning node from the POJO model.
|
||||
Node nodeVersioning = mConfig.createElement("versioning");
|
||||
r.appendChild(nodeVersioning);
|
||||
elementVersioning = (Element) nodeVersioning;
|
||||
if (!TextUtils.isEmpty(folder.versioning.type)) {
|
||||
elementVersioning.setAttribute("type", folder.versioning.type);
|
||||
for (Map.Entry<String, String> param : folder.versioning.params.entrySet()) {
|
||||
Log.v(TAG, "updateFolder: nodeVersioning: Adding param key=" + param.getKey() + ", val=" + param.getValue());
|
||||
Node nodeParam = mConfig.createElement("param");
|
||||
elementVersioning.appendChild(nodeParam);
|
||||
Element elementParam = (Element) nodeParam;
|
||||
elementParam.setAttribute("key", param.getKey());
|
||||
elementParam.setAttribute("val", param.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFolder(String folderId) {
|
||||
NodeList nodeFolders = mConfig.getDocumentElement().getElementsByTagName("folder");
|
||||
for (int i = nodeFolders.getLength() - 1; i >= 0; i--) {
|
||||
Element r = (Element) nodeFolders.item(i);
|
||||
if (folderId.equals(getAttributeOrDefault(r, "id", ""))) {
|
||||
// Found folder node to remove.
|
||||
Log.v(TAG, "removeFolder: Removing folder node, folderId=" + folderId);
|
||||
removeChildElementFromTextNode((Element) r.getParentNode(), r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setFolderPause(String folderId, Boolean paused) {
|
||||
NodeList nodeFolders = mConfig.getDocumentElement().getElementsByTagName("folder");
|
||||
for (int i = 0; i < nodeFolders.getLength(); i++) {
|
||||
Element r = (Element) nodeFolders.item(i);
|
||||
if (getAttributeOrDefault(r, "id", "").equals(folderId))
|
||||
{
|
||||
setConfigElement(r, "paused", Boolean.toString(paused));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets ignore list for given folder.
|
||||
*/
|
||||
public void getFolderIgnoreList(Folder folder, OnResultListener1<FolderIgnoreList> listener) {
|
||||
FolderIgnoreList folderIgnoreList = new FolderIgnoreList();
|
||||
File file;
|
||||
FileInputStream fileInputStream = null;
|
||||
try {
|
||||
file = new File(folder.path, ".stignore");
|
||||
if (file.exists()) {
|
||||
fileInputStream = new FileInputStream(file);
|
||||
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
|
||||
byte[] data = new byte[(int) file.length()];
|
||||
fileInputStream.read(data);
|
||||
folderIgnoreList.ignore = new String(data, "UTF-8").split("\n");
|
||||
} else {
|
||||
// File not found.
|
||||
Log.w(TAG, "getFolderIgnoreList: File missing " + file);
|
||||
/**
|
||||
* Don't fail as the file might be expectedly missing when users didn't
|
||||
* set ignores in the past storyline of that folder.
|
||||
*/
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "getFolderIgnoreList: Failed to read '" + folder.path + "/.stignore' #1", e);
|
||||
} finally {
|
||||
try {
|
||||
if (fileInputStream != null) {
|
||||
fileInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "getFolderIgnoreList: Failed to read '" + folder.path + "/.stignore' #2", e);
|
||||
}
|
||||
}
|
||||
listener.onResult(folderIgnoreList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores ignore list for given folder.
|
||||
*/
|
||||
public void postFolderIgnoreList(Folder folder, String[] ignore) {
|
||||
File file;
|
||||
FileOutputStream fileOutputStream = null;
|
||||
try {
|
||||
file = new File(folder.path, ".stignore");
|
||||
if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
}
|
||||
fileOutputStream = new FileOutputStream(file);
|
||||
// Log.v(TAG, "postFolderIgnoreList: Writing .stignore content=" + TextUtils.join("\n", ignore));
|
||||
fileOutputStream.write(TextUtils.join("\n", ignore).getBytes("UTF-8"));
|
||||
fileOutputStream.flush();
|
||||
} catch (IOException e) {
|
||||
/**
|
||||
* This will happen on external storage folders which exist outside the
|
||||
* "/Android/data/[package_name]/files" folder on Android 5+.
|
||||
*/
|
||||
Log.w(TAG, "postFolderIgnoreList: Failed to write '" + folder.path + "/.stignore' #1", e);
|
||||
} finally {
|
||||
try {
|
||||
if (fileOutputStream != null) {
|
||||
fileOutputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "postFolderIgnoreList: Failed to write '" + folder.path + "/.stignore' #2", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Device> getDevices(Boolean includeLocal) {
|
||||
String localDeviceID = getLocalDeviceIDfromPref();
|
||||
List<Device> devices = new ArrayList<>();
|
||||
|
||||
// Prevent enumerating "<device>" tags below "<folder>" nodes by enumerating child nodes manually.
|
||||
NodeList childNodes = mConfig.getDocumentElement().getChildNodes();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
if (node.getNodeName().equals("device")) {
|
||||
Element r = (Element) node;
|
||||
Device device = new Device();
|
||||
device.compression = getAttributeOrDefault(r, "compression", "metadata");
|
||||
device.deviceID = getAttributeOrDefault(r, "id", "");
|
||||
device.introducedBy = getAttributeOrDefault(r, "introducedBy", "");
|
||||
device.introducer = getAttributeOrDefault(r, "introducer", false);
|
||||
device.name = getAttributeOrDefault(r, "name", "");
|
||||
device.paused = getContentOrDefault(r.getElementsByTagName("paused").item(0), false);
|
||||
|
||||
// Addresses
|
||||
/*
|
||||
<device ...>
|
||||
<address>dynamic</address>
|
||||
<address>tcp4://192.168.1.67:2222</address>
|
||||
</device>
|
||||
*/
|
||||
device.addresses = new ArrayList<>();
|
||||
NodeList nodeAddresses = r.getElementsByTagName("address");
|
||||
for (int j = 0; j < nodeAddresses.getLength(); j++) {
|
||||
String address = getContentOrDefault(nodeAddresses.item(j), "");
|
||||
device.addresses.add(address);
|
||||
// Log.v(TAG, "getDevices: address=" + address);
|
||||
}
|
||||
|
||||
// For testing purposes only.
|
||||
// Log.v(TAG, "getDevices: device.name=" + device.name + "/" +"device.id=" + device.deviceID + "/" + "device.paused=" + device.paused);
|
||||
|
||||
// Exclude self if requested.
|
||||
Boolean isLocalDevice = !TextUtils.isEmpty(device.deviceID) && device.deviceID.equals(localDeviceID);
|
||||
if (includeLocal || !isLocalDevice) {
|
||||
devices.add(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
Collections.sort(devices, DEVICES_COMPARATOR);
|
||||
return devices;
|
||||
}
|
||||
|
||||
public void addDevice(final Device device, OnResultListener1<String> errorListener) {
|
||||
if (!isDeviceIdValid(device.deviceID)) {
|
||||
errorListener.onResult(mContext.getString(R.string.device_id_invalid));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.v(TAG, "addDevice: deviceID=" + device.deviceID);
|
||||
Node nodeConfig = mConfig.getDocumentElement();
|
||||
Node nodeDevice = mConfig.createElement("device");
|
||||
nodeConfig.appendChild(nodeDevice);
|
||||
Element elementDevice = (Element) nodeDevice;
|
||||
elementDevice.setAttribute("id", device.deviceID);
|
||||
updateDevice(device);
|
||||
}
|
||||
|
||||
public void updateDevice(final Device device) {
|
||||
// Prevent enumerating "<device>" tags below "<folder>" nodes by enumerating child nodes manually.
|
||||
NodeList childNodes = mConfig.getDocumentElement().getChildNodes();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
if (node.getNodeName().equals("device")) {
|
||||
Element r = (Element) node;
|
||||
if (device.deviceID.equals(getAttributeOrDefault(r, "id", ""))) {
|
||||
// Found device to update.
|
||||
r.setAttribute("compression", device.compression);
|
||||
r.setAttribute("introducedBy", device.introducedBy);
|
||||
r.setAttribute("introducer", Boolean.toString(device.introducer));
|
||||
r.setAttribute("name", device.name);
|
||||
|
||||
setConfigElement(r, "paused", Boolean.toString(device.paused));
|
||||
|
||||
// Addresses
|
||||
// Pass 1: Remove all addresses in XML.
|
||||
NodeList nodeAddresses = r.getElementsByTagName("address");
|
||||
for (int j = nodeAddresses.getLength() - 1; j >= 0; j--) {
|
||||
Element elementAddress = (Element) nodeAddresses.item(j);
|
||||
Log.v(TAG, "updateDevice: nodeAddresses: Removing address=" + getContentOrDefault(elementAddress, ""));
|
||||
removeChildElementFromTextNode(r, elementAddress);
|
||||
}
|
||||
|
||||
// Pass 2: Add addresses from the POJO model.
|
||||
if (device.addresses != null) {
|
||||
for (String address : device.addresses) {
|
||||
Log.v(TAG, "updateDevice: nodeAddresses: Adding address=" + address);
|
||||
Node nodeAddress = mConfig.createElement("address");
|
||||
r.appendChild(nodeAddress);
|
||||
Element elementAddress = (Element) nodeAddress;
|
||||
elementAddress.setTextContent(address);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeDevice(String deviceID) {
|
||||
// Prevent enumerating "<device>" tags below "<folder>" nodes by enumerating child nodes manually.
|
||||
NodeList childNodes = mConfig.getDocumentElement().getChildNodes();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
if (node.getNodeName().equals("device")) {
|
||||
Element r = (Element) node;
|
||||
if (deviceID.equals(getAttributeOrDefault(r, "id", ""))) {
|
||||
// Found device to remove.
|
||||
Log.v(TAG, "removeDevice: Removing device node, deviceID=" + deviceID);
|
||||
removeChildElementFromTextNode((Element) r.getParentNode(), r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDevicePause(String deviceId, Boolean paused) {
|
||||
// Prevent enumerating "<device>" tags below "<folder>" nodes by enumerating child nodes manually.
|
||||
NodeList childNodes = mConfig.getDocumentElement().getChildNodes();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
if (node.getNodeName().equals("device")) {
|
||||
Element r = (Element) node;
|
||||
if (getAttributeOrDefault(r, "id", "").equals(deviceId))
|
||||
{
|
||||
setConfigElement(r, "paused", Boolean.toString(paused));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If an indented child element is removed, whitespace and line break will be left by
|
||||
* Element.removeChild().
|
||||
* See https://stackoverflow.com/questions/14255064/removechild-how-to-remove-indent-too
|
||||
*/
|
||||
private void removeChildElementFromTextNode(Element parentElement, Element childElement) {
|
||||
Node prev = childElement.getPreviousSibling();
|
||||
if (prev != null &&
|
||||
prev.getNodeType() == Node.TEXT_NODE &&
|
||||
prev.getNodeValue().trim().length() == 0) {
|
||||
parentElement.removeChild(prev);
|
||||
}
|
||||
parentElement.removeChild(childElement);
|
||||
}
|
||||
|
||||
private boolean setConfigElement(Element parent, String tagName, String textContent) {
|
||||
Node element = parent.getElementsByTagName(tagName).item(0);
|
||||
if (element == null) {
|
||||
|
@ -347,16 +859,23 @@ public class ConfigXml {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a syncthing device ID is correctly formatted.
|
||||
*/
|
||||
private Boolean isDeviceIdValid(final String deviceID) {
|
||||
return deviceID.matches("^([A-Z0-9]{7}-){7}[A-Z0-9]{7}$");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes updated mConfig back to file.
|
||||
*/
|
||||
private void saveChanges() {
|
||||
public void saveChanges() {
|
||||
if (!mConfigFile.canWrite() && !Util.fixAppDataPermissions(mContext)) {
|
||||
Log.w(TAG, "Failed to save updated config. Cannot change the owner of the config file.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Writing updated config file");
|
||||
Log.i(TAG, "Saving config file");
|
||||
File mConfigTempFile = Constants.getConfigTempFile(mContext);
|
||||
try {
|
||||
// Write XML header.
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.content.res.Resources;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -16,11 +17,16 @@ import com.nutomic.syncthingandroid.model.Device;
|
|||
import com.nutomic.syncthingandroid.service.RestApi;
|
||||
import com.nutomic.syncthingandroid.util.Util;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
/**
|
||||
* Generates item views for device items.
|
||||
*/
|
||||
public class DevicesAdapter extends ArrayAdapter<Device> {
|
||||
|
||||
private static final String TAG = "DevicesAdapter";
|
||||
|
||||
private Connections mConnections;
|
||||
|
||||
public DevicesAdapter(Context context) {
|
||||
|
@ -36,6 +42,7 @@ public class DevicesAdapter extends ArrayAdapter<Device> {
|
|||
convertView = inflater.inflate(R.layout.item_device_list, parent, false);
|
||||
}
|
||||
|
||||
View rateInOutView = convertView.findViewById(R.id.rateInOutContainer);
|
||||
TextView name = convertView.findViewById(R.id.name);
|
||||
TextView status = convertView.findViewById(R.id.status);
|
||||
TextView download = convertView.findViewById(R.id.download);
|
||||
|
@ -52,16 +59,17 @@ public class DevicesAdapter extends ArrayAdapter<Device> {
|
|||
}
|
||||
|
||||
if (conn == null) {
|
||||
download.setText(Util.readableTransferRate(getContext(), 0));
|
||||
upload.setText(Util.readableTransferRate(getContext(), 0));
|
||||
// Syncthing is not running.
|
||||
rateInOutView.setVisibility(GONE);
|
||||
status.setVisibility(GONE);
|
||||
status.setText(r.getString(R.string.device_state_unknown));
|
||||
status.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red));
|
||||
return convertView;
|
||||
}
|
||||
|
||||
if (conn.paused) {
|
||||
download.setText(Util.readableTransferRate(getContext(), 0));
|
||||
upload.setText(Util.readableTransferRate(getContext(), 0));
|
||||
rateInOutView.setVisibility(GONE);
|
||||
status.setVisibility(VISIBLE);
|
||||
status.setText(r.getString(R.string.device_paused));
|
||||
status.setTextColor(ContextCompat.getColor(getContext(), R.color.text_black));
|
||||
return convertView;
|
||||
|
@ -70,6 +78,8 @@ public class DevicesAdapter extends ArrayAdapter<Device> {
|
|||
if (conn.connected) {
|
||||
download.setText(Util.readableTransferRate(getContext(), conn.inBits));
|
||||
upload.setText(Util.readableTransferRate(getContext(), conn.outBits));
|
||||
rateInOutView.setVisibility(VISIBLE);
|
||||
status.setVisibility(VISIBLE);
|
||||
if (conn.completion == 100) {
|
||||
status.setText(r.getString(R.string.device_up_to_date));
|
||||
status.setTextColor(ContextCompat.getColor(getContext(), R.color.text_green));
|
||||
|
@ -81,8 +91,8 @@ public class DevicesAdapter extends ArrayAdapter<Device> {
|
|||
}
|
||||
|
||||
// !conn.connected
|
||||
download.setText(Util.readableTransferRate(getContext(), 0));
|
||||
upload.setText(Util.readableTransferRate(getContext(), 0));
|
||||
rateInOutView.setVisibility(GONE);
|
||||
status.setVisibility(VISIBLE);
|
||||
status.setText(r.getString(R.string.device_disconnected));
|
||||
status.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red));
|
||||
return convertView;
|
||||
|
@ -91,14 +101,20 @@ public class DevicesAdapter extends ArrayAdapter<Device> {
|
|||
/**
|
||||
* Requests new connection info for all devices visible in listView.
|
||||
*/
|
||||
public void updateConnections(RestApi api) {
|
||||
public void updateDeviceStatus(RestApi restApi) {
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running. Clear last state.
|
||||
mConnections = null;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < getCount(); i++) {
|
||||
api.getConnections(this::onReceiveConnections);
|
||||
restApi.getConnections(this::onReceiveConnections);
|
||||
}
|
||||
}
|
||||
|
||||
private void onReceiveConnections(Connections connections) {
|
||||
mConnections = connections;
|
||||
// This will invoke "getView" for all elements.
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,20 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
|||
mContext.startService(intent);
|
||||
});
|
||||
binding.openFolder.setOnClickListener(view -> { FileUtils.openFolder(mContext, folder.path); } );
|
||||
|
||||
// Update folder icon.
|
||||
int drawableId = R.drawable.ic_folder_black_24dp_active;
|
||||
switch (folder.type) {
|
||||
case Constants.FOLDER_TYPE_RECEIVE_ONLY:
|
||||
drawableId = R.drawable.ic_folder_receive_only;
|
||||
break;
|
||||
case Constants.FOLDER_TYPE_SEND_ONLY:
|
||||
drawableId = R.drawable.ic_folder_send_only;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
binding.openFolder.setImageResource(drawableId);
|
||||
|
||||
updateFolderStatusView(binding, folder);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
@ -72,6 +86,7 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
|||
binding.items.setVisibility(GONE);
|
||||
binding.override.setVisibility(GONE);
|
||||
binding.size.setVisibility(GONE);
|
||||
binding.state.setVisibility(GONE);
|
||||
setTextOrHide(binding.invalid, folder.invalid);
|
||||
return;
|
||||
}
|
||||
|
@ -80,6 +95,7 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
|||
boolean outOfSync = folderStatus.state.equals("idle") && neededItems > 0;
|
||||
boolean overrideButtonVisible = folder.type.equals(Constants.FOLDER_TYPE_SEND_ONLY) && outOfSync;
|
||||
binding.override.setVisibility(overrideButtonVisible ? VISIBLE : GONE);
|
||||
binding.state.setVisibility(VISIBLE);
|
||||
if (outOfSync) {
|
||||
binding.state.setText(mContext.getString(R.string.status_outofsync));
|
||||
binding.state.setTextColor(ContextCompat.getColor(mContext, R.color.text_red));
|
||||
|
@ -143,8 +159,9 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
|||
* Requests updated folder status from the api for all visible items.
|
||||
*/
|
||||
public void updateFolderStatus(RestApi restApi) {
|
||||
if (restApi == null) {
|
||||
Log.e(TAG, "updateFolderStatus: restApi == null");
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
// Syncthing is not running. Clear last state.
|
||||
mLocalFolderStatuses.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -158,6 +175,7 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
|||
|
||||
private void onReceiveFolderStatus(String folderId, FolderStatus folderStatus) {
|
||||
mLocalFolderStatuses.put(folderId, folderStatus);
|
||||
// This will invoke "getView" for all elements.
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ Maintenance
|
|||
* Updated syncthing core to v0.14.54
|
||||
* Fallback to http if https tls 1.2 is unavailable on Android 4.x
|
||||
Enhancements
|
||||
* UI can be used to configure even when syncthing is not running [NEW]
|
||||
* Added "Recent changes" UI, click files to open [NEW]
|
||||
* Specify sync conditions differently for each folder, device [NEW]
|
||||
* Added offline 'tips & tricks' content [NEW]
|
||||
* UI explains why syncthing is running (or not)
|
||||
Fixes
|
||||
* Fixed the "battery eater"
|
||||
* Android 8 and 9 support
|
||||
|
|
BIN
app/src/main/res/drawable-hdpi/ic_folder_receive_only.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_folder_send_only.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-ldpi/ic_folder_receive_only.png
Normal file
After Width: | Height: | Size: 682 B |
BIN
app/src/main/res/drawable-ldpi/ic_folder_send_only.png
Normal file
After Width: | Height: | Size: 653 B |
BIN
app/src/main/res/drawable-mdpi/ic_folder_receive_only.png
Normal file
After Width: | Height: | Size: 711 B |
BIN
app/src/main/res/drawable-mdpi/ic_folder_send_only.png
Normal file
After Width: | Height: | Size: 719 B |
BIN
app/src/main/res/drawable-xhdpi/ic_folder_receive_only.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_folder_send_only.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_folder_receive_only.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_folder_send_only.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_folder_receive_only.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_folder_send_only.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
|
@ -1,6 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:descendantFocusability="blocksDescendants">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/inner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="8dp"
|
||||
|
@ -12,28 +21,22 @@
|
|||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toLeftOf="@+id/status"
|
||||
android:layout_toStartOf="@+id/status"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="?textAppearanceListItemPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@+id/name"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:textAppearance="?textAppearanceListItemSmall" />
|
||||
<RelativeLayout
|
||||
android:id="@+id/rateInOutContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/name">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/name"
|
||||
android:text="@string/download_title_colon"
|
||||
android:textAppearance="?textAppearanceListItemSecondary" />
|
||||
|
||||
|
@ -63,4 +66,42 @@
|
|||
android:layout_toRightOf="@id/upload_title"
|
||||
android:textAppearance="?textAppearanceListItemSecondary" />
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignBottom="@+id/inner"
|
||||
android:layout_alignTop="@+id/inner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@+id/open_device"
|
||||
android:layout_toLeftOf="@+id/open_device"
|
||||
android:lines="1"
|
||||
android:textAppearance="?textAppearanceListItemSmall"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/open_device"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@+id/inner"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignTop="@+id/inner"
|
||||
android:background="@null"
|
||||
android:contentDescription="@null"
|
||||
android:paddingBottom="5dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingLeft="25dp"
|
||||
android:paddingRight="20dp"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingTop="5dp"
|
||||
android:src="@drawable/ic_phonelink_black_24dp_active" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</layout>
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
android:id="@+id/state"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@id/id"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:textAppearance="?textAppearanceListItemSmall" />
|
||||
|
@ -41,7 +40,7 @@
|
|||
android:id="@+id/directory"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/state"
|
||||
android:layout_below="@id/label"
|
||||
android:ellipsize="end"
|
||||
android:textAppearance="?textAppearanceListItemSecondary" />
|
||||
|
||||
|
|
|
@ -220,6 +220,9 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
|
|||
<!-- Toast shown when trying to create a folder with an empty ID -->
|
||||
<string name="folder_id_required">Die Verzeichnis ID darf nicht leer sein</string>
|
||||
|
||||
<!-- Toast shown when trying to create a folder with an empty label -->
|
||||
<string name="folder_label_required">Die Verzeichnisbezeichnung darf nicht leer sein</string>
|
||||
|
||||
<!-- Toast shown when trying to create a folder with an empty path -->
|
||||
<string name="folder_path_required">Der Verzeichnispfad darf nicht leer sein</string>
|
||||
|
||||
|
@ -283,6 +286,12 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
|
|||
<!-- Toast shown when trying to create a device with an empty ID -->
|
||||
<string name="device_id_required">Die Geräte-ID darf nicht leer sein</string>
|
||||
|
||||
<!-- Toast shown when trying to create a device with an empty ID -->
|
||||
<string name="device_id_invalid">Die Geräte-ID ist nicht im richtigen Format. Bitte versuche es erneut und gebe die richtige ID ein.</string>
|
||||
|
||||
<!-- Toast shown when trying to create a device with an empty name -->
|
||||
<string name="device_name_required">Der Gerätename darf nicht leer sein</string>
|
||||
|
||||
<!-- Content description for device ID qr code icon -->
|
||||
<string name="scan_qr_code_description">QR Code scannen</string>
|
||||
|
||||
|
@ -416,7 +425,7 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
|
|||
<item>Keine</item>
|
||||
<item>Papierkorb</item>
|
||||
<item>Einfach</item>
|
||||
<item>Stufenweis</item>
|
||||
<item>Gestaffelt</item>
|
||||
<item>Extern</item>
|
||||
</string-array>
|
||||
|
||||
|
|
|
@ -220,6 +220,9 @@ Please report any problems you encounter via Github.</string>
|
|||
<!-- Toast shown when trying to create a folder with an empty ID -->
|
||||
<string name="folder_id_required">The folder ID must not be empty</string>
|
||||
|
||||
<!-- Toast shown when trying to create a folder with an empty label -->
|
||||
<string name="folder_label_required">The folder label must not be empty</string>
|
||||
|
||||
<!-- Toast shown when trying to create a folder with an empty path -->
|
||||
<string name="folder_path_required">The folder path must not be empty</string>
|
||||
|
||||
|
@ -283,6 +286,12 @@ Please report any problems you encounter via Github.</string>
|
|||
<!-- Toast shown when trying to create a device with an empty ID -->
|
||||
<string name="device_id_required">The device ID must not be empty</string>
|
||||
|
||||
<!-- Toast shown when trying to create a device with an empty ID -->
|
||||
<string name="device_id_invalid">The device ID is not formatted correctly. Please retry and enter the correct ID.</string>
|
||||
|
||||
<!-- Toast shown when trying to create a device with an empty name -->
|
||||
<string name="device_name_required">The device name must not be empty</string>
|
||||
|
||||
<!-- Content description for device ID qr code icon -->
|
||||
<string name="scan_qr_code_description">Scan QR Code</string>
|
||||
|
||||
|
|