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 
17 package com.android.documentsui.dirlist;
18 
19 import static com.android.documentsui.base.SharedMinimal.TAG;
20 
21 import android.app.Dialog;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.os.AsyncTask;
25 import android.os.Bundle;
26 import android.util.Log;
27 import android.view.KeyEvent;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.inputmethod.EditorInfo;
31 import android.widget.Button;
32 import android.widget.EditText;
33 import android.widget.TextView;
34 import android.widget.TextView.OnEditorActionListener;
35 
36 import androidx.annotation.Nullable;
37 import androidx.appcompat.app.AlertDialog;
38 import androidx.fragment.app.DialogFragment;
39 import androidx.fragment.app.FragmentManager;
40 
41 import com.android.documentsui.BaseActivity;
42 import com.android.documentsui.Metrics;
43 import com.android.documentsui.R;
44 import com.android.documentsui.base.DocumentInfo;
45 import com.android.documentsui.base.Shared;
46 import com.android.documentsui.ui.Snackbars;
47 
48 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
49 import com.google.android.material.snackbar.Snackbar;
50 import com.google.android.material.textfield.TextInputLayout;
51 
52 /**
53  * Dialog to rename file or directory.
54  */
55 public class RenameDocumentFragment extends DialogFragment {
56     private static final String TAG_RENAME_DOCUMENT = "rename_document";
57     private DocumentInfo mDocument;
58     private EditText mEditText;
59     private TextInputLayout mRenameInputWrapper;
60     private @Nullable DialogInterface mDialog;
61 
show(FragmentManager fm, DocumentInfo document)62     public static void show(FragmentManager fm, DocumentInfo document) {
63         final RenameDocumentFragment dialog = new RenameDocumentFragment();
64         dialog.mDocument = document;
65         dialog.show(fm, TAG_RENAME_DOCUMENT);
66     }
67 
68     /**
69      * Creates the dialog UI.
70      * @param savedInstanceState
71      * @return
72      */
73     @Override
onCreateDialog(Bundle savedInstanceState)74     public Dialog onCreateDialog(Bundle savedInstanceState) {
75         Context context = getActivity();
76         MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
77         LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
78         View view = dialogInflater.inflate(R.layout.dialog_file_name, null, false);
79 
80         mEditText = (EditText) view.findViewById(android.R.id.text1);
81         mRenameInputWrapper = (TextInputLayout) view.findViewById(R.id.input_wrapper);
82         mRenameInputWrapper.setHint(getString(R.string.input_hint_rename));
83         builder.setTitle(R.string.menu_rename);
84         builder.setView(view);
85         builder.setPositiveButton(android.R.string.ok, null);
86         builder.setNegativeButton(android.R.string.cancel, null);
87 
88         final AlertDialog dialog = builder.create();
89 
90         dialog.setOnShowListener(this::onShowDialog);
91 
92         // Workaround for the problem - virtual keyboard doesn't show on the phone.
93         Shared.ensureKeyboardPresent(context, dialog);
94 
95         mEditText.setOnEditorActionListener(
96                 new OnEditorActionListener() {
97                     @Override
98                     public boolean onEditorAction(
99                             TextView view, int actionId, @Nullable KeyEvent event) {
100                         if ((actionId == EditorInfo.IME_ACTION_DONE) || (event != null
101                                 && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
102                                 && event.hasNoModifiers())) {
103                             renameDocuments(mEditText.getText().toString());
104                         }
105                         return false;
106                     }
107                 });
108         mEditText.requestFocus();
109         return dialog;
110     }
111 
onShowDialog(DialogInterface dialog)112     private void onShowDialog(DialogInterface dialog){
113         mDialog = dialog;
114         Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
115         button.setOnClickListener(this::onClickDialog);
116     }
117 
onClickDialog(View view)118     private void onClickDialog(View view) {
119         renameDocuments(mEditText.getText().toString());
120     }
121 
122     /**
123      * Sets/Restores the data.
124      * @param savedInstanceState
125      * @return
126      */
127     @Override
onActivityCreated(Bundle savedInstanceState)128     public void onActivityCreated(Bundle savedInstanceState) {
129         super.onActivityCreated(savedInstanceState);
130 
131         if(savedInstanceState == null) {
132             // Fragment created for the first time, we set the text.
133             // mDocument value was set in show
134             mEditText.setText(mDocument.displayName);
135         }
136         else {
137             // Fragment restored, text was restored automatically.
138             // mDocument value needs to be restored.
139             mDocument = savedInstanceState.getParcelable(Shared.EXTRA_DOC);
140         }
141         // Do selection in both cases, because we cleared it.
142         selectFileName(mEditText);
143     }
144 
145     @Override
onSaveInstanceState(Bundle outState)146     public void onSaveInstanceState(Bundle outState) {
147         // Clear selection before storing state and restore it manually,
148         // because otherwise after rotation selection is displayed with cut/copy menu visible :/
149         clearFileNameSelection(mEditText);
150 
151         super.onSaveInstanceState(outState);
152 
153         outState.putParcelable(Shared.EXTRA_DOC, mDocument);
154     }
155 
156     /**
157      * Validates if string is a proper document name.
158      * Checks if string is not empty. More rules might be added later.
159      * @param docName string representing document name
160      * @returns true if string is a valid name.
161      **/
isValidDocumentName(String docName)162     private boolean isValidDocumentName(String docName) {
163         return !docName.isEmpty();
164     }
165 
166     /**
167      * Fills text field with the file name and selects the name without extension.
168      *
169      * @param editText text field to be filled
170      */
selectFileName(EditText editText)171     private void selectFileName(EditText editText) {
172         String text = editText.getText().toString();
173         int separatorIndex = text.indexOf(".");
174         editText.setSelection(0,
175                 (separatorIndex == -1 || mDocument.isDirectory()) ? text.length() : separatorIndex);
176     }
177 
178     /**
179      * Clears selection in text field.
180      *
181      * @param editText text field to be cleared.
182      */
clearFileNameSelection(EditText editText)183     private void clearFileNameSelection(EditText editText) {
184         editText.setSelection(0, 0);
185     }
186 
renameDocuments(String newDisplayName)187     private void renameDocuments(String newDisplayName) {
188         BaseActivity activity = (BaseActivity) getActivity();
189 
190         if (newDisplayName.equals(mDocument.displayName)) {
191             mDialog.dismiss();
192         } else if (!isValidDocumentName(newDisplayName)) {
193             Log.w(TAG, "Failed to rename file - invalid name:" + newDisplayName);
194             Snackbars.makeSnackbar(getActivity(), R.string.rename_error,
195                     Snackbar.LENGTH_SHORT).show();
196         } else if (activity.getInjector().getModel().hasFileWithName(newDisplayName)){
197             mRenameInputWrapper.setError(getContext().getString(R.string.name_conflict));
198             selectFileName(mEditText);
199             Metrics.logRenameFileError();
200         } else {
201             new RenameDocumentsTask(activity, newDisplayName).execute(mDocument);
202         }
203 
204     }
205 
206     private class RenameDocumentsTask extends AsyncTask<DocumentInfo, Void, DocumentInfo> {
207         private final BaseActivity mActivity;
208         private final String mNewDisplayName;
209 
RenameDocumentsTask(BaseActivity activity, String newDisplayName)210         public RenameDocumentsTask(BaseActivity activity, String newDisplayName) {
211             mActivity = activity;
212             mNewDisplayName = newDisplayName;
213         }
214 
215         @Override
onPreExecute()216         protected void onPreExecute() {
217             mActivity.setPending(true);
218         }
219 
220         @Override
doInBackground(DocumentInfo... document)221         protected DocumentInfo doInBackground(DocumentInfo... document) {
222             assert(document.length == 1);
223 
224             return mActivity.getInjector().actions.renameDocument(mNewDisplayName, document[0]);
225         }
226 
227         @Override
onPostExecute(DocumentInfo result)228         protected void onPostExecute(DocumentInfo result) {
229             if (result != null) {
230                 Metrics.logRenameFileOperation();
231             } else {
232                 Snackbars.showRenameFailed(mActivity);
233                 Metrics.logRenameFileError();
234             }
235             if (mDialog != null) {
236                 mDialog.dismiss();
237             }
238             mActivity.setPending(false);
239         }
240     }
241 }
242