1c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay/*
2c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * Copyright (C) 2016 The Android Open Source Project
3c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay *
4c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * Licensed under the Apache License, Version 2.0 (the "License");
5c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * you may not use this file except in compliance with the License.
6c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * You may obtain a copy of the License at
7c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay *
8c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay *      http://www.apache.org/licenses/LICENSE-2.0
9c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay *
10c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * Unless required by applicable law or agreed to in writing, software
11c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * distributed under the License is distributed on an "AS IS" BASIS,
12c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * See the License for the specific language governing permissions and
14c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay * limitations under the License.
15c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay */
16c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKaypackage com.android.documentsui.ui;
17c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
18c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport android.app.Activity;
19c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport android.app.AlertDialog;
2023ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tanimport android.app.FragmentManager;
21c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport android.content.DialogInterface;
22eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKayimport android.support.design.widget.Snackbar;
236f6d578302425aca2eb5ec2bd278be9a77065172Ben Linimport android.widget.Button;
24c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport android.widget.TextView;
25c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
26c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport com.android.documentsui.R;
27c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport com.android.documentsui.base.ConfirmationCallback;
28c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport com.android.documentsui.base.DocumentInfo;
290d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewskiimport com.android.documentsui.base.Features;
3023ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tanimport com.android.documentsui.picker.OverwriteConfirmFragment;
310d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewskiimport com.android.documentsui.services.FileOperation;
32c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport com.android.documentsui.services.FileOperationService.OpType;
330d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewskiimport com.android.documentsui.services.FileOperationService;
34c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport com.android.documentsui.services.FileOperations.Callback.Status;
350d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewskiimport com.android.documentsui.services.FileOperations;
36c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
37c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKayimport java.util.List;
380d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewskiimport javax.annotation.Nullable;
39c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
40c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKaypublic interface DialogController {
41c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
4223ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan    // Dialogs used in FilesActivity
43c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback);
4468689622255bc831ad0f832de4c8f82c319fdb29Ben Lin    void showFileOperationStatus(int status, int opType, int docCount);
450d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
460d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski    /**
470d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski     * There can be only one progress dialog active at the time. Each call to this
480d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski     * method will discard any previously created progress dialogs.
490d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski     */
500d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski    void showProgressDialog(String jobId, FileOperation operation);
510d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
52eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay    void showNoApplicationFound();
53c1a32aea59f47b06f3157a7f87ef5b9bf45f7627Ben Lin    void showOperationUnsupported();
54179f0695a8d4a0d87d28303f8d6e5e9c7194d981Tomasz Mikolajewski    void showViewInArchivesUnsupported();
55d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin    void showDocumentsClipped(int size);
56c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
5723ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan    // Dialogs used in PickActivity
5823ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan    void confirmOverwrite(FragmentManager fm, DocumentInfo overwriteTarget);
5923ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan
60c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    // Should be private, but Java doesn't like me treating an interface like a mini-package.
61c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    public static final class RuntimeDialogController implements DialogController {
62c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
63c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        private final Activity mActivity;
64c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        private final MessageBuilder mMessages;
650d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        private final Features mFeatures;
660d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        private OperationProgressDialog mCurrentProgressDialog = null;
67c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
680d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        public RuntimeDialogController(Features features, Activity activity, MessageBuilder messages) {
690d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            mFeatures = features;
70c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            mActivity = activity;
715b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mMessages = messages;
72c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        }
73c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
74c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        @Override
75c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        public void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback) {
76c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            assert(!docs.isEmpty());
77c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
78c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            TextView message =
79c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                    (TextView) mActivity.getLayoutInflater().inflate(
80c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                            R.layout.dialog_delete_confirmation, null);
81c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            message.setText(mMessages.generateDeleteMessage(docs));
82c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
83c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            // For now, we implement this dialog NOT
84c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            // as a fragment (which can survive rotation and have its own state),
85c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            // but as a simple runtime dialog. So rotating a device with an
86c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            // active delete dialog...results in that dialog disappearing.
87c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            // We can do better, but don't have cycles for it now.
886f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin            final AlertDialog alertDialog = new AlertDialog.Builder(mActivity)
896f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                    .setView(message)
906f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                    .setPositiveButton(
916f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                            android.R.string.ok,
926f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                            new DialogInterface.OnClickListener() {
936f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                                @Override
946f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                                public void onClick(DialogInterface dialog, int id) {
956f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                                    callback.accept(ConfirmationCallback.CONFIRM);
966f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                                }
976f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                            })
986f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                    .setNegativeButton(android.R.string.cancel, null)
996f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                    .create();
1006f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin
1016f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin            alertDialog.setOnShowListener(
1026f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                    (DialogInterface) -> {
1036f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                        Button positive = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
1046f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                        positive.setFocusable(true);
1056f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                        positive.requestFocus();
1066f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin                    });
1076f6d578302425aca2eb5ec2bd278be9a77065172Ben Lin            alertDialog.show();
108c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        }
109c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
110c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        @Override
1110d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        public void showFileOperationStatus(@Status int status, @OpType int opType,
1120d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski                int docCount) {
113c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            if (status == FileOperations.Callback.STATUS_REJECTED) {
114c1a32aea59f47b06f3157a7f87ef5b9bf45f7627Ben Lin                showOperationUnsupported();
115f657025bfc915df39e06e8e21067af2fd045759dJon Mann                return;
116f657025bfc915df39e06e8e21067af2fd045759dJon Mann            }
117f657025bfc915df39e06e8e21067af2fd045759dJon Mann            if (status == FileOperations.Callback.STATUS_FAILED) {
11868689622255bc831ad0f832de4c8f82c319fdb29Ben Lin                Snackbars.showOperationFailed(mActivity);
119c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                return;
120c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            }
121c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
122c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            if (docCount == 0) {
123c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                // Nothing has been pasted, so there is no need to show a snackbar.
124c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                return;
125c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            }
126c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
1270d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            if (shouldShowProgressDialogForOperation(opType)) {
1280d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski                // The operation has a progress dialog created, so do not show a snackbar
1290d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski                // for operation start, as it would duplicate the UI.
1300d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski                return;
1310d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            }
1320d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
133c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            switch (opType) {
134c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                case FileOperationService.OPERATION_MOVE:
135c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                    Snackbars.showMove(mActivity, docCount);
136c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                    break;
137c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                case FileOperationService.OPERATION_COPY:
138c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                    Snackbars.showCopy(mActivity, docCount);
139c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                    break;
140bc7eb5fc39b5555eedc7d7b3591687343ecda18dTomasz Mikolajewski                case FileOperationService.OPERATION_COMPRESS:
141bc7eb5fc39b5555eedc7d7b3591687343ecda18dTomasz Mikolajewski                    Snackbars.showCompress(mActivity, docCount);
142bc7eb5fc39b5555eedc7d7b3591687343ecda18dTomasz Mikolajewski                    break;
143bc7eb5fc39b5555eedc7d7b3591687343ecda18dTomasz Mikolajewski                case FileOperationService.OPERATION_EXTRACT:
144bc7eb5fc39b5555eedc7d7b3591687343ecda18dTomasz Mikolajewski                    Snackbars.showExtract(mActivity, docCount);
145bc7eb5fc39b5555eedc7d7b3591687343ecda18dTomasz Mikolajewski                    break;
146c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                case FileOperationService.OPERATION_DELETE:
14768689622255bc831ad0f832de4c8f82c319fdb29Ben Lin                    Snackbars.showDelete(mActivity, docCount);
148c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                    break;
149c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                default:
150c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay                    throw new UnsupportedOperationException("Unsupported Operation: " + opType);
151c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            }
152eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay        }
153eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay
1540d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        private boolean shouldShowProgressDialogForOperation(@OpType int opType) {
1550d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            // TODO: Hook up progress dialog to the delete operation.
1560d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            if (opType == FileOperationService.OPERATION_DELETE) {
1570d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski                return false;
1580d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            }
1590d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
1600d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            return mFeatures.isJobProgressDialogEnabled();
1610d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        }
1620d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
1630d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        @Override
1640d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        public void showProgressDialog(String jobId, FileOperation operation) {
1650d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            assert(operation.getOpType() != FileOperationService.OPERATION_UNKNOWN);
1660d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
1670d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            if (!shouldShowProgressDialogForOperation(operation.getOpType())) {
1680d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski                return;
1690d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            }
1700d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
1710d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            if (mCurrentProgressDialog != null) {
1720d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski                mCurrentProgressDialog.dismiss();
1730d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            }
1740d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
1750d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            mCurrentProgressDialog = OperationProgressDialog.create(mActivity, jobId, operation);
1760d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski            mCurrentProgressDialog.show();
1770d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        }
1780d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski
179eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay        @Override
180eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay        public void showNoApplicationFound() {
181eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay            Snackbars.makeSnackbar(
182eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay                    mActivity, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
183eed2f4ec78227b5e173ff667a5517637a91b9a30Steve McKay        }
184d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin
185d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin        @Override
186c1a32aea59f47b06f3157a7f87ef5b9bf45f7627Ben Lin        public void showOperationUnsupported() {
187c1a32aea59f47b06f3157a7f87ef5b9bf45f7627Ben Lin            Snackbars.showOperationRejected(mActivity);
188c1a32aea59f47b06f3157a7f87ef5b9bf45f7627Ben Lin        }
189c1a32aea59f47b06f3157a7f87ef5b9bf45f7627Ben Lin
190c1a32aea59f47b06f3157a7f87ef5b9bf45f7627Ben Lin        @Override
191179f0695a8d4a0d87d28303f8d6e5e9c7194d981Tomasz Mikolajewski        public void showViewInArchivesUnsupported() {
192179f0695a8d4a0d87d28303f8d6e5e9c7194d981Tomasz Mikolajewski            Snackbars.makeSnackbar(mActivity, R.string.toast_view_in_archives_unsupported,
193179f0695a8d4a0d87d28303f8d6e5e9c7194d981Tomasz Mikolajewski                    Snackbar.LENGTH_SHORT).show();
194179f0695a8d4a0d87d28303f8d6e5e9c7194d981Tomasz Mikolajewski        }
195179f0695a8d4a0d87d28303f8d6e5e9c7194d981Tomasz Mikolajewski
196179f0695a8d4a0d87d28303f8d6e5e9c7194d981Tomasz Mikolajewski        @Override
197d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin        public void showDocumentsClipped(int size) {
198d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin            Snackbars.showDocumentsClipped(mActivity, size);
199d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin        }
20023ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan
20123ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan        @Override
20223ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan        public void confirmOverwrite(FragmentManager fm, DocumentInfo overwriteTarget) {
20323ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan            OverwriteConfirmFragment.show(fm, overwriteTarget);
20423ac60cd2b1beb59b6cd760b91c7e1b1f7e8fe49Garfield Tan        }
205c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    }
206c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
2070d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski    static DialogController create(Features features, Activity activity, MessageBuilder messages) {
2080d83d32b66332034b17b327352693b125e9b3a05Tomasz Mikolajewski        return new RuntimeDialogController(features, activity, messages);
209c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    }
210c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay}
211