mirror of
https://github.com/syncthing/syncthing-android.git
synced 2025-02-09 02:24:42 +00:00
* Fix race during config reload and missing null check in FolderListFragment and DeviceListFragment (fixes #1194) * Review - synchronize(mConfigLock) when mConfig is accessed * Review - add two breaks in RestApi in removeFolderInternal, removeDeviceInternal
This commit is contained in:
parent
5426e750ef
commit
b50fcf1fa1
3 changed files with 132 additions and 61 deletions
|
@ -15,6 +15,7 @@ import com.nutomic.syncthingandroid.activities.DeviceActivity;
|
|||
import com.nutomic.syncthingandroid.activities.SyncthingActivity;
|
||||
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.views.DevicesAdapter;
|
||||
|
||||
|
@ -78,10 +79,17 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
|||
*/
|
||||
private void updateList() {
|
||||
SyncthingActivity activity = (SyncthingActivity) getActivity();
|
||||
if (activity == null || activity.getApi() == null || !activity.getApi().isConfigLoaded() ||
|
||||
getView() == null || activity.isFinishing())
|
||||
if (activity == null || getView() == null || activity.isFinishing()) {
|
||||
return;
|
||||
|
||||
}
|
||||
RestApi restApi = activity.getApi();
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
return;
|
||||
}
|
||||
List<Device> devices = restApi.getDevices(false);
|
||||
if (devices == null) {
|
||||
return;
|
||||
}
|
||||
if (mAdapter == null) {
|
||||
mAdapter = new DevicesAdapter(activity);
|
||||
setListAdapter(mAdapter);
|
||||
|
@ -90,10 +98,9 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
|||
// Prevent scroll position reset due to list update from clear().
|
||||
mAdapter.setNotifyOnChange(false);
|
||||
mAdapter.clear();
|
||||
List<Device> devices = activity.getApi().getDevices(false);
|
||||
Collections.sort(devices, DEVICES_COMPARATOR);
|
||||
mAdapter.addAll(devices);
|
||||
mAdapter.updateConnections(activity.getApi());
|
||||
mAdapter.updateConnections(restApi);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
setListShown(true);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.nutomic.syncthingandroid.activities.FolderActivity;
|
|||
import com.nutomic.syncthingandroid.activities.SyncthingActivity;
|
||||
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.views.FoldersAdapter;
|
||||
|
||||
|
@ -73,10 +74,17 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
|||
*/
|
||||
private void updateList() {
|
||||
SyncthingActivity activity = (SyncthingActivity) getActivity();
|
||||
if (activity == null || activity.getApi() == null || !activity.getApi().isConfigLoaded() ||
|
||||
getView() == null || activity.isFinishing())
|
||||
if (activity == null || getView() == null || activity.isFinishing()) {
|
||||
return;
|
||||
|
||||
}
|
||||
RestApi restApi = activity.getApi();
|
||||
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||
return;
|
||||
}
|
||||
List<Folder> folders = restApi.getFolders();
|
||||
if (folders == null) {
|
||||
return;
|
||||
}
|
||||
if (mAdapter == null) {
|
||||
mAdapter = new FoldersAdapter(activity);
|
||||
setListAdapter(mAdapter);
|
||||
|
@ -85,9 +93,8 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
|||
// Prevent scroll position reset due to list update from clear().
|
||||
mAdapter.setNotifyOnChange(false);
|
||||
mAdapter.clear();
|
||||
List<Folder> folders = activity.getApi().getFolders();
|
||||
mAdapter.addAll(folders);
|
||||
mAdapter.updateFolderStatus(activity.getApi());
|
||||
mAdapter.updateFolderStatus(restApi);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
setListShown(true);
|
||||
}
|
||||
|
|
|
@ -120,6 +120,11 @@ public class RestApi {
|
|||
*/
|
||||
private final Object mAsyncQueryCompleteLock = new Object();
|
||||
|
||||
/**
|
||||
* Object that must be locked upon accessing mConfig
|
||||
*/
|
||||
private final Object mConfigLock = new Object();
|
||||
|
||||
/**
|
||||
* Stores the latest result of {@link #getFolderStatus} for each folder
|
||||
*/
|
||||
|
@ -199,8 +204,12 @@ public class RestApi {
|
|||
}
|
||||
|
||||
private void onReloadConfigComplete(String result) {
|
||||
mConfig = new Gson().fromJson(result, Config.class);
|
||||
if (mConfig == null) {
|
||||
Boolean configParseSuccess;
|
||||
synchronized(mConfigLock) {
|
||||
mConfig = new Gson().fromJson(result, Config.class);
|
||||
configParseSuccess = mConfig != null;
|
||||
}
|
||||
if (!configParseSuccess) {
|
||||
throw new RuntimeException("config is null: " + result);
|
||||
}
|
||||
Log.v(TAG, "onReloadConfigComplete: Successfully parsed configuration.");
|
||||
|
@ -249,10 +258,12 @@ public class RestApi {
|
|||
* in {@link EventProcessor#onEvent}.
|
||||
*/
|
||||
public void ignoreDevice(String deviceId) {
|
||||
if (!mConfig.ignoredDevices.contains(deviceId)) {
|
||||
mConfig.ignoredDevices.add(deviceId);
|
||||
sendConfig();
|
||||
Log.d(TAG, "Ignored device [" + deviceId + "]");
|
||||
synchronized (mConfigLock) {
|
||||
if (!mConfig.ignoredDevices.contains(deviceId)) {
|
||||
mConfig.ignoredDevices.add(deviceId);
|
||||
sendConfig();
|
||||
Log.d(TAG, "Ignored device [" + deviceId + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,10 +273,12 @@ public class RestApi {
|
|||
* in {@link EventProcessor#onEvent}.
|
||||
*/
|
||||
public void ignoreFolder(String folderId) {
|
||||
if (!mConfig.ignoredFolders.contains(folderId)) {
|
||||
mConfig.ignoredFolders.add(folderId);
|
||||
sendConfig();
|
||||
Log.d(TAG, "Ignored folder [" + folderId + "]");
|
||||
synchronized (mConfigLock) {
|
||||
if (!mConfig.ignoredFolders.contains(folderId)) {
|
||||
mConfig.ignoredFolders.add(folderId);
|
||||
sendConfig();
|
||||
Log.d(TAG, "Ignored folder [" + folderId + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,8 +287,10 @@ public class RestApi {
|
|||
*/
|
||||
public void undoIgnoredDevicesAndFolders() {
|
||||
Log.d(TAG, "Undo ignoring devices and folders ...");
|
||||
mConfig.ignoredDevices.clear();
|
||||
mConfig.ignoredFolders.clear();
|
||||
synchronized (mConfigLock) {
|
||||
mConfig.ignoredDevices.clear();
|
||||
mConfig.ignoredFolders.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -294,7 +309,11 @@ public class RestApi {
|
|||
* EventProcessor will trigger this.reloadConfig().
|
||||
*/
|
||||
private void sendConfig() {
|
||||
new PostConfigRequest(mContext, mUrl, mApiKey, new Gson().toJson(mConfig), null);
|
||||
String jsonConfig;
|
||||
synchronized (mConfigLock) {
|
||||
jsonConfig = new Gson().toJson(mConfig);
|
||||
}
|
||||
new PostConfigRequest(mContext, mUrl, mApiKey, jsonConfig, null);
|
||||
mOnConfigChangedListener.onConfigChanged();
|
||||
}
|
||||
|
||||
|
@ -302,7 +321,11 @@ public class RestApi {
|
|||
* Sends current config and restarts Syncthing.
|
||||
*/
|
||||
public void saveConfigAndRestart() {
|
||||
new PostConfigRequest(mContext, mUrl, mApiKey, new Gson().toJson(mConfig), result -> {
|
||||
String jsonConfig;
|
||||
synchronized (mConfigLock) {
|
||||
jsonConfig = new Gson().toJson(mConfig);
|
||||
}
|
||||
new PostConfigRequest(mContext, mUrl, mApiKey, jsonConfig, result -> {
|
||||
Intent intent = new Intent(mContext, SyncthingService.class)
|
||||
.setAction(SyncthingService.ACTION_RESTART);
|
||||
mContext.startService(intent);
|
||||
|
@ -322,7 +345,10 @@ public class RestApi {
|
|||
}
|
||||
|
||||
public List<Folder> getFolders() {
|
||||
List<Folder> folders = deepCopy(mConfig.folders, new TypeToken<List<Folder>>(){}.getType());
|
||||
List<Folder> folders;
|
||||
synchronized (mConfigLock) {
|
||||
folders = deepCopy(mConfig.folders, new TypeToken<List<Folder>>(){}.getType());
|
||||
}
|
||||
Collections.sort(folders, FOLDERS_COMPARATOR);
|
||||
return folders;
|
||||
}
|
||||
|
@ -331,34 +357,43 @@ public class RestApi {
|
|||
* This is only used for new folder creation, see {@link FolderActivity}.
|
||||
*/
|
||||
public void createFolder(Folder folder) {
|
||||
// Add the new folder to the model.
|
||||
mConfig.folders.add(folder);
|
||||
// Send model changes to syncthing, does not require a restart.
|
||||
sendConfig();
|
||||
synchronized (mConfigLock) {
|
||||
// Add the new folder to the model.
|
||||
mConfig.folders.add(folder);
|
||||
// Send model changes to syncthing, does not require a restart.
|
||||
sendConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFolder(Folder newFolder) {
|
||||
removeFolderInternal(newFolder.id);
|
||||
mConfig.folders.add(newFolder);
|
||||
sendConfig();
|
||||
synchronized (mConfigLock) {
|
||||
removeFolderInternal(newFolder.id);
|
||||
mConfig.folders.add(newFolder);
|
||||
sendConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFolder(String id) {
|
||||
removeFolderInternal(id);
|
||||
// mCompletion will be updated after the ConfigSaved event.
|
||||
sendConfig();
|
||||
// Remove saved data from share activity for this folder.
|
||||
synchronized (mConfigLock) {
|
||||
removeFolderInternal(id);
|
||||
// mCompletion will be updated after the ConfigSaved event.
|
||||
sendConfig();
|
||||
// Remove saved data from share activity for this folder.
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
|
||||
.remove(ShareActivity.PREF_FOLDER_SAVED_SUBDIRECTORY+id)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void removeFolderInternal(String id) {
|
||||
Iterator<Folder> it = mConfig.folders.iterator();
|
||||
while (it.hasNext()) {
|
||||
Folder f = it.next();
|
||||
if (f.id.equals(id)) {
|
||||
it.remove();
|
||||
synchronized (mConfigLock) {
|
||||
Iterator<Folder> it = mConfig.folders.iterator();
|
||||
while (it.hasNext()) {
|
||||
Folder f = it.next();
|
||||
if (f.id.equals(id)) {
|
||||
it.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -369,7 +404,10 @@ public class RestApi {
|
|||
* @param includeLocal True if the local device should be included in the result.
|
||||
*/
|
||||
public List<Device> getDevices(boolean includeLocal) {
|
||||
List<Device> devices = deepCopy(mConfig.devices, new TypeToken<List<Device>>(){}.getType());
|
||||
List<Device> devices;
|
||||
synchronized (mConfigLock) {
|
||||
devices = deepCopy(mConfig.devices, new TypeToken<List<Device>>(){}.getType());
|
||||
}
|
||||
|
||||
Iterator<Device> it = devices.iterator();
|
||||
while (it.hasNext()) {
|
||||
|
@ -397,44 +435,59 @@ public class RestApi {
|
|||
|
||||
public void addDevice(Device device, OnResultListener1<String> errorListener) {
|
||||
normalizeDeviceId(device.deviceID, normalizedId -> {
|
||||
mConfig.devices.add(device);
|
||||
sendConfig();
|
||||
synchronized (mConfigLock) {
|
||||
mConfig.devices.add(device);
|
||||
sendConfig();
|
||||
}
|
||||
}, errorListener);
|
||||
}
|
||||
|
||||
public void editDevice(Device newDevice) {
|
||||
removeDeviceInternal(newDevice.deviceID);
|
||||
mConfig.devices.add(newDevice);
|
||||
sendConfig();
|
||||
synchronized (mConfigLock) {
|
||||
removeDeviceInternal(newDevice.deviceID);
|
||||
mConfig.devices.add(newDevice);
|
||||
sendConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeDevice(String deviceId) {
|
||||
removeDeviceInternal(deviceId);
|
||||
// mCompletion will be updated after the ConfigSaved event.
|
||||
sendConfig();
|
||||
synchronized (mConfigLock) {
|
||||
removeDeviceInternal(deviceId);
|
||||
// mCompletion will be updated after the ConfigSaved event.
|
||||
sendConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeDeviceInternal(String deviceId) {
|
||||
Iterator<Device> it = mConfig.devices.iterator();
|
||||
while (it.hasNext()) {
|
||||
Device d = it.next();
|
||||
if (d.deviceID.equals(deviceId)) {
|
||||
it.remove();
|
||||
synchronized (mConfigLock) {
|
||||
Iterator<Device> it = mConfig.devices.iterator();
|
||||
while (it.hasNext()) {
|
||||
Device d = it.next();
|
||||
if (d.deviceID.equals(deviceId)) {
|
||||
it.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Options getOptions() {
|
||||
return deepCopy(mConfig.options, Options.class);
|
||||
synchronized (mConfigLock) {
|
||||
return deepCopy(mConfig.options, Options.class);
|
||||
}
|
||||
}
|
||||
|
||||
public Config.Gui getGui() {
|
||||
return deepCopy(mConfig.gui, Config.Gui.class);
|
||||
synchronized (mConfigLock) {
|
||||
return deepCopy(mConfig.gui, Config.Gui.class);
|
||||
}
|
||||
}
|
||||
|
||||
public void editSettings(Config.Gui newGui, Options newOptions) {
|
||||
mConfig.gui = newGui;
|
||||
mConfig.options = newOptions;
|
||||
synchronized (mConfigLock) {
|
||||
mConfig.gui = newGui;
|
||||
mConfig.options = newOptions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -456,7 +509,9 @@ public class RestApi {
|
|||
}
|
||||
|
||||
public boolean isConfigLoaded() {
|
||||
return mConfig != null;
|
||||
synchronized(mConfigLock) {
|
||||
return mConfig != null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -611,6 +666,8 @@ public class RestApi {
|
|||
return;
|
||||
}
|
||||
options.urAccepted = acceptUsageReporting ? mUrVersionMax : Options.USAGE_REPORTING_DENIED;
|
||||
mConfig.options = options;
|
||||
synchronized (mConfigLock) {
|
||||
mConfig.options = options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue