mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-11-26 14:21:16 +00:00
Fix incorrect remote device syncing status UI (fixes #1062)
This commit is contained in:
parent
7e3c6c0b8f
commit
f13ed587d7
9 changed files with 291 additions and 108 deletions
|
@ -87,7 +87,7 @@ public class FolderListFragment extends ListFragment implements SyncthingService
|
||||||
mAdapter.clear();
|
mAdapter.clear();
|
||||||
List<Folder> folders = activity.getApi().getFolders();
|
List<Folder> folders = activity.getApi().getFolders();
|
||||||
mAdapter.addAll(folders);
|
mAdapter.addAll(folders);
|
||||||
mAdapter.updateModel(activity.getApi());
|
mAdapter.updateFolderStatus(activity.getApi());
|
||||||
mAdapter.notifyDataSetChanged();
|
mAdapter.notifyDataSetChanged();
|
||||||
setListShown(true);
|
setListShown(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ public class GetRequest extends ApiRequest {
|
||||||
public static final String URI_VERSION = "/rest/system/version";
|
public static final String URI_VERSION = "/rest/system/version";
|
||||||
public static final String URI_SYSTEM = "/rest/system/status";
|
public static final String URI_SYSTEM = "/rest/system/status";
|
||||||
public static final String URI_CONNECTIONS = "/rest/system/connections";
|
public static final String URI_CONNECTIONS = "/rest/system/connections";
|
||||||
public static final String URI_MODEL = "/rest/db/status";
|
public static final String URI_STATUS = "/rest/db/status";
|
||||||
public static final String URI_DEVICEID = "/rest/svc/deviceid";
|
public static final String URI_DEVICEID = "/rest/svc/deviceid";
|
||||||
public static final String URI_REPORT = "/rest/svc/report";
|
public static final String URI_REPORT = "/rest/svc/report";
|
||||||
public static final String URI_EVENTS = "/rest/events";
|
public static final String URI_EVENTS = "/rest/events";
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
package com.nutomic.syncthingandroid.model;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class caches remote folder and device synchronization
|
||||||
|
* completion indicators defined in {@link CompletionInfo#CompletionInfo}
|
||||||
|
* according to syncthing's REST "/completion" JSON result schema.
|
||||||
|
* Completion model of syncthing's web UI is completion[deviceId][folderId]
|
||||||
|
*/
|
||||||
|
public class Completion {
|
||||||
|
|
||||||
|
private static final String TAG = "Completion";
|
||||||
|
|
||||||
|
HashMap<String, HashMap<String, CompletionInfo>> deviceFolderMap =
|
||||||
|
new HashMap<String, HashMap<String, CompletionInfo>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a folder from the cache model.
|
||||||
|
*/
|
||||||
|
private void removeFolder(String folderId) {
|
||||||
|
for (HashMap<String, CompletionInfo> folderMap : deviceFolderMap.values()) {
|
||||||
|
if (folderMap.containsKey(folderId)) {
|
||||||
|
folderMap.remove(folderId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates device and folder information in the cache model
|
||||||
|
* after a config update.
|
||||||
|
*/
|
||||||
|
public void updateFromConfig(List<Device> newDevices, List<Folder> newFolders) {
|
||||||
|
HashMap<String, CompletionInfo> folderMap;
|
||||||
|
|
||||||
|
// Handle devices that were removed from the config.
|
||||||
|
List<String> removedDevices = new ArrayList<>();;
|
||||||
|
Boolean deviceFound;
|
||||||
|
for (String deviceId : deviceFolderMap.keySet()) {
|
||||||
|
deviceFound = false;
|
||||||
|
for (Device device : newDevices) {
|
||||||
|
if (device.deviceID.equals(deviceId)) {
|
||||||
|
deviceFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!deviceFound) {
|
||||||
|
removedDevices.add(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String deviceId : removedDevices) {
|
||||||
|
Log.v(TAG, "updateFromConfig: Remove device '" + deviceId + "' from cache model");
|
||||||
|
deviceFolderMap.remove(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle devices that were added to the config.
|
||||||
|
for (Device device : newDevices) {
|
||||||
|
if (!deviceFolderMap.containsKey(device.deviceID)) {
|
||||||
|
Log.v(TAG, "updateFromConfig: Add device '" + device.deviceID + "' to cache model");
|
||||||
|
deviceFolderMap.put(device.deviceID, new HashMap<String, CompletionInfo>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle folders that were removed from the config.
|
||||||
|
List<String> removedFolders = new ArrayList<>();;
|
||||||
|
Boolean folderFound;
|
||||||
|
for (Map.Entry<String, HashMap<String, CompletionInfo>> device : deviceFolderMap.entrySet()) {
|
||||||
|
for (String folderId : device.getValue().keySet()) {
|
||||||
|
folderFound = false;
|
||||||
|
for (Folder folder : newFolders) {
|
||||||
|
if (folder.id.equals(folderId)) {
|
||||||
|
folderFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!folderFound) {
|
||||||
|
removedFolders.add(folderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String folderId : removedFolders) {
|
||||||
|
Log.v(TAG, "updateFromConfig: Remove folder '" + folderId + "' from cache model");
|
||||||
|
removeFolder(folderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle folders that were added to the config.
|
||||||
|
for (Folder folder : newFolders) {
|
||||||
|
for (Device device : newDevices) {
|
||||||
|
if (folder.getDevice(device.deviceID) != null) {
|
||||||
|
// folder is shared with device.
|
||||||
|
folderMap = deviceFolderMap.get(device.deviceID);
|
||||||
|
if (!folderMap.containsKey(folder.id)) {
|
||||||
|
Log.v(TAG, "updateFromConfig: Add folder '" + folder.id +
|
||||||
|
"' shared with device '" + device.deviceID + "' to cache model.");
|
||||||
|
folderMap.put(folder.id, new CompletionInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates remote device sync completion percentage across all folders
|
||||||
|
* shared with the device.
|
||||||
|
*/
|
||||||
|
public int getDeviceCompletion(String deviceId) {
|
||||||
|
int folderCount = 0;
|
||||||
|
double sumCompletion = 0;
|
||||||
|
HashMap<String, CompletionInfo> folderMap = deviceFolderMap.get(deviceId);
|
||||||
|
if (folderMap != null) {
|
||||||
|
for (Map.Entry<String, CompletionInfo> folder : folderMap.entrySet()) {
|
||||||
|
sumCompletion += folder.getValue().completion;
|
||||||
|
folderCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (folderCount == 0) {
|
||||||
|
return 100;
|
||||||
|
} else {
|
||||||
|
return (int) Math.floor(sumCompletion / folderCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set completionInfo within the completion[deviceId][folderId] model.
|
||||||
|
*/
|
||||||
|
public void setCompletionInfo(String deviceId, String folderId,
|
||||||
|
CompletionInfo completionInfo) {
|
||||||
|
// Add device parent node if it does not exist.
|
||||||
|
if (!deviceFolderMap.containsKey(deviceId)) {
|
||||||
|
deviceFolderMap.put(deviceId, new HashMap<String, CompletionInfo>());
|
||||||
|
}
|
||||||
|
// Add folder or update existing folder entry.
|
||||||
|
deviceFolderMap.get(deviceId).put(folderId, completionInfo);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.nutomic.syncthingandroid.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* According to syncthing REST API
|
||||||
|
* https://docs.syncthing.net/rest/db-completion-get.html
|
||||||
|
*
|
||||||
|
* completion is also returned by the events API
|
||||||
|
* https://docs.syncthing.net/events/foldercompletion.html
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class CompletionInfo {
|
||||||
|
public double completion = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following values are only returned by the REST API call
|
||||||
|
* to ""/completion". We will need them in the future to show
|
||||||
|
* more statistics in the device UI.
|
||||||
|
*/
|
||||||
|
// public long globalBytes = 0;
|
||||||
|
// public long needBytes = 0;
|
||||||
|
// public long needDeletes = 0;
|
||||||
|
// public long needItems = 0;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package com.nutomic.syncthingandroid.model;
|
package com.nutomic.syncthingandroid.model;
|
||||||
|
|
||||||
public class Model {
|
public class FolderStatus {
|
||||||
public long globalBytes;
|
public long globalBytes;
|
||||||
public long globalDeleted;
|
public long globalDeleted;
|
||||||
public long globalDirectories;
|
public long globalDirectories;
|
|
@ -13,10 +13,12 @@ import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
import com.nutomic.syncthingandroid.BuildConfig;
|
||||||
import com.nutomic.syncthingandroid.R;
|
import com.nutomic.syncthingandroid.R;
|
||||||
import com.nutomic.syncthingandroid.SyncthingApp;
|
import com.nutomic.syncthingandroid.SyncthingApp;
|
||||||
import com.nutomic.syncthingandroid.activities.DeviceActivity;
|
import com.nutomic.syncthingandroid.activities.DeviceActivity;
|
||||||
import com.nutomic.syncthingandroid.activities.FolderActivity;
|
import com.nutomic.syncthingandroid.activities.FolderActivity;
|
||||||
|
import com.nutomic.syncthingandroid.model.CompletionInfo;
|
||||||
import com.nutomic.syncthingandroid.model.Device;
|
import com.nutomic.syncthingandroid.model.Device;
|
||||||
import com.nutomic.syncthingandroid.model.Event;
|
import com.nutomic.syncthingandroid.model.Event;
|
||||||
import com.nutomic.syncthingandroid.model.Folder;
|
import com.nutomic.syncthingandroid.model.Folder;
|
||||||
|
@ -93,6 +95,9 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onEvent(Event event) {
|
public void onEvent(Event event) {
|
||||||
|
String deviceId;
|
||||||
|
String folderId;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "ConfigSaved":
|
case "ConfigSaved":
|
||||||
if (mApi != null) {
|
if (mApi != null) {
|
||||||
|
@ -101,8 +106,8 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "DeviceRejected":
|
case "DeviceRejected":
|
||||||
String deviceId = (String) event.data.get("device");
|
deviceId = (String) event.data.get("device");
|
||||||
Log.d(TAG, "Unknwon device " + deviceId + " wants to connect");
|
Log.d(TAG, "Unknown device " + deviceId + " wants to connect");
|
||||||
|
|
||||||
Intent intent = new Intent(mContext, DeviceActivity.class)
|
Intent intent = new Intent(mContext, DeviceActivity.class)
|
||||||
.putExtra(DeviceActivity.EXTRA_IS_CREATE, true)
|
.putExtra(DeviceActivity.EXTRA_IS_CREATE, true)
|
||||||
|
@ -117,9 +122,16 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene
|
||||||
|
|
||||||
notify(title, pi);
|
notify(title, pi);
|
||||||
break;
|
break;
|
||||||
|
case "FolderCompletion":
|
||||||
|
deviceId = (String) event.data.get("device");
|
||||||
|
folderId = (String) event.data.get("folder");
|
||||||
|
CompletionInfo completionInfo = new CompletionInfo();
|
||||||
|
completionInfo.completion = (Double) event.data.get("completion");
|
||||||
|
mApi.setCompletionInfo(deviceId, folderId, completionInfo);
|
||||||
|
break;
|
||||||
case "FolderRejected":
|
case "FolderRejected":
|
||||||
deviceId = (String) event.data.get("device");
|
deviceId = (String) event.data.get("device");
|
||||||
String folderId = (String) event.data.get("folder");
|
folderId = (String) event.data.get("folder");
|
||||||
String folderLabel = (String) event.data.get("folderLabel");
|
String folderLabel = (String) event.data.get("folderLabel");
|
||||||
Log.d(TAG, "Device " + deviceId + " wants to share folder " + folderId);
|
Log.d(TAG, "Device " + deviceId + " wants to share folder " + folderId);
|
||||||
|
|
||||||
|
@ -170,6 +182,25 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene
|
||||||
case "Ping":
|
case "Ping":
|
||||||
// Ignored.
|
// Ignored.
|
||||||
break;
|
break;
|
||||||
|
case "DeviceConnected":
|
||||||
|
case "DeviceDisconnected":
|
||||||
|
case "DeviceDiscovered":
|
||||||
|
case "DownloadProgress":
|
||||||
|
case "FolderPaused":
|
||||||
|
case "FolderScanProgress":
|
||||||
|
case "FolderSummary":
|
||||||
|
case "ItemStarted":
|
||||||
|
case "LocalIndexUpdated":
|
||||||
|
case "LoginAttempt":
|
||||||
|
case "RemoteDownloadProgress":
|
||||||
|
case "RemoteIndexUpdated":
|
||||||
|
case "Starting":
|
||||||
|
case "StartupComplete":
|
||||||
|
case "StateChanged":
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.v(TAG, "Ignored event " + event.type + ", data " + event.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Log.v(TAG, "Unhandled event " + event.type);
|
Log.v(TAG, "Unhandled event " + event.type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,13 @@ import com.nutomic.syncthingandroid.http.GetRequest;
|
||||||
import com.nutomic.syncthingandroid.http.PostConfigRequest;
|
import com.nutomic.syncthingandroid.http.PostConfigRequest;
|
||||||
import com.nutomic.syncthingandroid.http.PostScanRequest;
|
import com.nutomic.syncthingandroid.http.PostScanRequest;
|
||||||
import com.nutomic.syncthingandroid.model.Config;
|
import com.nutomic.syncthingandroid.model.Config;
|
||||||
|
import com.nutomic.syncthingandroid.model.Completion;
|
||||||
|
import com.nutomic.syncthingandroid.model.CompletionInfo;
|
||||||
import com.nutomic.syncthingandroid.model.Connections;
|
import com.nutomic.syncthingandroid.model.Connections;
|
||||||
import com.nutomic.syncthingandroid.model.Device;
|
import com.nutomic.syncthingandroid.model.Device;
|
||||||
import com.nutomic.syncthingandroid.model.Event;
|
import com.nutomic.syncthingandroid.model.Event;
|
||||||
import com.nutomic.syncthingandroid.model.Folder;
|
import com.nutomic.syncthingandroid.model.Folder;
|
||||||
import com.nutomic.syncthingandroid.model.Model;
|
import com.nutomic.syncthingandroid.model.FolderStatus;
|
||||||
import com.nutomic.syncthingandroid.model.Options;
|
import com.nutomic.syncthingandroid.model.Options;
|
||||||
import com.nutomic.syncthingandroid.model.SystemInfo;
|
import com.nutomic.syncthingandroid.model.SystemInfo;
|
||||||
import com.nutomic.syncthingandroid.model.SystemVersion;
|
import com.nutomic.syncthingandroid.model.SystemVersion;
|
||||||
|
@ -95,10 +97,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
private long mPreviousConnectionTime = 0;
|
private long mPreviousConnectionTime = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the latest result of {@link #getModel} for each folder, for calculating device
|
* Stores the latest result of {@link #getFolderStatus} for each folder
|
||||||
* percentage in {@link #getConnections}.
|
|
||||||
*/
|
*/
|
||||||
private final HashMap<String, Model> mCachedModelInfo = new HashMap<>();
|
private HashMap<String, FolderStatus> mCachedFolderStatuses = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the latest result of device and folder completion events.
|
||||||
|
*/
|
||||||
|
private Completion mCompletion = new Completion();
|
||||||
|
|
||||||
@Inject NotificationHandler mNotificationHandler;
|
@Inject NotificationHandler mNotificationHandler;
|
||||||
|
|
||||||
|
@ -143,11 +149,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
tryIsAvailable();
|
tryIsAvailable();
|
||||||
});
|
});
|
||||||
new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> {
|
new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> {
|
||||||
Log.v(TAG, "onWebGuiAvailable: " + result);
|
onReloadConfigComplete(result);
|
||||||
mConfig = new Gson().fromJson(result, Config.class);
|
|
||||||
if (mConfig == null) {
|
|
||||||
throw new RuntimeException("config is null: " + result);
|
|
||||||
}
|
|
||||||
tryIsAvailable();
|
tryIsAvailable();
|
||||||
});
|
});
|
||||||
getSystemInfo(info -> {
|
getSystemInfo(info -> {
|
||||||
|
@ -157,13 +159,18 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reloadConfig() {
|
public void reloadConfig() {
|
||||||
new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> {
|
new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, this::onReloadConfigComplete);
|
||||||
Log.v(TAG, "reloadConfig: " + result);
|
}
|
||||||
|
|
||||||
|
private void onReloadConfigComplete(String result) {
|
||||||
|
Log.v(TAG, "onReloadConfigComplete: " + result);
|
||||||
mConfig = new Gson().fromJson(result, Config.class);
|
mConfig = new Gson().fromJson(result, Config.class);
|
||||||
if (mConfig == null) {
|
if (mConfig == null) {
|
||||||
throw new RuntimeException("config is null: " + result);
|
throw new RuntimeException("config is null: " + result);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Update cached device and folder information stored in the mCompletion model.
|
||||||
|
mCompletion.updateFromConfig(getDevices(true), getFolders());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -241,6 +248,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
|
|
||||||
public void removeFolder(String id) {
|
public void removeFolder(String id) {
|
||||||
removeFolderInternal(id);
|
removeFolderInternal(id);
|
||||||
|
// 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()
|
||||||
|
@ -300,6 +308,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
|
|
||||||
public void removeDevice(String deviceId) {
|
public void removeDevice(String deviceId) {
|
||||||
removeDeviceInternal(deviceId);
|
removeDeviceInternal(deviceId);
|
||||||
|
// mCompletion will be updated after the ConfigSaved event.
|
||||||
sendConfig();
|
sendConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,7 +383,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
mPreviousConnectionTime = now;
|
mPreviousConnectionTime = now;
|
||||||
Connections connections = new Gson().fromJson(result, Connections.class);
|
Connections connections = new Gson().fromJson(result, Connections.class);
|
||||||
for (Map.Entry<String, Connections.Connection> e : connections.connections.entrySet()) {
|
for (Map.Entry<String, Connections.Connection> e : connections.connections.entrySet()) {
|
||||||
e.getValue().completion = getDeviceCompletion(e.getKey());
|
e.getValue().completion = mCompletion.getDeviceCompletion(e.getKey());
|
||||||
|
|
||||||
Connections.Connection prev =
|
Connections.Connection prev =
|
||||||
(mPreviousConnections.isPresent() && mPreviousConnections.get().connections.containsKey(e.getKey()))
|
(mPreviousConnections.isPresent() && mPreviousConnections.get().connections.containsKey(e.getKey()))
|
||||||
|
@ -390,46 +399,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates completion percentage for the given device using {@link #mCachedModelInfo}.
|
|
||||||
*/
|
|
||||||
private int getDeviceCompletion(String deviceId) {
|
|
||||||
int folderCount = 0;
|
|
||||||
float percentageSum = 0;
|
|
||||||
// Syncthing UI limits pending deletes to 95% completion of a device
|
|
||||||
int maxPercentage = 100;
|
|
||||||
for (Map.Entry<String, Model> modelInfo : mCachedModelInfo.entrySet()) {
|
|
||||||
boolean isShared = false;
|
|
||||||
for (Folder r : getFolders()) {
|
|
||||||
if (r.getDevice(deviceId) != null) {
|
|
||||||
isShared = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isShared) {
|
|
||||||
long global = modelInfo.getValue().globalBytes;
|
|
||||||
long local = modelInfo.getValue().inSyncBytes;
|
|
||||||
if (modelInfo.getValue().needFiles == 0 && modelInfo.getValue().needDeletes > 0)
|
|
||||||
maxPercentage = 95;
|
|
||||||
percentageSum += (global != 0)
|
|
||||||
? (local * 100f) / global
|
|
||||||
: 100f;
|
|
||||||
folderCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (folderCount != 0)
|
|
||||||
? Math.min(Math.round(percentageSum / folderCount), maxPercentage)
|
|
||||||
: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns status information about the folder with the given id.
|
* Returns status information about the folder with the given id.
|
||||||
*/
|
*/
|
||||||
public void getModel(final String folderId, final OnResultListener2<String, Model> listener) {
|
public void getFolderStatus(final String folderId, final OnResultListener2<String, FolderStatus> listener) {
|
||||||
new GetRequest(mContext, mUrl, GetRequest.URI_MODEL, mApiKey,
|
new GetRequest(mContext, mUrl, GetRequest.URI_STATUS, mApiKey,
|
||||||
ImmutableMap.of("folder", folderId), result -> {
|
ImmutableMap.of("folder", folderId), result -> {
|
||||||
Model m = new Gson().fromJson(result, Model.class);
|
FolderStatus m = new Gson().fromJson(result, FolderStatus.class);
|
||||||
mCachedModelInfo.put(folderId, m);
|
mCachedFolderStatuses.put(folderId, m);
|
||||||
listener.onResult(folderId, m);
|
listener.onResult(folderId, m);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -494,6 +471,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates cached folder and device completion info according to event data.
|
||||||
|
*/
|
||||||
|
public void setCompletionInfo(String deviceId, String folderId, CompletionInfo completionInfo) {
|
||||||
|
mCompletion.setCompletionInfo(deviceId, folderId, completionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns prettyfied usage report.
|
* Returns prettyfied usage report.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -271,7 +271,7 @@ public class ConfigXml {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set model name as device name for Syncthing.
|
* Set device model name as device name for Syncthing.
|
||||||
*
|
*
|
||||||
* We need to iterate through XML nodes manually, as mConfig.getDocumentElement() will also
|
* We need to iterate through XML nodes manually, as mConfig.getDocumentElement() will also
|
||||||
* return nested elements inside folder element. We have to check that we only rename the
|
* return nested elements inside folder element. We have to check that we only rename the
|
||||||
|
@ -300,12 +300,12 @@ public class ConfigXml {
|
||||||
private boolean changeDefaultFolder() {
|
private boolean changeDefaultFolder() {
|
||||||
Element folder = (Element) mConfig.getDocumentElement()
|
Element folder = (Element) mConfig.getDocumentElement()
|
||||||
.getElementsByTagName("folder").item(0);
|
.getElementsByTagName("folder").item(0);
|
||||||
String model = Build.MODEL
|
String deviceModel = Build.MODEL
|
||||||
.replace(" ", "_")
|
.replace(" ", "_")
|
||||||
.toLowerCase(Locale.US)
|
.toLowerCase(Locale.US)
|
||||||
.replaceAll("[^a-z0-9_-]", "");
|
.replaceAll("[^a-z0-9_-]", "");
|
||||||
folder.setAttribute("label", mContext.getString(R.string.default_folder_label));
|
folder.setAttribute("label", mContext.getString(R.string.default_folder_label));
|
||||||
folder.setAttribute("id", mContext.getString(R.string.default_folder_id, model));
|
folder.setAttribute("id", mContext.getString(R.string.default_folder_id, deviceModel));
|
||||||
folder.setAttribute("path", Environment
|
folder.setAttribute("path", Environment
|
||||||
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());
|
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());
|
||||||
folder.setAttribute("type", "readonly");
|
folder.setAttribute("type", "readonly");
|
||||||
|
|
|
@ -15,11 +15,10 @@ import android.widget.ArrayAdapter;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.nutomic.syncthingandroid.BuildConfig;
|
|
||||||
import com.nutomic.syncthingandroid.R;
|
import com.nutomic.syncthingandroid.R;
|
||||||
import com.nutomic.syncthingandroid.databinding.ItemFolderListBinding;
|
import com.nutomic.syncthingandroid.databinding.ItemFolderListBinding;
|
||||||
import com.nutomic.syncthingandroid.model.Folder;
|
import com.nutomic.syncthingandroid.model.Folder;
|
||||||
import com.nutomic.syncthingandroid.model.Model;
|
import com.nutomic.syncthingandroid.model.FolderStatus;
|
||||||
import com.nutomic.syncthingandroid.service.RestApi;
|
import com.nutomic.syncthingandroid.service.RestApi;
|
||||||
import com.nutomic.syncthingandroid.util.Util;
|
import com.nutomic.syncthingandroid.util.Util;
|
||||||
|
|
||||||
|
@ -36,7 +35,7 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
||||||
|
|
||||||
private static final String TAG = "FoldersAdapter";
|
private static final String TAG = "FoldersAdapter";
|
||||||
|
|
||||||
private final HashMap<String, Model> mModels = new HashMap<>();
|
private final HashMap<String, FolderStatus> mLocalFolderStatuses = new HashMap<>();
|
||||||
|
|
||||||
public FoldersAdapter(Context context) {
|
public FoldersAdapter(Context context) {
|
||||||
super(context, 0);
|
super(context, 0);
|
||||||
|
@ -50,7 +49,6 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
||||||
: DataBindingUtil.bind(convertView);
|
: DataBindingUtil.bind(convertView);
|
||||||
|
|
||||||
Folder folder = getItem(position);
|
Folder folder = getItem(position);
|
||||||
Model model = mModels.get(folder.id);
|
|
||||||
binding.label.setText(TextUtils.isEmpty(folder.label) ? folder.id : folder.label);
|
binding.label.setText(TextUtils.isEmpty(folder.label) ? folder.id : folder.label);
|
||||||
binding.directory.setText(folder.path);
|
binding.directory.setText(folder.path);
|
||||||
binding.openFolder.setOnClickListener(v -> {
|
binding.openFolder.setOnClickListener(v -> {
|
||||||
|
@ -73,12 +71,24 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (model != null) {
|
updateFolderStatusView(binding, folder);
|
||||||
int percentage = (model.globalBytes != 0)
|
return binding.getRoot();
|
||||||
? Math.round(100 * model.inSyncBytes / model.globalBytes)
|
}
|
||||||
|
|
||||||
|
private void updateFolderStatusView(ItemFolderListBinding binding, Folder folder) {
|
||||||
|
FolderStatus folderStatus = mLocalFolderStatuses.get(folder.id);
|
||||||
|
if (folderStatus == null) {
|
||||||
|
binding.items.setVisibility(GONE);
|
||||||
|
binding.size.setVisibility(GONE);
|
||||||
|
setTextOrHide(binding.invalid, folder.invalid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int percentage = (folderStatus.globalBytes != 0)
|
||||||
|
? Math.round(100 * folderStatus.inSyncBytes / folderStatus.globalBytes)
|
||||||
: 100;
|
: 100;
|
||||||
long neededItems = model.needFiles + model.needDirectories + model.needSymlinks + model.needDeletes;
|
long neededItems = folderStatus.needFiles + folderStatus.needDirectories + folderStatus.needSymlinks + folderStatus.needDeletes;
|
||||||
if (model.state.equals("idle") && neededItems > 0) {
|
if (folderStatus.state.equals("idle") && neededItems > 0) {
|
||||||
binding.state.setText(getContext().getString(R.string.status_outofsync));
|
binding.state.setText(getContext().getString(R.string.status_outofsync));
|
||||||
binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red));
|
binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red));
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,8 +96,8 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
||||||
binding.state.setText(getContext().getString(R.string.state_paused));
|
binding.state.setText(getContext().getString(R.string.state_paused));
|
||||||
binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_black));
|
binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_black));
|
||||||
} else {
|
} else {
|
||||||
binding.state.setText(getLocalizedState(getContext(), model.state, percentage));
|
binding.state.setText(getLocalizedState(getContext(), folderStatus.state, percentage));
|
||||||
switch(model.state) {
|
switch(folderStatus.state) {
|
||||||
case "idle":
|
case "idle":
|
||||||
binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_green));
|
binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_green));
|
||||||
break;
|
break;
|
||||||
|
@ -102,19 +112,12 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
||||||
}
|
}
|
||||||
binding.items.setVisibility(VISIBLE);
|
binding.items.setVisibility(VISIBLE);
|
||||||
binding.items.setText(getContext().getResources()
|
binding.items.setText(getContext().getResources()
|
||||||
.getQuantityString(R.plurals.files, (int) model.inSyncFiles, model.inSyncFiles, model.globalFiles));
|
.getQuantityString(R.plurals.files, (int) folderStatus.inSyncFiles, folderStatus.inSyncFiles, folderStatus.globalFiles));
|
||||||
binding.size.setVisibility(VISIBLE);
|
binding.size.setVisibility(VISIBLE);
|
||||||
binding.size.setText(getContext().getString(R.string.folder_size_format,
|
binding.size.setText(getContext().getString(R.string.folder_size_format,
|
||||||
Util.readableFileSize(getContext(), model.inSyncBytes),
|
Util.readableFileSize(getContext(), folderStatus.inSyncBytes),
|
||||||
Util.readableFileSize(getContext(), model.globalBytes)));
|
Util.readableFileSize(getContext(), folderStatus.globalBytes)));
|
||||||
setTextOrHide(binding.invalid, model.invalid);
|
setTextOrHide(binding.invalid, folderStatus.invalid);
|
||||||
} else {
|
|
||||||
binding.items.setVisibility(GONE);
|
|
||||||
binding.size.setVisibility(GONE);
|
|
||||||
setTextOrHide(binding.invalid, folder.invalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,16 +135,16 @@ public class FoldersAdapter extends ArrayAdapter<Folder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests updated model info from the api for all visible items.
|
* Requests updated folder status from the api for all visible items.
|
||||||
*/
|
*/
|
||||||
public void updateModel(RestApi api) {
|
public void updateFolderStatus(RestApi api) {
|
||||||
for (int i = 0; i < getCount(); i++) {
|
for (int i = 0; i < getCount(); i++) {
|
||||||
api.getModel(getItem(i).id, this::onReceiveModel);
|
api.getFolderStatus(getItem(i).id, this::onReceiveFolderStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onReceiveModel(String folderId, Model model) {
|
private void onReceiveFolderStatus(String folderId, FolderStatus folderStatus) {
|
||||||
mModels.put(folderId, model);
|
mLocalFolderStatuses.put(folderId, folderStatus);
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue