mirror of
https://github.com/syncthing/syncthing-android.git
synced 2025-02-10 11:04:41 +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.activities.SyncthingActivity;
|
||||||
import com.nutomic.syncthingandroid.model.Device;
|
import com.nutomic.syncthingandroid.model.Device;
|
||||||
import com.nutomic.syncthingandroid.service.Constants;
|
import com.nutomic.syncthingandroid.service.Constants;
|
||||||
|
import com.nutomic.syncthingandroid.service.RestApi;
|
||||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||||
import com.nutomic.syncthingandroid.views.DevicesAdapter;
|
import com.nutomic.syncthingandroid.views.DevicesAdapter;
|
||||||
|
|
||||||
|
@ -78,10 +79,17 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
||||||
*/
|
*/
|
||||||
private void updateList() {
|
private void updateList() {
|
||||||
SyncthingActivity activity = (SyncthingActivity) getActivity();
|
SyncthingActivity activity = (SyncthingActivity) getActivity();
|
||||||
if (activity == null || activity.getApi() == null || !activity.getApi().isConfigLoaded() ||
|
if (activity == null || getView() == null || activity.isFinishing()) {
|
||||||
getView() == null || activity.isFinishing())
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
RestApi restApi = activity.getApi();
|
||||||
|
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Device> devices = restApi.getDevices(false);
|
||||||
|
if (devices == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mAdapter == null) {
|
if (mAdapter == null) {
|
||||||
mAdapter = new DevicesAdapter(activity);
|
mAdapter = new DevicesAdapter(activity);
|
||||||
setListAdapter(mAdapter);
|
setListAdapter(mAdapter);
|
||||||
|
@ -90,10 +98,9 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
|
||||||
// Prevent scroll position reset due to list update from clear().
|
// Prevent scroll position reset due to list update from clear().
|
||||||
mAdapter.setNotifyOnChange(false);
|
mAdapter.setNotifyOnChange(false);
|
||||||
mAdapter.clear();
|
mAdapter.clear();
|
||||||
List<Device> devices = activity.getApi().getDevices(false);
|
|
||||||
Collections.sort(devices, DEVICES_COMPARATOR);
|
Collections.sort(devices, DEVICES_COMPARATOR);
|
||||||
mAdapter.addAll(devices);
|
mAdapter.addAll(devices);
|
||||||
mAdapter.updateConnections(activity.getApi());
|
mAdapter.updateConnections(restApi);
|
||||||
mAdapter.notifyDataSetChanged();
|
mAdapter.notifyDataSetChanged();
|
||||||
setListShown(true);
|
setListShown(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import com.nutomic.syncthingandroid.activities.FolderActivity;
|
||||||
import com.nutomic.syncthingandroid.activities.SyncthingActivity;
|
import com.nutomic.syncthingandroid.activities.SyncthingActivity;
|
||||||
import com.nutomic.syncthingandroid.model.Folder;
|
import com.nutomic.syncthingandroid.model.Folder;
|
||||||
import com.nutomic.syncthingandroid.service.Constants;
|
import com.nutomic.syncthingandroid.service.Constants;
|
||||||
|
import com.nutomic.syncthingandroid.service.RestApi;
|
||||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||||
import com.nutomic.syncthingandroid.views.FoldersAdapter;
|
import com.nutomic.syncthingandroid.views.FoldersAdapter;
|
||||||
|
|
||||||
|
@ -73,10 +74,17 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
||||||
*/
|
*/
|
||||||
private void updateList() {
|
private void updateList() {
|
||||||
SyncthingActivity activity = (SyncthingActivity) getActivity();
|
SyncthingActivity activity = (SyncthingActivity) getActivity();
|
||||||
if (activity == null || activity.getApi() == null || !activity.getApi().isConfigLoaded() ||
|
if (activity == null || getView() == null || activity.isFinishing()) {
|
||||||
getView() == null || activity.isFinishing())
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
RestApi restApi = activity.getApi();
|
||||||
|
if (restApi == null || !restApi.isConfigLoaded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Folder> folders = restApi.getFolders();
|
||||||
|
if (folders == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mAdapter == null) {
|
if (mAdapter == null) {
|
||||||
mAdapter = new FoldersAdapter(activity);
|
mAdapter = new FoldersAdapter(activity);
|
||||||
setListAdapter(mAdapter);
|
setListAdapter(mAdapter);
|
||||||
|
@ -85,9 +93,8 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
||||||
// Prevent scroll position reset due to list update from clear().
|
// Prevent scroll position reset due to list update from clear().
|
||||||
mAdapter.setNotifyOnChange(false);
|
mAdapter.setNotifyOnChange(false);
|
||||||
mAdapter.clear();
|
mAdapter.clear();
|
||||||
List<Folder> folders = activity.getApi().getFolders();
|
|
||||||
mAdapter.addAll(folders);
|
mAdapter.addAll(folders);
|
||||||
mAdapter.updateFolderStatus(activity.getApi());
|
mAdapter.updateFolderStatus(restApi);
|
||||||
mAdapter.notifyDataSetChanged();
|
mAdapter.notifyDataSetChanged();
|
||||||
setListShown(true);
|
setListShown(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,11 @@ public class RestApi {
|
||||||
*/
|
*/
|
||||||
private final Object mAsyncQueryCompleteLock = new Object();
|
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
|
* Stores the latest result of {@link #getFolderStatus} for each folder
|
||||||
*/
|
*/
|
||||||
|
@ -199,8 +204,12 @@ public class RestApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onReloadConfigComplete(String result) {
|
private void onReloadConfigComplete(String result) {
|
||||||
|
Boolean configParseSuccess;
|
||||||
|
synchronized(mConfigLock) {
|
||||||
mConfig = new Gson().fromJson(result, Config.class);
|
mConfig = new Gson().fromJson(result, Config.class);
|
||||||
if (mConfig == null) {
|
configParseSuccess = mConfig != null;
|
||||||
|
}
|
||||||
|
if (!configParseSuccess) {
|
||||||
throw new RuntimeException("config is null: " + result);
|
throw new RuntimeException("config is null: " + result);
|
||||||
}
|
}
|
||||||
Log.v(TAG, "onReloadConfigComplete: Successfully parsed configuration.");
|
Log.v(TAG, "onReloadConfigComplete: Successfully parsed configuration.");
|
||||||
|
@ -249,12 +258,14 @@ public class RestApi {
|
||||||
* in {@link EventProcessor#onEvent}.
|
* in {@link EventProcessor#onEvent}.
|
||||||
*/
|
*/
|
||||||
public void ignoreDevice(String deviceId) {
|
public void ignoreDevice(String deviceId) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
if (!mConfig.ignoredDevices.contains(deviceId)) {
|
if (!mConfig.ignoredDevices.contains(deviceId)) {
|
||||||
mConfig.ignoredDevices.add(deviceId);
|
mConfig.ignoredDevices.add(deviceId);
|
||||||
sendConfig();
|
sendConfig();
|
||||||
Log.d(TAG, "Ignored device [" + deviceId + "]");
|
Log.d(TAG, "Ignored device [" + deviceId + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permanently ignore a folder share request.
|
* Permanently ignore a folder share request.
|
||||||
|
@ -262,21 +273,25 @@ public class RestApi {
|
||||||
* in {@link EventProcessor#onEvent}.
|
* in {@link EventProcessor#onEvent}.
|
||||||
*/
|
*/
|
||||||
public void ignoreFolder(String folderId) {
|
public void ignoreFolder(String folderId) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
if (!mConfig.ignoredFolders.contains(folderId)) {
|
if (!mConfig.ignoredFolders.contains(folderId)) {
|
||||||
mConfig.ignoredFolders.add(folderId);
|
mConfig.ignoredFolders.add(folderId);
|
||||||
sendConfig();
|
sendConfig();
|
||||||
Log.d(TAG, "Ignored folder [" + folderId + "]");
|
Log.d(TAG, "Ignored folder [" + folderId + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo ignoring devices and folders.
|
* Undo ignoring devices and folders.
|
||||||
*/
|
*/
|
||||||
public void undoIgnoredDevicesAndFolders() {
|
public void undoIgnoredDevicesAndFolders() {
|
||||||
Log.d(TAG, "Undo ignoring devices and folders ...");
|
Log.d(TAG, "Undo ignoring devices and folders ...");
|
||||||
|
synchronized (mConfigLock) {
|
||||||
mConfig.ignoredDevices.clear();
|
mConfig.ignoredDevices.clear();
|
||||||
mConfig.ignoredFolders.clear();
|
mConfig.ignoredFolders.clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override folder changes. This is the same as hitting
|
* Override folder changes. This is the same as hitting
|
||||||
|
@ -294,7 +309,11 @@ public class RestApi {
|
||||||
* EventProcessor will trigger this.reloadConfig().
|
* EventProcessor will trigger this.reloadConfig().
|
||||||
*/
|
*/
|
||||||
private void sendConfig() {
|
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();
|
mOnConfigChangedListener.onConfigChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,7 +321,11 @@ public class RestApi {
|
||||||
* Sends current config and restarts Syncthing.
|
* Sends current config and restarts Syncthing.
|
||||||
*/
|
*/
|
||||||
public void saveConfigAndRestart() {
|
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)
|
Intent intent = new Intent(mContext, SyncthingService.class)
|
||||||
.setAction(SyncthingService.ACTION_RESTART);
|
.setAction(SyncthingService.ACTION_RESTART);
|
||||||
mContext.startService(intent);
|
mContext.startService(intent);
|
||||||
|
@ -322,7 +345,10 @@ public class RestApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Folder> getFolders() {
|
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);
|
Collections.sort(folders, FOLDERS_COMPARATOR);
|
||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
@ -331,34 +357,43 @@ public class RestApi {
|
||||||
* This is only used for new folder creation, see {@link FolderActivity}.
|
* This is only used for new folder creation, see {@link FolderActivity}.
|
||||||
*/
|
*/
|
||||||
public void createFolder(Folder folder) {
|
public void createFolder(Folder folder) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
// Add the new folder to the model.
|
// Add the new folder to the model.
|
||||||
mConfig.folders.add(folder);
|
mConfig.folders.add(folder);
|
||||||
// Send model changes to syncthing, does not require a restart.
|
// Send model changes to syncthing, does not require a restart.
|
||||||
sendConfig();
|
sendConfig();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateFolder(Folder newFolder) {
|
public void updateFolder(Folder newFolder) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
removeFolderInternal(newFolder.id);
|
removeFolderInternal(newFolder.id);
|
||||||
mConfig.folders.add(newFolder);
|
mConfig.folders.add(newFolder);
|
||||||
sendConfig();
|
sendConfig();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void removeFolder(String id) {
|
public void removeFolder(String id) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
removeFolderInternal(id);
|
removeFolderInternal(id);
|
||||||
// mCompletion will be updated after the ConfigSaved event.
|
// mCompletion will be updated after the ConfigSaved event.
|
||||||
sendConfig();
|
sendConfig();
|
||||||
// Remove saved data from share activity for this folder.
|
// Remove saved data from share activity for this folder.
|
||||||
|
}
|
||||||
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
|
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
|
||||||
.remove(ShareActivity.PREF_FOLDER_SAVED_SUBDIRECTORY+id)
|
.remove(ShareActivity.PREF_FOLDER_SAVED_SUBDIRECTORY+id)
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeFolderInternal(String id) {
|
private void removeFolderInternal(String id) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
Iterator<Folder> it = mConfig.folders.iterator();
|
Iterator<Folder> it = mConfig.folders.iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Folder f = it.next();
|
Folder f = it.next();
|
||||||
if (f.id.equals(id)) {
|
if (f.id.equals(id)) {
|
||||||
it.remove();
|
it.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -369,7 +404,10 @@ public class RestApi {
|
||||||
* @param includeLocal True if the local device should be included in the result.
|
* @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 = 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();
|
Iterator<Device> it = devices.iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
|
@ -397,45 +435,60 @@ public class RestApi {
|
||||||
|
|
||||||
public void addDevice(Device device, OnResultListener1<String> errorListener) {
|
public void addDevice(Device device, OnResultListener1<String> errorListener) {
|
||||||
normalizeDeviceId(device.deviceID, normalizedId -> {
|
normalizeDeviceId(device.deviceID, normalizedId -> {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
mConfig.devices.add(device);
|
mConfig.devices.add(device);
|
||||||
sendConfig();
|
sendConfig();
|
||||||
|
}
|
||||||
}, errorListener);
|
}, errorListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void editDevice(Device newDevice) {
|
public void editDevice(Device newDevice) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
removeDeviceInternal(newDevice.deviceID);
|
removeDeviceInternal(newDevice.deviceID);
|
||||||
mConfig.devices.add(newDevice);
|
mConfig.devices.add(newDevice);
|
||||||
sendConfig();
|
sendConfig();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void removeDevice(String deviceId) {
|
public void removeDevice(String deviceId) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
removeDeviceInternal(deviceId);
|
removeDeviceInternal(deviceId);
|
||||||
// mCompletion will be updated after the ConfigSaved event.
|
// mCompletion will be updated after the ConfigSaved event.
|
||||||
sendConfig();
|
sendConfig();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void removeDeviceInternal(String deviceId) {
|
private void removeDeviceInternal(String deviceId) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
Iterator<Device> it = mConfig.devices.iterator();
|
Iterator<Device> it = mConfig.devices.iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Device d = it.next();
|
Device d = it.next();
|
||||||
if (d.deviceID.equals(deviceId)) {
|
if (d.deviceID.equals(deviceId)) {
|
||||||
it.remove();
|
it.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Options getOptions() {
|
public Options getOptions() {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
return deepCopy(mConfig.options, Options.class);
|
return deepCopy(mConfig.options, Options.class);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Config.Gui getGui() {
|
public Config.Gui getGui() {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
return deepCopy(mConfig.gui, Config.Gui.class);
|
return deepCopy(mConfig.gui, Config.Gui.class);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void editSettings(Config.Gui newGui, Options newOptions) {
|
public void editSettings(Config.Gui newGui, Options newOptions) {
|
||||||
|
synchronized (mConfigLock) {
|
||||||
mConfig.gui = newGui;
|
mConfig.gui = newGui;
|
||||||
mConfig.options = newOptions;
|
mConfig.options = newOptions;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a deep copy of object.
|
* Returns a deep copy of object.
|
||||||
|
@ -456,8 +509,10 @@ public class RestApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isConfigLoaded() {
|
public boolean isConfigLoaded() {
|
||||||
|
synchronized(mConfigLock) {
|
||||||
return mConfig != null;
|
return mConfig != null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests and parses system version information.
|
* Requests and parses system version information.
|
||||||
|
@ -611,6 +666,8 @@ public class RestApi {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
options.urAccepted = acceptUsageReporting ? mUrVersionMax : Options.USAGE_REPORTING_DENIED;
|
options.urAccepted = acceptUsageReporting ? mUrVersionMax : Options.USAGE_REPORTING_DENIED;
|
||||||
|
synchronized (mConfigLock) {
|
||||||
mConfig.options = options;
|
mConfig.options = options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue