1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.documentsui.ui;
17 
18 import android.app.Activity;
19 import android.content.DialogInterface;
20 import android.widget.Button;
21 import android.widget.TextView;
22 
23 import androidx.appcompat.app.AlertDialog;
24 import androidx.fragment.app.FragmentManager;
25 
26 import com.android.documentsui.R;
27 import com.android.documentsui.base.ConfirmationCallback;
28 import com.android.documentsui.base.DocumentInfo;
29 import com.android.documentsui.base.Features;
30 import com.android.documentsui.picker.ConfirmFragment;
31 import com.android.documentsui.services.FileOperation;
32 import com.android.documentsui.services.FileOperationService;
33 import com.android.documentsui.services.FileOperationService.OpType;
34 import com.android.documentsui.services.FileOperations;
35 import com.android.documentsui.services.FileOperations.Callback.Status;
36 
37 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
38 import com.google.android.material.snackbar.Snackbar;
39 
40 import java.util.List;
41 
42 public interface DialogController {
43 
44     // Dialogs used in FilesActivity
confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback)45     void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback);
showFileOperationStatus(int status, int opType, int docCount)46     void showFileOperationStatus(int status, int opType, int docCount);
47 
48     /**
49      * There can be only one progress dialog active at the time. Each call to this
50      * method will discard any previously created progress dialogs.
51      */
showProgressDialog(String jobId, FileOperation operation)52     void showProgressDialog(String jobId, FileOperation operation);
53 
showNoApplicationFound()54     void showNoApplicationFound();
showOperationUnsupported()55     void showOperationUnsupported();
showViewInArchivesUnsupported()56     void showViewInArchivesUnsupported();
showDocumentsClipped(int size)57     void showDocumentsClipped(int size);
58 
59     // Dialogs used in PickActivity
confirmAction(FragmentManager fm, DocumentInfo pickTarget, int type)60     void confirmAction(FragmentManager fm, DocumentInfo pickTarget, int type);
61 
62     // Should be private, but Java doesn't like me treating an interface like a mini-package.
63     public static final class RuntimeDialogController implements DialogController {
64 
65         private final Activity mActivity;
66         private final MessageBuilder mMessages;
67         private final Features mFeatures;
68         private OperationProgressDialog mCurrentProgressDialog = null;
69 
RuntimeDialogController(Features features, Activity activity, MessageBuilder messages)70         public RuntimeDialogController(Features features, Activity activity, MessageBuilder messages) {
71             mFeatures = features;
72             mActivity = activity;
73             mMessages = messages;
74         }
75 
76         @Override
confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback)77         public void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback) {
78             assert(!docs.isEmpty());
79 
80             TextView message =
81                     (TextView) mActivity.getLayoutInflater().inflate(
82                             R.layout.dialog_delete_confirmation, null);
83             message.setText(mMessages.generateDeleteMessage(docs));
84 
85             // For now, we implement this dialog NOT
86             // as a fragment (which can survive rotation and have its own state),
87             // but as a simple runtime dialog. So rotating a device with an
88             // active delete dialog...results in that dialog disappearing.
89             // We can do better, but don't have cycles for it now.
90             final AlertDialog alertDialog = new MaterialAlertDialogBuilder(mActivity)
91                     .setView(message)
92                     .setPositiveButton(
93                             android.R.string.ok,
94                             new DialogInterface.OnClickListener() {
95                                 @Override
96                                 public void onClick(DialogInterface dialog, int id) {
97                                     callback.accept(ConfirmationCallback.CONFIRM);
98                                 }
99                             })
100                     .setNegativeButton(android.R.string.cancel, null)
101                     .create();
102 
103             alertDialog.setOnShowListener(
104                     (DialogInterface) -> {
105                         Button positive = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
106                         positive.setFocusable(true);
107                         positive.requestFocus();
108                     });
109             alertDialog.show();
110         }
111 
112         @Override
showFileOperationStatus(@tatus int status, @OpType int opType, int docCount)113         public void showFileOperationStatus(@Status int status, @OpType int opType, int docCount) {
114             if (status == FileOperations.Callback.STATUS_REJECTED) {
115                 showOperationUnsupported();
116                 return;
117             }
118             if (status == FileOperations.Callback.STATUS_FAILED) {
119                 Snackbars.showOperationFailed(mActivity);
120                 return;
121             }
122 
123             if (docCount == 0) {
124                 // Nothing has been pasted, so there is no need to show a snackbar.
125                 return;
126             }
127 
128             if (shouldShowProgressDialogForOperation(opType)) {
129                 // The operation has a progress dialog created, so do not show a snackbar
130                 // for operation start, as it would duplicate the UI.
131                 return;
132             }
133 
134             switch (opType) {
135                 case FileOperationService.OPERATION_MOVE:
136                     Snackbars.showMove(mActivity, docCount);
137                     break;
138                 case FileOperationService.OPERATION_COPY:
139                     Snackbars.showCopy(mActivity, docCount);
140                     break;
141                 case FileOperationService.OPERATION_COMPRESS:
142                     Snackbars.showCompress(mActivity, docCount);
143                     break;
144                 case FileOperationService.OPERATION_EXTRACT:
145                     Snackbars.showExtract(mActivity, docCount);
146                     break;
147                 case FileOperationService.OPERATION_DELETE:
148                     Snackbars.showDelete(mActivity, docCount);
149                     break;
150                 default:
151                     throw new UnsupportedOperationException("Unsupported Operation: " + opType);
152             }
153         }
154 
shouldShowProgressDialogForOperation(@pType int opType)155         private boolean shouldShowProgressDialogForOperation(@OpType int opType) {
156             // TODO: Hook up progress dialog to the delete operation.
157             if (opType == FileOperationService.OPERATION_DELETE) {
158                 return false;
159             }
160 
161             return mFeatures.isJobProgressDialogEnabled();
162         }
163 
164         @Override
showProgressDialog(String jobId, FileOperation operation)165         public void showProgressDialog(String jobId, FileOperation operation) {
166             assert(operation.getOpType() != FileOperationService.OPERATION_UNKNOWN);
167 
168             if (!shouldShowProgressDialogForOperation(operation.getOpType())) {
169                 return;
170             }
171 
172             if (mCurrentProgressDialog != null) {
173                 mCurrentProgressDialog.dismiss();
174             }
175 
176             mCurrentProgressDialog = OperationProgressDialog.create(mActivity, jobId, operation);
177             mCurrentProgressDialog.show();
178         }
179 
180         @Override
showNoApplicationFound()181         public void showNoApplicationFound() {
182             Snackbars.makeSnackbar(
183                     mActivity, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
184         }
185 
186         @Override
showOperationUnsupported()187         public void showOperationUnsupported() {
188             Snackbars.showOperationRejected(mActivity);
189         }
190 
191         @Override
showViewInArchivesUnsupported()192         public void showViewInArchivesUnsupported() {
193             Snackbars.makeSnackbar(mActivity, R.string.toast_view_in_archives_unsupported,
194                     Snackbar.LENGTH_SHORT).show();
195         }
196 
197         @Override
showDocumentsClipped(int size)198         public void showDocumentsClipped(int size) {
199             Snackbars.showDocumentsClipped(mActivity, size);
200         }
201 
202         @Override
confirmAction(FragmentManager fm, DocumentInfo pickTarget, int type)203         public void confirmAction(FragmentManager fm, DocumentInfo pickTarget, int type) {
204             ConfirmFragment.show(fm, pickTarget, type);
205         }
206     }
207 
create(Features features, Activity activity, MessageBuilder messages)208     static DialogController create(Features features, Activity activity, MessageBuilder messages) {
209         return new RuntimeDialogController(features, activity, messages);
210     }
211 }
212