Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 05df8ec3 authored by Garfield, Tan's avatar Garfield, Tan Committed by Garfield Tan
Browse files

[multi-part] Eliminate 1k selection limit

* Rename class ClipDetails.
* Add FileOperation class and merge it with JobFactory.

Bug: 28194201
Change-Id: I46639b21c9e424644289a1bf34b85234a9becd2b
(cherry picked from commit 7f17819f1320d12f1f35a8f5327e0c7e63dfed54)
parent 4be082a1
Loading
Loading
Loading
Loading
+34 −10
Original line number Original line Diff line number Diff line
@@ -32,6 +32,7 @@ import android.util.Log;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;
import com.android.documentsui.services.FileOperations;
@@ -349,28 +350,35 @@ public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChan
            return;
            return;
        }
        }


        ClipDetails details = ClipDetails.createClipDetails(clipData);
        PersistableBundle bundle = clipData.getDescription().getExtras();
        @OpType int opType = getOpType(bundle);
        UrisSupplier uris = UrisSupplier.create(clipData);


        if (!canCopy(destination)) {
        if (!canCopy(destination)) {
            callback.onOperationResult(
            callback.onOperationResult(
                    FileOperations.Callback.STATUS_REJECTED, details.getOpType(), 0);
                    FileOperations.Callback.STATUS_REJECTED, opType, 0);
            return;
            return;
        }
        }


        if (details.getItemCount() == 0) {
        if (uris.getItemCount() == 0) {
            callback.onOperationResult(
            callback.onOperationResult(
                    FileOperations.Callback.STATUS_ACCEPTED, details.getOpType(), 0);
                    FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
            return;
            return;
        }
        }


        DocumentStack dstStack = new DocumentStack();
        DocumentStack dstStack = new DocumentStack(docStack, destination);
        dstStack.push(destination);
        dstStack.addAll(docStack);


        // Pass root here so that we can perform "download" root check when
        String srcParentString = bundle.getString(SRC_PARENT_KEY);
        dstStack.root = docStack.root;
        Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);


        FileOperations.start(mContext, details, dstStack, callback);
        FileOperation operation = new FileOperation.Builder()
                .withOpType(opType)
                .withSrcParent(srcParent)
                .withDestination(dstStack)
                .withSrcs(uris)
                .build();

        FileOperations.start(mContext, operation, callback);
    }
    }


    /**
    /**
@@ -399,8 +407,24 @@ public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChan
        }
        }


        ClipDescription description = data.getDescription();
        ClipDescription description = data.getDescription();
        if (description == null) {
            return ClipStorage.NO_SELECTION_TAG;
        }

        BaseBundle bundle = description.getExtras();
        BaseBundle bundle = description.getExtras();
        if (bundle == null) {
            return ClipStorage.NO_SELECTION_TAG;
        }

        return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
        return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
    }
    }


    public static @OpType int getOpType(ClipData data) {
        PersistableBundle bundle = data.getDescription().getExtras();
        return getOpType(bundle);
    }

    private static @OpType int getOpType(PersistableBundle bundle) {
        return bundle.getInt(OP_TYPE_KEY);
    }
}
}
+2 −3
Original line number Original line Diff line number Diff line
@@ -41,7 +41,6 @@ import android.provider.DocumentsContract;
import android.support.design.widget.Snackbar;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.util.Log;
import android.view.Menu;
import android.view.Menu;
import android.view.MenuItem;


import com.android.documentsui.MenuManager.DirectoryDetails;
import com.android.documentsui.MenuManager.DirectoryDetails;
import com.android.documentsui.RecentsProvider.RecentColumns;
import com.android.documentsui.RecentsProvider.RecentColumns;
@@ -156,7 +155,7 @@ public class DocumentsActivity extends BaseActivity {
            state.directoryCopy = intent.getBooleanExtra(
            state.directoryCopy = intent.getBooleanExtra(
                    Shared.EXTRA_DIRECTORY_COPY, false);
                    Shared.EXTRA_DIRECTORY_COPY, false);
            state.copyOperationSubType = intent.getIntExtra(
            state.copyOperationSubType = intent.getIntExtra(
                    FileOperationService.EXTRA_OPERATION,
                    FileOperationService.EXTRA_OPERATION_TYPE,
                    FileOperationService.OPERATION_COPY);
                    FileOperationService.OPERATION_COPY);
        }
        }
    }
    }
@@ -386,7 +385,7 @@ public class DocumentsActivity extends BaseActivity {
            // Picking a copy destination is only used internally by us, so we
            // Picking a copy destination is only used internally by us, so we
            // don't need to extend permissions to the caller.
            // don't need to extend permissions to the caller.
            intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
            intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
            intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType);
            intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
        } else {
        } else {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+1 −1
Original line number Original line Diff line number Diff line
@@ -139,7 +139,7 @@ public class FilesActivity extends BaseActivity {
        // Only show it manually for the first time (icicle is null).
        // Only show it manually for the first time (icicle is null).
        if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) {
        if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) {
            final int opType = intent.getIntExtra(
            final int opType = intent.getIntExtra(
                    FileOperationService.EXTRA_OPERATION,
                    FileOperationService.EXTRA_OPERATION_TYPE,
                    FileOperationService.OPERATION_COPY);
                    FileOperationService.OPERATION_COPY);
            final ArrayList<DocumentInfo> srcList =
            final ArrayList<DocumentInfo> srcList =
                    intent.getParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST);
                    intent.getParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST);
+2 −2
Original line number Original line Diff line number Diff line
@@ -60,7 +60,7 @@ public class OperationDialogFragment extends DialogFragment {
            @OpType int operationType) {
            @OpType int operationType) {
        final Bundle args = new Bundle();
        final Bundle args = new Bundle();
        args.putInt(FileOperationService.EXTRA_DIALOG_TYPE, dialogType);
        args.putInt(FileOperationService.EXTRA_DIALOG_TYPE, dialogType);
        args.putInt(FileOperationService.EXTRA_OPERATION, operationType);
        args.putInt(FileOperationService.EXTRA_OPERATION_TYPE, operationType);
        args.putParcelableArrayList(FileOperationService.EXTRA_SRC_LIST, failedSrcList);
        args.putParcelableArrayList(FileOperationService.EXTRA_SRC_LIST, failedSrcList);


        final FragmentTransaction ft = fm.beginTransaction();
        final FragmentTransaction ft = fm.beginTransaction();
@@ -78,7 +78,7 @@ public class OperationDialogFragment extends DialogFragment {
        final @DialogType int dialogType =
        final @DialogType int dialogType =
              getArguments().getInt(FileOperationService.EXTRA_DIALOG_TYPE);
              getArguments().getInt(FileOperationService.EXTRA_DIALOG_TYPE);
        final @OpType int operationType =
        final @OpType int operationType =
              getArguments().getInt(FileOperationService.EXTRA_OPERATION);
              getArguments().getInt(FileOperationService.EXTRA_OPERATION_TYPE);
        final ArrayList<DocumentInfo> srcList = getArguments().getParcelableArrayList(
        final ArrayList<DocumentInfo> srcList = getArguments().getParcelableArrayList(
                FileOperationService.EXTRA_SRC_LIST);
                FileOperationService.EXTRA_SRC_LIST);


+275 −0
Original line number Original line Diff line number Diff line
@@ -18,11 +18,7 @@ package com.android.documentsui;


import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_SIZE;
import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_SIZE;
import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_TAG;
import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_TAG;
import static com.android.documentsui.DocumentClipper.OP_TYPE_KEY;
import static com.android.documentsui.DocumentClipper.SRC_PARENT_KEY;


import android.annotation.CallSuper;
import android.annotation.Nullable;
import android.content.ClipData;
import android.content.ClipData;
import android.content.Context;
import android.content.Context;
import android.net.Uri;
import android.net.Uri;
@@ -33,67 +29,33 @@ import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.Log;


import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService.OpType;


import java.io.IOException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collection;
import java.util.List;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Function;


/**
/**
 * ClipDetails is a parcelable project providing information of different type of file
 * UrisSupplier provides doc uri list to {@link FileOperation}.
 * management operations like cut, move and copy.
 *
 *
 * Under the hood it provides cross-process synchronization support such that its consumer doesn't
 * <p>Under the hood it provides cross-process synchronization support such that its consumer doesn't
 * need to explicitly synchronize its access.
 * need to explicitly synchronize its access.
 */
 */
public abstract class ClipDetails implements Parcelable {
public abstract class UrisSupplier implements Parcelable {
    private final @OpType int mOpType;

    // This field is used only for moving and deleting. Currently it's not the case,
    // but in the future those files may be from multiple different parents. In
    // such case, this needs to be replaced with pairs of parent and child.
    private final @Nullable Uri mSrcParent;

    private ClipDetails(ClipData clipData) {
        PersistableBundle bundle = clipData.getDescription().getExtras();
        mOpType = bundle.getInt(OP_TYPE_KEY);

        String srcParentString = bundle.getString(SRC_PARENT_KEY);
        mSrcParent = (srcParentString == null) ? null : Uri.parse(srcParentString);

        // Only copy doesn't need src parent
        assert(mOpType == FileOperationService.OPERATION_COPY || mSrcParent != null);
    }

    private ClipDetails(@OpType int opType, @Nullable Uri srcParent) {
        mOpType = opType;
        mSrcParent = srcParent;

        // Only copy doesn't need src parent
        assert(mOpType == FileOperationService.OPERATION_COPY || mSrcParent != null);
    }

    public @OpType int getOpType() {
        return mOpType;
    }

    public @Nullable Uri getSrcParent() {
        return mSrcParent;
    }


    public abstract int getItemCount();
    public abstract int getItemCount();


    /**
    /**
     * Gets doc list from this clip detail. This may only be called once because it may read a file
     * Gets doc list. This may only be called once because it may read a file
     * to get the list.
     * to get the list.
     *
     * @param context We need context to obtain {@link ClipStorage}. It can't be sent in a parcel.
     */
     */
    public Iterable<Uri> getDocs(Context context) throws IOException {
    public Iterable<Uri> getDocs(Context context) throws IOException {
        ClipStorage storage = DocumentsApplication.getClipStorage(context);
        return getDocs(DocumentsApplication.getClipStorage(context));

        return getDocs(storage);
    }
    }


    @VisibleForTesting
    @VisibleForTesting
@@ -107,41 +69,24 @@ public abstract class ClipDetails implements Parcelable {
    @VisibleForTesting
    @VisibleForTesting
    void dispose(ClipStorage storage) {}
    void dispose(ClipStorage storage) {}


    private ClipDetails(Parcel in) {
        mOpType = in.readInt();
        mSrcParent = in.readParcelable(ClassLoader.getSystemClassLoader());
    }

    @Override
    @Override
    public int describeContents() {
    public int describeContents() {
        return 0;
        return 0;
    }
    }


    @CallSuper
    public static UrisSupplier create(ClipData clipData) {
    @Override
        UrisSupplier uris;
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mOpType);
        dest.writeParcelable(mSrcParent, 0);
    }

    private void appendTo(StringBuilder builder) {
        builder.append("opType=").append(mOpType);
        builder.append(", srcParent=").append(mSrcParent);
    }

    public static ClipDetails createClipDetails(ClipData clipData) {
        ClipDetails details;
        PersistableBundle bundle = clipData.getDescription().getExtras();
        PersistableBundle bundle = clipData.getDescription().getExtras();
        if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) {
        if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) {
            details = new JumboClipDetails(clipData);
            uris = new JumboUrisSupplier(clipData);
        } else {
        } else {
            details = new StandardClipDetails(clipData);
            uris = new StandardUrisSupplier(clipData);
        }
        }


        return details;
        return uris;
    }
    }


    public static ClipDetails createClipDetails(@OpType int opType, @Nullable Uri srcParent,
    public static UrisSupplier create(
            Selection selection, Function<String, Uri> uriBuilder, Context context) {
            Selection selection, Function<String, Uri> uriBuilder, Context context) {
        ClipStorage storage = DocumentsApplication.getClipStorage(context);
        ClipStorage storage = DocumentsApplication.getClipStorage(context);


@@ -150,30 +95,28 @@ public abstract class ClipDetails implements Parcelable {
            uris.add(uriBuilder.apply(id));
            uris.add(uriBuilder.apply(id));
        }
        }


        return createClipDetails(opType, srcParent, uris, storage);
        return create(uris, storage);
    }
    }


    @VisibleForTesting
    @VisibleForTesting
    static ClipDetails createClipDetails(@OpType int opType, @Nullable Uri srcParent,
    static UrisSupplier create(List<Uri> uris, ClipStorage storage) {
            List<Uri> uris, ClipStorage storage) {
        UrisSupplier urisSupplier = (uris.size() > Shared.MAX_DOCS_IN_INTENT)
        ClipDetails details = (uris.size() > Shared.MAX_DOCS_IN_INTENT)
                ? new JumboUrisSupplier(uris, storage)
                ? new JumboClipDetails(opType, srcParent, uris, storage)
                : new StandardUrisSupplier(uris);
                : new StandardClipDetails(opType, srcParent, uris);


        return details;
        return urisSupplier;
    }
    }


    private static class JumboClipDetails extends ClipDetails {
    private static class JumboUrisSupplier extends UrisSupplier {
        private static final String TAG = "JumboClipDetails";
        private static final String TAG = "JumboUrisSupplier";


        private final long mSelectionTag;
        private final long mSelectionTag;
        private final int mSelectionSize;
        private final int mSelectionSize;


        private transient ClipStorage.Reader mReader;
        private final transient AtomicReference<ClipStorage.Reader> mReader =

                new AtomicReference<>();
        private JumboClipDetails(ClipData clipData) {
            super(clipData);


        private JumboUrisSupplier(ClipData clipData) {
            PersistableBundle bundle = clipData.getDescription().getExtras();
            PersistableBundle bundle = clipData.getDescription().getExtras();
            mSelectionTag = bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
            mSelectionTag = bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
            assert(mSelectionTag != ClipStorage.NO_SELECTION_TAG);
            assert(mSelectionTag != ClipStorage.NO_SELECTION_TAG);
@@ -182,10 +125,7 @@ public abstract class ClipDetails implements Parcelable {
            assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT);
            assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT);
        }
        }


        private JumboClipDetails(@OpType int opType, @Nullable Uri srcParent, Collection<Uri> uris,
        private JumboUrisSupplier(Collection<Uri> uris, ClipStorage storage) {
                ClipStorage storage) {
            super(opType, srcParent);

            mSelectionTag = storage.createTag();
            mSelectionTag = storage.createTag();
            new ClipStorage.PersistTask(storage, uris, mSelectionTag).execute();
            new ClipStorage.PersistTask(storage, uris, mSelectionTag).execute();
            mSelectionSize = uris.size();
            mSelectionSize = uris.size();
@@ -197,26 +137,27 @@ public abstract class ClipDetails implements Parcelable {
        }
        }


        @Override
        @Override
        public Iterable<Uri> getDocs(ClipStorage storage) throws IOException {
        Iterable<Uri> getDocs(ClipStorage storage) throws IOException {
            if (mReader != null) {
            ClipStorage.Reader reader = mReader.getAndSet(storage.createReader(mSelectionTag));
                throw new IllegalStateException(
            if (reader != null) {
                        "JumboClipDetails#getDocs() can only be called once.");
                reader.close();
                mReader.get().close();
                throw new IllegalStateException("This method can only be called once.");
            }
            }


            mReader = storage.createReader(mSelectionTag);
            return mReader.get();

            return mReader;
        }
        }


        @Override
        @Override
        void dispose(ClipStorage storage) {
        void dispose(ClipStorage storage) {
            if (mReader != null) {
            try {
            try {
                    mReader.close();
                ClipStorage.Reader reader = mReader.get();
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
            } catch (IOException e) {
                Log.w(TAG, "Failed to close the reader.", e);
                Log.w(TAG, "Failed to close the reader.", e);
            }
            }
            }
            try {
            try {
                storage.delete(mSelectionTag);
                storage.delete(mSelectionTag);
            } catch(IOException e) {
            } catch(IOException e) {
@@ -227,9 +168,8 @@ public abstract class ClipDetails implements Parcelable {
        @Override
        @Override
        public String toString() {
        public String toString() {
            StringBuilder builder = new StringBuilder();
            StringBuilder builder = new StringBuilder();
            builder.append("JumboClipDetails{");
            builder.append("JumboUrisSupplier{");
            super.appendTo(builder);
            builder.append("selectionTag=").append(mSelectionTag);
            builder.append(", selectionTag=").append(mSelectionTag);
            builder.append(", selectionSize=").append(mSelectionSize);
            builder.append(", selectionSize=").append(mSelectionSize);
            builder.append("}");
            builder.append("}");
            return builder.toString();
            return builder.toString();
@@ -237,47 +177,44 @@ public abstract class ClipDetails implements Parcelable {


        @Override
        @Override
        public void writeToParcel(Parcel dest, int flags) {
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);

            dest.writeLong(mSelectionTag);
            dest.writeLong(mSelectionTag);
            dest.writeInt(mSelectionSize);
            dest.writeInt(mSelectionSize);
        }
        }


        private JumboClipDetails(Parcel in) {
        private JumboUrisSupplier(Parcel in) {
            super(in);

            mSelectionTag = in.readLong();
            mSelectionTag = in.readLong();
            mSelectionSize = in.readInt();
            mSelectionSize = in.readInt();
        }
        }


        public static final Parcelable.Creator<JumboClipDetails> CREATOR =
        public static final Parcelable.Creator<JumboUrisSupplier> CREATOR =
                new Parcelable.Creator<JumboClipDetails>() {
                new Parcelable.Creator<JumboUrisSupplier>() {


                    @Override
                    @Override
                    public JumboClipDetails createFromParcel(Parcel source) {
                    public JumboUrisSupplier createFromParcel(Parcel source) {
                        return new JumboClipDetails(source);
                        return new JumboUrisSupplier(source);
                    }
                    }


                    @Override
                    @Override
                    public JumboClipDetails[] newArray(int size) {
                    public JumboUrisSupplier[] newArray(int size) {
                        return new JumboClipDetails[size];
                        return new JumboUrisSupplier[size];
                    }
                    }
                };
                };
    }
    }


    /**
     * This class and its constructor is visible for testing to create test doubles of
     * {@link UrisSupplier}.
     */
    @VisibleForTesting
    @VisibleForTesting
    public static class StandardClipDetails extends ClipDetails {
    public static class StandardUrisSupplier extends UrisSupplier {
        private final List<Uri> mDocs;
        private final List<Uri> mDocs;


        private StandardClipDetails(ClipData clipData) {
        private StandardUrisSupplier(ClipData clipData) {
            super(clipData);
            mDocs = listDocs(clipData);
            mDocs = listDocs(clipData);
        }
        }


        @VisibleForTesting
        @VisibleForTesting
        public StandardClipDetails(@OpType int opType, @Nullable Uri srcParent, List<Uri> docs) {
        public StandardUrisSupplier(List<Uri> docs) {
            super(opType, srcParent);

            mDocs = docs;
            mDocs = docs;
        }
        }


@@ -299,44 +236,39 @@ public abstract class ClipDetails implements Parcelable {
        }
        }


        @Override
        @Override
        public Iterable<Uri> getDocs(ClipStorage storage) {
        Iterable<Uri> getDocs(ClipStorage storage) {
            return mDocs;
            return mDocs;
        }
        }


        @Override
        @Override
        public String toString() {
        public String toString() {
            StringBuilder builder = new StringBuilder();
            StringBuilder builder = new StringBuilder();
            builder.append("StandardClipDetails{");
            builder.append("StandardUrisSupplier{");
            super.appendTo(builder);
            builder.append("docs=").append(mDocs.toString());
            builder.append(", ").append("docs=").append(mDocs.toString());
            builder.append("}");
            builder.append("}");
            return builder.toString();
            return builder.toString();
        }
        }


        @Override
        @Override
        public void writeToParcel(Parcel dest, int flags) {
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);

            dest.writeTypedList(mDocs);
            dest.writeTypedList(mDocs);
        }
        }


        private StandardClipDetails(Parcel in) {
        private StandardUrisSupplier(Parcel in) {
            super(in);

            mDocs = in.createTypedArrayList(Uri.CREATOR);
            mDocs = in.createTypedArrayList(Uri.CREATOR);
        }
        }


        public static final Parcelable.Creator<StandardClipDetails> CREATOR =
        public static final Parcelable.Creator<StandardUrisSupplier> CREATOR =
                new Parcelable.Creator<StandardClipDetails>() {
                new Parcelable.Creator<StandardUrisSupplier>() {


                    @Override
                    @Override
                    public StandardClipDetails createFromParcel(Parcel source) {
                    public StandardUrisSupplier createFromParcel(Parcel source) {
                        return new StandardClipDetails(source);
                        return new StandardUrisSupplier(source);
                    }
                    }


                    @Override
                    @Override
                    public StandardClipDetails[] newArray(int size) {
                    public StandardUrisSupplier[] newArray(int size) {
                        return new StandardClipDetails[size];
                        return new StandardUrisSupplier[size];
                    }
                    }
                };
                };
    }
    }
Loading