|
@@ -0,0 +1,413 @@
|
|
|
|
+/**
|
|
|
|
+ * Nextcloud Android client application
|
|
|
|
+ *
|
|
|
|
+ * @author Andy Scherzinger
|
|
|
|
+ * Copyright (C) 2016 Andy Scherzinger
|
|
|
|
+ * Copyright (C) 2016 Nextcloud
|
|
|
|
+ * <p>
|
|
|
|
+ * This program is free software; you can redistribute it and/or
|
|
|
|
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
|
|
+ * License as published by the Free Software Foundation; either
|
|
|
|
+ * version 3 of the License, or any later version.
|
|
|
|
+ * <p>
|
|
|
|
+ * This program is distributed in the hope that it will be useful,
|
|
|
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
|
|
|
+ * <p>
|
|
|
|
+ * You should have received a copy of the GNU Affero General Public
|
|
|
|
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+package com.owncloud.android.ui.activity;
|
|
|
|
+
|
|
|
|
+import android.content.Intent;
|
|
|
|
+import android.os.Bundle;
|
|
|
|
+import android.os.Handler;
|
|
|
|
+import android.support.annotation.NonNull;
|
|
|
|
+import android.support.v4.app.FragmentManager;
|
|
|
|
+import android.support.v4.app.FragmentTransaction;
|
|
|
|
+import android.support.v7.widget.GridLayoutManager;
|
|
|
|
+import android.support.v7.widget.RecyclerView;
|
|
|
|
+import android.view.MenuItem;
|
|
|
|
+import android.view.View;
|
|
|
|
+import android.widget.LinearLayout;
|
|
|
|
+import android.widget.TextView;
|
|
|
|
+
|
|
|
|
+import com.owncloud.android.MainApp;
|
|
|
|
+import com.owncloud.android.R;
|
|
|
|
+import com.owncloud.android.authentication.AccountUtils;
|
|
|
|
+import com.owncloud.android.datamodel.MediaFolder;
|
|
|
|
+import com.owncloud.android.datamodel.MediaProvider;
|
|
|
|
+import com.owncloud.android.datamodel.OCFile;
|
|
|
|
+import com.owncloud.android.datamodel.SyncedFolder;
|
|
|
|
+import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
|
|
|
|
+import com.owncloud.android.datamodel.SyncedFolderProvider;
|
|
|
|
+import com.owncloud.android.ui.adapter.FolderSyncAdapter;
|
|
|
|
+import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
|
|
|
|
+import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
|
|
|
|
+import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
|
|
|
|
+
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.Collections;
|
|
|
|
+import java.util.Comparator;
|
|
|
|
+import java.util.HashMap;
|
|
|
|
+import java.util.List;
|
|
|
|
+import java.util.Map;
|
|
|
|
+import java.util.TimerTask;
|
|
|
|
+
|
|
|
|
+import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Activity displaying all auto-synced folders and/or instant upload media folders.
|
|
|
|
+ */
|
|
|
|
+public class FolderSyncActivity extends FileActivity implements FolderSyncAdapter.ClickListener,
|
|
|
|
+ SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener {
|
|
|
|
+ private static final String TAG = FolderSyncActivity.class.getSimpleName();
|
|
|
|
+
|
|
|
|
+ private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
|
|
|
|
+ public static final String PRIORITIZED_FOLDER = "Camera";
|
|
|
|
+
|
|
|
|
+ private RecyclerView mRecyclerView;
|
|
|
|
+ private FolderSyncAdapter mAdapter;
|
|
|
|
+ private LinearLayout mProgress;
|
|
|
|
+ private TextView mEmpty;
|
|
|
|
+ private SyncedFolderProvider mSyncedFolderProvider;
|
|
|
|
+ private List<SyncedFolderDisplayItem> syncFolderItems;
|
|
|
|
+ private SyncedFolderPreferencesDialogFragment mSyncedFolderPreferencesDialogFragment;
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ protected void onCreate(Bundle savedInstanceState) {
|
|
|
|
+ super.onCreate(savedInstanceState);
|
|
|
|
+
|
|
|
|
+ setContentView(R.layout.folder_sync_layout);
|
|
|
|
+
|
|
|
|
+ // setup toolbar
|
|
|
|
+ setupToolbar();
|
|
|
|
+
|
|
|
|
+ // setup drawer
|
|
|
|
+ setupDrawer(R.id.nav_folder_sync);
|
|
|
|
+ getSupportActionBar().setTitle(getString(R.string.drawer_folder_sync));
|
|
|
|
+
|
|
|
|
+ setupContent();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * sets up the UI elements and loads all media/synced folders.
|
|
|
|
+ */
|
|
|
|
+ private void setupContent() {
|
|
|
|
+ mRecyclerView = (RecyclerView) findViewById(android.R.id.list);
|
|
|
|
+
|
|
|
|
+ mProgress = (LinearLayout) findViewById(android.R.id.progress);
|
|
|
|
+ mEmpty = (TextView) findViewById(android.R.id.empty);
|
|
|
|
+
|
|
|
|
+ final int gridWidth = getResources().getInteger(R.integer.media_grid_width);
|
|
|
|
+ mAdapter = new FolderSyncAdapter(this, gridWidth, this);
|
|
|
|
+ mSyncedFolderProvider = new SyncedFolderProvider(getContentResolver());
|
|
|
|
+
|
|
|
|
+ final GridLayoutManager lm = new GridLayoutManager(this, gridWidth);
|
|
|
|
+ mAdapter.setLayoutManager(lm);
|
|
|
|
+ int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
|
|
|
|
+ mRecyclerView.addItemDecoration(new MediaGridItemDecoration(spacing));
|
|
|
|
+ mRecyclerView.setLayoutManager(lm);
|
|
|
|
+ mRecyclerView.setAdapter(mAdapter);
|
|
|
|
+
|
|
|
|
+ load(gridWidth * 2);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * loads all media/synced folders, adds them to the recycler view adapter and shows the list.
|
|
|
|
+ *
|
|
|
|
+ * @param perFolderMediaItemLimit the amount of media items to be loaded/shown per media folder
|
|
|
|
+ */
|
|
|
|
+ private void load(final int perFolderMediaItemLimit) {
|
|
|
|
+ if (mAdapter.getItemCount() > 0) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ setListShown(false);
|
|
|
|
+ final Handler mHandler = new Handler();
|
|
|
|
+ new Thread(new Runnable() {
|
|
|
|
+ @Override
|
|
|
|
+ public void run() {
|
|
|
|
+ final List<MediaFolder> mediaFolders = MediaProvider.getMediaFolders(getContentResolver(),
|
|
|
|
+ perFolderMediaItemLimit);
|
|
|
|
+ syncFolderItems = sortSyncedFolderItems(mergeFolderData(mSyncedFolderProvider.getSyncedFolders(),
|
|
|
|
+ mediaFolders));
|
|
|
|
+
|
|
|
|
+ mHandler.post(new TimerTask() {
|
|
|
|
+ @Override
|
|
|
|
+ public void run() {
|
|
|
|
+ mAdapter.setSyncFolderItems(syncFolderItems);
|
|
|
|
+ setListShown(true);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }).start();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * merges two lists of {@link SyncedFolder} and {@link MediaFolder} items into one of SyncedFolderItems.
|
|
|
|
+ *
|
|
|
|
+ * @param syncedFolders the synced folders
|
|
|
|
+ * @param mediaFolders the media folders
|
|
|
|
+ * @return the merged list of SyncedFolderItems
|
|
|
|
+ */
|
|
|
|
+ @NonNull
|
|
|
|
+ private List<SyncedFolderDisplayItem> mergeFolderData(List<SyncedFolder> syncedFolders,
|
|
|
|
+ @NonNull List<MediaFolder> mediaFolders) {
|
|
|
|
+ Map<String, SyncedFolder> syncedFoldersMap = createSyncedFoldersMap(syncedFolders);
|
|
|
|
+ List<SyncedFolderDisplayItem> result = new ArrayList<>();
|
|
|
|
+
|
|
|
|
+ for (MediaFolder mediaFolder : mediaFolders) {
|
|
|
|
+ if (syncedFoldersMap.containsKey(mediaFolder.absolutePath)) {
|
|
|
|
+ SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath);
|
|
|
|
+ result.add(createSyncedFolder(syncedFolder, mediaFolder));
|
|
|
|
+ } else {
|
|
|
|
+ result.add(createSyncedFolderFromMediaFolder(mediaFolder));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Sorts list of {@link SyncedFolderDisplayItem}s.
|
|
|
|
+ *
|
|
|
|
+ * @param syncFolderItemList list of items to be sorted
|
|
|
|
+ * @return sorted list of items
|
|
|
|
+ */
|
|
|
|
+ public static List<SyncedFolderDisplayItem> sortSyncedFolderItems(List<SyncedFolderDisplayItem>
|
|
|
|
+ syncFolderItemList) {
|
|
|
|
+ Collections.sort(syncFolderItemList, new Comparator<SyncedFolderDisplayItem>() {
|
|
|
|
+ public int compare(SyncedFolderDisplayItem f1, SyncedFolderDisplayItem f2) {
|
|
|
|
+ if (f1 == null && f2 == null) {
|
|
|
|
+ return 0;
|
|
|
|
+ } else if (f1 == null) {
|
|
|
|
+ return -1;
|
|
|
|
+ } else if (f2 == null) {
|
|
|
|
+ return 1;
|
|
|
|
+ } else if (f1.isEnabled() && f2.isEnabled()) {
|
|
|
|
+ return f1.getFolderName().toLowerCase().compareTo(f2.getFolderName().toLowerCase());
|
|
|
|
+ } else if (f1.isEnabled()) {
|
|
|
|
+ return -1;
|
|
|
|
+ } else if (f2.isEnabled()) {
|
|
|
|
+ return 1;
|
|
|
|
+ } else if (f1.getFolderName() == null && f2.getFolderName() == null) {
|
|
|
|
+ return 0;
|
|
|
|
+ } else if (f1.getFolderName() == null) {
|
|
|
|
+ return -1;
|
|
|
|
+ } else if (f2.getFolderName() == null) {
|
|
|
|
+ return 1;
|
|
|
|
+ } else if (PRIORITIZED_FOLDER.equals(f1.getFolderName())) {
|
|
|
|
+ return -1;
|
|
|
|
+ } else if (PRIORITIZED_FOLDER.equals(f2.getFolderName())) {
|
|
|
|
+ return 1;
|
|
|
|
+ } else {
|
|
|
|
+ return f1.getFolderName().toLowerCase().compareTo(f2.getFolderName().toLowerCase());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return syncFolderItemList;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * creates a SyncedFolderDisplayItem merging a {@link SyncedFolder} and a {@link MediaFolder} object instance.
|
|
|
|
+ *
|
|
|
|
+ * @param syncedFolder the synced folder object
|
|
|
|
+ * @param mediaFolder the media folder object
|
|
|
|
+ * @return the created SyncedFolderDisplayItem
|
|
|
|
+ */
|
|
|
|
+ @NonNull
|
|
|
|
+ private SyncedFolderDisplayItem createSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull MediaFolder mediaFolder) {
|
|
|
|
+ return new SyncedFolderDisplayItem(
|
|
|
|
+ syncedFolder.getId(),
|
|
|
|
+ syncedFolder.getLocalPath(),
|
|
|
|
+ syncedFolder.getRemotePath(),
|
|
|
|
+ syncedFolder.getWifiOnly(),
|
|
|
|
+ syncedFolder.getChargingOnly(),
|
|
|
|
+ syncedFolder.getSubfolderByDate(),
|
|
|
|
+ syncedFolder.getAccount(),
|
|
|
|
+ syncedFolder.getUploadAction(),
|
|
|
|
+ syncedFolder.isEnabled(),
|
|
|
|
+ mediaFolder.filePaths,
|
|
|
|
+ mediaFolder.folderName,
|
|
|
|
+ mediaFolder.numberOfFiles);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * creates a {@link SyncedFolderDisplayItem} based on a {@link MediaFolder} object instance.
|
|
|
|
+ *
|
|
|
|
+ * @param mediaFolder the media folder object
|
|
|
|
+ * @return the created SyncedFolderDisplayItem
|
|
|
|
+ */
|
|
|
|
+ @NonNull
|
|
|
|
+ private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull MediaFolder mediaFolder) {
|
|
|
|
+ return new SyncedFolderDisplayItem(
|
|
|
|
+ UNPERSISTED_ID,
|
|
|
|
+ mediaFolder.absolutePath,
|
|
|
|
+ getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName,
|
|
|
|
+ true,
|
|
|
|
+ false,
|
|
|
|
+ false,
|
|
|
|
+ AccountUtils.getCurrentOwnCloudAccount(this).name,
|
|
|
|
+ 0,
|
|
|
|
+ false,
|
|
|
|
+ mediaFolder.filePaths,
|
|
|
|
+ mediaFolder.folderName,
|
|
|
|
+ mediaFolder.numberOfFiles);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * creates a lookup map for a list of given {@link SyncedFolder}s with their local path as the key.
|
|
|
|
+ *
|
|
|
|
+ * @param syncFolders list of {@link SyncedFolder}s
|
|
|
|
+ * @return the lookup map for {@link SyncedFolder}s
|
|
|
|
+ */
|
|
|
|
+ @NonNull
|
|
|
|
+ private Map<String, SyncedFolder> createSyncedFoldersMap(List<SyncedFolder> syncFolders) {
|
|
|
|
+ Map<String, SyncedFolder> result = new HashMap<>();
|
|
|
|
+ if (syncFolders != null) {
|
|
|
|
+ for (SyncedFolder syncFolder : syncFolders) {
|
|
|
|
+ result.put(syncFolder.getLocalPath(), syncFolder);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * show/hide recycler view list or the empty message / progress info.
|
|
|
|
+ *
|
|
|
|
+ * @param shown flag if list should be shown
|
|
|
|
+ */
|
|
|
|
+ private void setListShown(boolean shown) {
|
|
|
|
+ if (mRecyclerView != null) {
|
|
|
|
+ mRecyclerView.setVisibility(shown ? View.VISIBLE : View.GONE);
|
|
|
|
+ mProgress.setVisibility(shown ? View.GONE : View.VISIBLE);
|
|
|
|
+ mEmpty.setVisibility(shown && mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public boolean onOptionsItemSelected(MenuItem item) {
|
|
|
|
+ boolean result;
|
|
|
|
+ switch (item.getItemId()) {
|
|
|
|
+ case android.R.id.home: {
|
|
|
|
+ if (isDrawerOpen()) {
|
|
|
|
+ closeDrawer();
|
|
|
|
+ } else {
|
|
|
|
+ openDrawer();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ default:
|
|
|
|
+ result = super.onOptionsItemSelected(item);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void restart() {
|
|
|
|
+ Intent i = new Intent(this, FileDisplayActivity.class);
|
|
|
|
+ i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
|
|
+ startActivity(i);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void showFiles(boolean onDeviceOnly) {
|
|
|
|
+ MainApp.showOnlyFilesOnDevice(onDeviceOnly);
|
|
|
|
+ Intent fileDisplayActivity = new Intent(getApplicationContext(), FileDisplayActivity.class);
|
|
|
|
+ fileDisplayActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
|
|
+ startActivity(fileDisplayActivity);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
|
|
|
|
+ if (syncedFolderDisplayItem.getId() > UNPERSISTED_ID) {
|
|
|
|
+ mSyncedFolderProvider.updateFolderSyncEnabled(syncedFolderDisplayItem.getId(), syncedFolderDisplayItem.isEnabled());
|
|
|
|
+ } else {
|
|
|
|
+ mSyncedFolderProvider.storeFolderSync(syncedFolderDisplayItem);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
|
|
|
|
+ FragmentManager fm = getSupportFragmentManager();
|
|
|
|
+ FragmentTransaction ft = fm.beginTransaction();
|
|
|
|
+ ft.addToBackStack(null);
|
|
|
|
+
|
|
|
|
+ mSyncedFolderPreferencesDialogFragment = SyncedFolderPreferencesDialogFragment.newInstance(
|
|
|
|
+ syncedFolderDisplayItem, section);
|
|
|
|
+ mSyncedFolderPreferencesDialogFragment.show(ft, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
|
|
+ if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_REMOTE_FOLDER
|
|
|
|
+ && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
|
|
|
|
+ OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
|
|
|
|
+ mSyncedFolderPreferencesDialogFragment.setRemoteFolderSummary(chosenFolder.getRemotePath());
|
|
|
|
+
|
|
|
|
+ } else {
|
|
|
|
+ super.onActivityResult(requestCode, resultCode, data);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
|
|
|
|
+ SyncedFolderDisplayItem item = syncFolderItems.get(syncedFolder.getSection());
|
|
|
|
+ boolean dirty = item.isEnabled() != syncedFolder.getEnabled();
|
|
|
|
+ item = updateSyncedFolderItem(item, syncedFolder.getLocalPath(), syncedFolder.getRemotePath(), syncedFolder
|
|
|
|
+ .getWifiOnly(), syncedFolder.getChargingOnly(), syncedFolder.getSubfolderByDate(), syncedFolder
|
|
|
|
+ .getUploadAction(), syncedFolder.getEnabled());
|
|
|
|
+
|
|
|
|
+ if (syncedFolder.getId() == UNPERSISTED_ID) {
|
|
|
|
+ // newly set up folder sync config
|
|
|
|
+ mSyncedFolderProvider.storeFolderSync(item);
|
|
|
|
+ } else {
|
|
|
|
+ // existing synced folder setup to be updated
|
|
|
|
+ mSyncedFolderProvider.updateSyncFolder(item);
|
|
|
|
+ }
|
|
|
|
+ mSyncedFolderPreferencesDialogFragment = null;
|
|
|
|
+
|
|
|
|
+ if(dirty) {
|
|
|
|
+ mAdapter.setSyncFolderItem(syncedFolder.getSection(), item);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void onCancelSyncedFolderPreference() {
|
|
|
|
+ mSyncedFolderPreferencesDialogFragment = null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * update given synced folder with the given values.
|
|
|
|
+ *
|
|
|
|
+ * @param item the synced folder to be updated
|
|
|
|
+ * @param localPath the local path
|
|
|
|
+ * @param remotePath the remote path
|
|
|
|
+ * @param wifiOnly upload on wifi only
|
|
|
|
+ * @param chargingOnly upload on charging only
|
|
|
|
+ * @param subfolderByDate created sub folders
|
|
|
|
+ * @param uploadAction upload action
|
|
|
|
+ * @param enabled is sync enabled
|
|
|
|
+ * @return the updated item
|
|
|
|
+ */
|
|
|
|
+ private SyncedFolderDisplayItem updateSyncedFolderItem(SyncedFolderDisplayItem item,
|
|
|
|
+ String localPath,
|
|
|
|
+ String remotePath,
|
|
|
|
+ Boolean wifiOnly,
|
|
|
|
+ Boolean chargingOnly,
|
|
|
|
+ Boolean subfolderByDate,
|
|
|
|
+ Integer uploadAction,
|
|
|
|
+ Boolean enabled) {
|
|
|
|
+ item.setLocalPath(localPath);
|
|
|
|
+ item.setRemotePath(remotePath);
|
|
|
|
+ item.setWifiOnly(wifiOnly);
|
|
|
|
+ item.setChargingOnly(chargingOnly);
|
|
|
|
+ item.setSubfolderByDate(subfolderByDate);
|
|
|
|
+ item.setUploadAction(uploadAction);
|
|
|
|
+ item.setEnabled(enabled);
|
|
|
|
+ return item;
|
|
|
|
+ }
|
|
|
|
+}
|