diff --git a/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java b/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java index 4dbf3985..628c833a 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java @@ -87,7 +87,7 @@ public class FolderListFragment extends ListFragment implements SyncthingService mAdapter.clear(); List folders = activity.getApi().getFolders(); mAdapter.addAll(folders); - mAdapter.updateModel(activity.getApi()); + mAdapter.updateFolderStatus(activity.getApi()); mAdapter.notifyDataSetChanged(); setListShown(true); } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/http/GetRequest.java b/app/src/main/java/com/nutomic/syncthingandroid/http/GetRequest.java index b7fe28a6..4a9bf2bb 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/http/GetRequest.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/http/GetRequest.java @@ -20,7 +20,7 @@ public class GetRequest extends ApiRequest { public static final String URI_VERSION = "/rest/system/version"; public static final String URI_SYSTEM = "/rest/system/status"; 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_REPORT = "/rest/svc/report"; public static final String URI_EVENTS = "/rest/events"; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/model/Completion.java b/app/src/main/java/com/nutomic/syncthingandroid/model/Completion.java new file mode 100644 index 00000000..56e1733b --- /dev/null +++ b/app/src/main/java/com/nutomic/syncthingandroid/model/Completion.java @@ -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> deviceFolderMap = + new HashMap>(); + + /** + * Removes a folder from the cache model. + */ + private void removeFolder(String folderId) { + for (HashMap 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 newDevices, List newFolders) { + HashMap folderMap; + + // Handle devices that were removed from the config. + List 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()); + } + } + + // Handle folders that were removed from the config. + List removedFolders = new ArrayList<>();; + Boolean folderFound; + for (Map.Entry> 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 folderMap = deviceFolderMap.get(deviceId); + if (folderMap != null) { + for (Map.Entry 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()); + } + // Add folder or update existing folder entry. + deviceFolderMap.get(deviceId).put(folderId, completionInfo); + } +} diff --git a/app/src/main/java/com/nutomic/syncthingandroid/model/CompletionInfo.java b/app/src/main/java/com/nutomic/syncthingandroid/model/CompletionInfo.java new file mode 100644 index 00000000..796bc12c --- /dev/null +++ b/app/src/main/java/com/nutomic/syncthingandroid/model/CompletionInfo.java @@ -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; +} diff --git a/app/src/main/java/com/nutomic/syncthingandroid/model/Model.java b/app/src/main/java/com/nutomic/syncthingandroid/model/FolderStatus.java similarity index 96% rename from app/src/main/java/com/nutomic/syncthingandroid/model/Model.java rename to app/src/main/java/com/nutomic/syncthingandroid/model/FolderStatus.java index 834585c2..8d587da8 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/model/Model.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/model/FolderStatus.java @@ -1,6 +1,6 @@ package com.nutomic.syncthingandroid.model; -public class Model { +public class FolderStatus { public long globalBytes; public long globalDeleted; public long globalDirectories; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java index 550de46a..ba3a1298 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java @@ -13,10 +13,12 @@ import android.provider.MediaStore; import android.util.Log; import com.annimon.stream.Stream; +import com.nutomic.syncthingandroid.BuildConfig; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.activities.DeviceActivity; import com.nutomic.syncthingandroid.activities.FolderActivity; +import com.nutomic.syncthingandroid.model.CompletionInfo; import com.nutomic.syncthingandroid.model.Device; import com.nutomic.syncthingandroid.model.Event; import com.nutomic.syncthingandroid.model.Folder; @@ -93,6 +95,9 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene */ @Override public void onEvent(Event event) { + String deviceId; + String folderId; + switch (event.type) { case "ConfigSaved": if (mApi != null) { @@ -101,8 +106,8 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene } break; case "DeviceRejected": - String deviceId = (String) event.data.get("device"); - Log.d(TAG, "Unknwon device " + deviceId + " wants to connect"); + deviceId = (String) event.data.get("device"); + Log.d(TAG, "Unknown device " + deviceId + " wants to connect"); Intent intent = new Intent(mContext, DeviceActivity.class) .putExtra(DeviceActivity.EXTRA_IS_CREATE, true) @@ -117,9 +122,16 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene notify(title, pi); 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": 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"); Log.d(TAG, "Device " + deviceId + " wants to share folder " + folderId); @@ -170,6 +182,25 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene case "Ping": // Ignored. 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: Log.v(TAG, "Unhandled event " + event.type); } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java b/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java index 35ede351..2ecff8aa 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java @@ -24,11 +24,13 @@ import com.nutomic.syncthingandroid.http.GetRequest; import com.nutomic.syncthingandroid.http.PostConfigRequest; import com.nutomic.syncthingandroid.http.PostScanRequest; 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.Device; import com.nutomic.syncthingandroid.model.Event; 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.SystemInfo; import com.nutomic.syncthingandroid.model.SystemVersion; @@ -95,10 +97,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { private long mPreviousConnectionTime = 0; /** - * Stores the latest result of {@link #getModel} for each folder, for calculating device - * percentage in {@link #getConnections}. + * Stores the latest result of {@link #getFolderStatus} for each folder */ - private final HashMap mCachedModelInfo = new HashMap<>(); + private HashMap mCachedFolderStatuses = new HashMap<>(); + + /** + * Stores the latest result of device and folder completion events. + */ + private Completion mCompletion = new Completion(); @Inject NotificationHandler mNotificationHandler; @@ -143,11 +149,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { tryIsAvailable(); }); new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> { - Log.v(TAG, "onWebGuiAvailable: " + result); - mConfig = new Gson().fromJson(result, Config.class); - if (mConfig == null) { - throw new RuntimeException("config is null: " + result); - } + onReloadConfigComplete(result); tryIsAvailable(); }); getSystemInfo(info -> { @@ -157,13 +159,18 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { } public void reloadConfig() { - new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> { - Log.v(TAG, "reloadConfig: " + result); - mConfig = new Gson().fromJson(result, Config.class); - if (mConfig == null) { - throw new RuntimeException("config is null: " + result); - } - }); + new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, this::onReloadConfigComplete); + } + + private void onReloadConfigComplete(String result) { + Log.v(TAG, "onReloadConfigComplete: " + result); + mConfig = new Gson().fromJson(result, Config.class); + if (mConfig == null) { + 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) { removeFolderInternal(id); + // mCompletion will be updated after the ConfigSaved event. sendConfig(); // Remove saved data from share activity for this folder. PreferenceManager.getDefaultSharedPreferences(mContext).edit() @@ -300,6 +308,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { public void removeDevice(String deviceId) { removeDeviceInternal(deviceId); + // mCompletion will be updated after the ConfigSaved event. sendConfig(); } @@ -374,7 +383,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { mPreviousConnectionTime = now; Connections connections = new Gson().fromJson(result, Connections.class); for (Map.Entry e : connections.connections.entrySet()) { - e.getValue().completion = getDeviceCompletion(e.getKey()); + e.getValue().completion = mCompletion.getDeviceCompletion(e.getKey()); Connections.Connection prev = (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 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. */ - public void getModel(final String folderId, final OnResultListener2 listener) { - new GetRequest(mContext, mUrl, GetRequest.URI_MODEL, mApiKey, + public void getFolderStatus(final String folderId, final OnResultListener2 listener) { + new GetRequest(mContext, mUrl, GetRequest.URI_STATUS, mApiKey, ImmutableMap.of("folder", folderId), result -> { - Model m = new Gson().fromJson(result, Model.class); - mCachedModelInfo.put(folderId, m); + FolderStatus m = new Gson().fromJson(result, FolderStatus.class); + mCachedFolderStatuses.put(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. */ diff --git a/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java b/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java index dc465420..d7f964d0 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java @@ -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 * 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() { Element folder = (Element) mConfig.getDocumentElement() .getElementsByTagName("folder").item(0); - String model = Build.MODEL + String deviceModel = Build.MODEL .replace(" ", "_") .toLowerCase(Locale.US) .replaceAll("[^a-z0-9_-]", ""); 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 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()); folder.setAttribute("type", "readonly"); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/views/FoldersAdapter.java b/app/src/main/java/com/nutomic/syncthingandroid/views/FoldersAdapter.java index 5cef5518..2bf3a228 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/views/FoldersAdapter.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/views/FoldersAdapter.java @@ -15,11 +15,10 @@ import android.widget.ArrayAdapter; import android.widget.TextView; import android.widget.Toast; -import com.nutomic.syncthingandroid.BuildConfig; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.databinding.ItemFolderListBinding; 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.util.Util; @@ -36,7 +35,7 @@ public class FoldersAdapter extends ArrayAdapter { private static final String TAG = "FoldersAdapter"; - private final HashMap mModels = new HashMap<>(); + private final HashMap mLocalFolderStatuses = new HashMap<>(); public FoldersAdapter(Context context) { super(context, 0); @@ -50,7 +49,6 @@ public class FoldersAdapter extends ArrayAdapter { : DataBindingUtil.bind(convertView); Folder folder = getItem(position); - Model model = mModels.get(folder.id); binding.label.setText(TextUtils.isEmpty(folder.label) ? folder.id : folder.label); binding.directory.setText(folder.path); binding.openFolder.setOnClickListener(v -> { @@ -73,48 +71,53 @@ public class FoldersAdapter extends ArrayAdapter { } }); - if (model != null) { - int percentage = (model.globalBytes != 0) - ? Math.round(100 * model.inSyncBytes / model.globalBytes) - : 100; - long neededItems = model.needFiles + model.needDirectories + model.needSymlinks + model.needDeletes; - if (model.state.equals("idle") && neededItems > 0) { - binding.state.setText(getContext().getString(R.string.status_outofsync)); - binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red)); - } else { - if (folder.paused) { - binding.state.setText(getContext().getString(R.string.state_paused)); - binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_black)); - } else { - binding.state.setText(getLocalizedState(getContext(), model.state, percentage)); - switch(model.state) { - case "idle": - binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_green)); - break; - case "scanning": - case "syncing": - binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_blue)); - break; - default: - binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red)); - } - } - } - binding.items.setVisibility(VISIBLE); - binding.items.setText(getContext().getResources() - .getQuantityString(R.plurals.files, (int) model.inSyncFiles, model.inSyncFiles, model.globalFiles)); - binding.size.setVisibility(VISIBLE); - binding.size.setText(getContext().getString(R.string.folder_size_format, - Util.readableFileSize(getContext(), model.inSyncBytes), - Util.readableFileSize(getContext(), model.globalBytes))); - setTextOrHide(binding.invalid, model.invalid); - } else { + updateFolderStatusView(binding, folder); + return binding.getRoot(); + } + + 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; } - return binding.getRoot(); + int percentage = (folderStatus.globalBytes != 0) + ? Math.round(100 * folderStatus.inSyncBytes / folderStatus.globalBytes) + : 100; + long neededItems = folderStatus.needFiles + folderStatus.needDirectories + folderStatus.needSymlinks + folderStatus.needDeletes; + if (folderStatus.state.equals("idle") && neededItems > 0) { + binding.state.setText(getContext().getString(R.string.status_outofsync)); + binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red)); + } else { + if (folder.paused) { + binding.state.setText(getContext().getString(R.string.state_paused)); + binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_black)); + } else { + binding.state.setText(getLocalizedState(getContext(), folderStatus.state, percentage)); + switch(folderStatus.state) { + case "idle": + binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_green)); + break; + case "scanning": + case "syncing": + binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_blue)); + break; + default: + binding.state.setTextColor(ContextCompat.getColor(getContext(), R.color.text_red)); + } + } + } + binding.items.setVisibility(VISIBLE); + binding.items.setText(getContext().getResources() + .getQuantityString(R.plurals.files, (int) folderStatus.inSyncFiles, folderStatus.inSyncFiles, folderStatus.globalFiles)); + binding.size.setVisibility(VISIBLE); + binding.size.setText(getContext().getString(R.string.folder_size_format, + Util.readableFileSize(getContext(), folderStatus.inSyncBytes), + Util.readableFileSize(getContext(), folderStatus.globalBytes))); + setTextOrHide(binding.invalid, folderStatus.invalid); } /** @@ -132,16 +135,16 @@ public class FoldersAdapter extends ArrayAdapter { } /** - * 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++) { - api.getModel(getItem(i).id, this::onReceiveModel); + api.getFolderStatus(getItem(i).id, this::onReceiveFolderStatus); } } - private void onReceiveModel(String folderId, Model model) { - mModels.put(folderId, model); + private void onReceiveFolderStatus(String folderId, FolderStatus folderStatus) { + mLocalFolderStatuses.put(folderId, folderStatus); notifyDataSetChanged(); }