OSDN Git Service

Remove console selection (Issue #17) - Part II (Settings & Code)
[android-x86/packages-apps-CMFileManager.git] / src / com / cyanogenmod / filemanager / activities / EditorActivity.java
1 /*
2  * Copyright (C) 2012 The CyanogenMod 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.cyanogenmod.filemanager.activities;
18
19 import android.app.ActionBar;
20 import android.app.Activity;
21 import android.app.AlertDialog;
22 import android.content.DialogInterface;
23 import android.content.DialogInterface.OnClickListener;
24 import android.content.Intent;
25 import android.content.pm.ActivityInfo;
26 import android.os.AsyncTask;
27 import android.os.Bundle;
28 import android.text.Editable;
29 import android.text.TextUtils;
30 import android.text.TextWatcher;
31 import android.util.Log;
32 import android.view.KeyEvent;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.widget.EditText;
36 import android.widget.ProgressBar;
37 import android.widget.TextView;
38 import android.widget.TextView.BufferType;
39 import android.widget.Toast;
40
41 import com.cyanogenmod.filemanager.R;
42 import com.cyanogenmod.filemanager.commands.AsyncResultListener;
43 import com.cyanogenmod.filemanager.commands.WriteExecutable;
44 import com.cyanogenmod.filemanager.console.ConsoleBuilder;
45 import com.cyanogenmod.filemanager.model.FileSystemObject;
46 import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
47 import com.cyanogenmod.filemanager.ui.widgets.ButtonItem;
48 import com.cyanogenmod.filemanager.util.CommandHelper;
49 import com.cyanogenmod.filemanager.util.DialogHelper;
50 import com.cyanogenmod.filemanager.util.ExceptionUtil;
51
52 import java.io.ByteArrayInputStream;
53 import java.io.File;
54 import java.io.OutputStream;
55
56 /**
57  * An internal activity for view and edit files.
58  */
59 public class EditorActivity extends Activity implements TextWatcher {
60
61     private static final String TAG = "EditorActivity"; //$NON-NLS-1$
62
63     private static boolean DEBUG = false;
64
65     private static final char[] VALID_NON_PRINTABLE_CHARS = {' ', '\t', '\r', '\n'};
66
67     /**
68      * Internal interface to notify progress update
69      */
70     private interface OnProgressListener {
71         void onProgress(int progress);
72     }
73
74     /**
75      * An internal listener for read a file
76      */
77     @SuppressWarnings("hiding")
78     private class AsyncReader implements AsyncResultListener {
79
80         final Object mSync = new Object();
81         StringBuilder mBuffer = new StringBuilder();
82         Exception mCause;
83         long mSize;
84         FileSystemObject mFso;
85         OnProgressListener mListener;
86
87         /**
88          * Constructor of <code>AsyncReader</code>. For enclosing access.
89          */
90         public AsyncReader() {
91             super();
92         }
93
94         /**
95          * {@inheritDoc}
96          */
97         @Override
98         public void onAsyncStart() {
99             this.mBuffer = new StringBuilder();
100             this.mSize = 0;
101         }
102
103         /**
104          * {@inheritDoc}
105          */
106         @Override
107         public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
108
109         /**
110          * {@inheritDoc}
111          */
112         @Override
113         public void onAsyncExitCode(int exitCode) {
114             synchronized (this.mSync) {
115                 this.mSync.notify();
116             }
117         }
118
119         /**
120          * {@inheritDoc}
121          */
122         @Override
123         public void onPartialResult(Object result) {
124             try {
125                 byte[] partial = (byte[])result;
126
127                 // Check if the file is a binary file. In this case the editor
128                 // is read-only
129                 if (!EditorActivity.this.mReadOnly && partial != null) {
130                     for (int i = 0; i < partial.length-1; i++) {
131                         if (!isPrintableCharacter((char)partial[i])) {
132                             EditorActivity.this.mReadOnly = true;
133                             break;
134                         }
135                     }
136                 }
137
138                 this.mBuffer.append(new String(partial));
139                 this.mSize += this.mBuffer.length();
140                 if (this.mListener != null && this.mFso != null) {
141                     int progress = 0;
142                     if (this.mFso.getSize() != 0) {
143                         progress = (int)((this.mSize*100) / this.mFso.getSize());
144                     }
145                     this.mListener.onProgress(progress);
146                 }
147             } catch (Exception e) {
148                 this.mCause = e;
149             }
150         }
151
152         /**
153          * {@inheritDoc}
154          */
155         @Override
156         public void onException(Exception cause) {
157             this.mCause = cause;
158         }
159     }
160
161     /**
162      * An internal listener for write a file
163      */
164     private class AsyncWriter implements AsyncResultListener {
165
166         Exception mCause;
167
168         /**
169          * Constructor of <code>AsyncWriter</code>. For enclosing access.
170          */
171         public AsyncWriter() {
172             super();
173         }
174
175         /**
176          * {@inheritDoc}
177          */
178         @Override
179         public void onAsyncStart() {/**NON BLOCK**/}
180
181         /**
182          * {@inheritDoc}
183          */
184         @Override
185         public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
186
187         /**
188          * {@inheritDoc}
189          */
190         @Override
191         public void onAsyncExitCode(int exitCode) {/**NON BLOCK**/}
192
193         /**
194          * {@inheritDoc}
195          */
196         @Override
197         public void onPartialResult(Object result) {/**NON BLOCK**/}
198
199         /**
200          * {@inheritDoc}
201          */
202         @Override
203         public void onException(Exception cause) {
204             this.mCause = cause;
205         }
206     }
207
208     private FileSystemObject mFso;
209
210     private int mBufferSize;
211     private int mMaxFileSize;
212
213     /**
214      * @hide
215      */
216     boolean mDirty;
217     /**
218      * @hide
219      */
220     boolean mReadOnly;
221
222     /**
223      * @hide
224      */
225     TextView mTitle;
226     /**
227      * @hide
228      */
229     EditText mEditor;
230     /**
231      * @hide
232      */
233     View mProgress;
234     /**
235      * @hide
236      */
237     ProgressBar mProgressBar;
238     /**
239      * @hide
240      */
241     ButtonItem mSave;
242
243     /**
244      * Intent extra parameter for the path of the file to open.
245      */
246     public static final String EXTRA_OPEN_FILE = "extra_open_file";  //$NON-NLS-1$
247
248     /**
249      * {@inheritDoc}
250      */
251     @Override
252     protected void onCreate(Bundle state) {
253         if (DEBUG) {
254             Log.d(TAG, "EditorActivity.onCreate"); //$NON-NLS-1$
255         }
256
257         //Request features
258         setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
259
260         //Set the main layout of the activity
261         setContentView(R.layout.editor);
262
263         // Get the limit vars
264         this.mBufferSize =
265                 getApplicationContext().getResources().getInteger(R.integer.buffer_size);
266         this.mMaxFileSize =
267                 getApplicationContext().getResources().getInteger(R.integer.editor_max_file_size);
268
269         //Initialize
270         initTitleActionBar();
271         initLayout();
272         initializeConsole();
273         readFile();
274
275         //Save state
276         super.onCreate(state);
277     }
278
279     /**
280      * Method that initializes the titlebar of the activity.
281      */
282     private void initTitleActionBar() {
283         //Configure the action bar options
284         getActionBar().setBackgroundDrawable(
285                 getResources().getDrawable(R.drawable.bg_holo_titlebar));
286         getActionBar().setDisplayOptions(
287                 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME);
288         getActionBar().setDisplayHomeAsUpEnabled(true);
289         View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false);
290         this.mTitle = (TextView)customTitle.findViewById(R.id.customtitle_title);
291         this.mTitle.setText(R.string.editor);
292         this.mTitle.setContentDescription(getString(R.string.editor));
293         this.mSave = (ButtonItem)customTitle.findViewById(R.id.ab_button1);
294         this.mSave.setImageResource(R.drawable.ic_holo_light_save);
295         this.mSave.setContentDescription(getString(R.string.actionbar_button_save_cd));
296         this.mSave.setVisibility(View.INVISIBLE);
297
298         getActionBar().setCustomView(customTitle);
299     }
300
301     /**
302      * Method that initializes the layout and components of the activity.
303      */
304     private void initLayout() {
305         this.mEditor = (EditText)findViewById(R.id.editor);
306         this.mEditor.setText(null);
307         this.mEditor.addTextChangedListener(this);
308         this.mEditor.setEnabled(false);
309
310         this.mProgress = findViewById(R.id.editor_progress);
311         this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar);
312     }
313
314     /**
315      * {@inheritDoc}
316      */
317     @Override
318     public boolean onKeyUp(int keyCode, KeyEvent event) {
319         if (keyCode == KeyEvent.KEYCODE_BACK) {
320             checkDirtyState();
321             return false;
322         }
323         return super.onKeyUp(keyCode, event);
324     }
325
326     /**
327      * {@inheritDoc}
328      */
329     @Override
330     public boolean onOptionsItemSelected(MenuItem item) {
331        switch (item.getItemId()) {
332           case android.R.id.home:
333               if ((getActionBar().getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP)
334                       == ActionBar.DISPLAY_HOME_AS_UP) {
335                   checkDirtyState();
336               }
337               return true;
338           default:
339              return super.onOptionsItemSelected(item);
340        }
341     }
342
343     /**
344      * Method invoked when an action item is clicked.
345      *
346      * @param view The button pushed
347      */
348     public void onActionBarItemClick(View view) {
349         switch (view.getId()) {
350             case R.id.ab_button1:
351                 // Save the file
352                 writeFile();
353                 break;
354
355             default:
356                 break;
357         }
358     }
359
360     /**
361      * Method that initializes a console
362      */
363     private boolean initializeConsole() {
364         try {
365             // Is there a console allocate
366             if (!ConsoleBuilder.isAlloc()) {
367                 // Create a console
368                 ConsoleBuilder.getConsole(this);
369             }
370             // There is a console allocated. Use it.
371             return true;
372         } catch (Throwable _throw) {
373             // Capture the exception
374             ExceptionUtil.translateException(this, _throw, false, true);
375         }
376         return false;
377     }
378
379     /**
380      * Method that reads the requested file
381      */
382     private void readFile() {
383         // For now editor is not dirty and editable.
384         setDirty(false);
385
386         // Check for a valid action
387         String action = getIntent().getAction();
388         if (action == null ||
389                 (action.compareTo(Intent.ACTION_VIEW) != 0) &&
390                 (action.compareTo(Intent.ACTION_EDIT) != 0)) {
391             DialogHelper.showToast(
392                     this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
393             return;
394         }
395         this.mReadOnly = (action.compareTo(Intent.ACTION_VIEW) == 0);
396
397         // Read the intent and check that is has a valid request
398         String path = getIntent().getData().getPath();
399         if (path == null || path.length() == 0) {
400             DialogHelper.showToast(
401                     this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
402             return;
403         }
404
405         // Set the title of the dialog
406         File f = new File(path);
407         this.mTitle.setText(f.getName());
408
409         // Check that we have access to the file (the real file, not the symlink)
410         try {
411             this.mFso = CommandHelper.getFileInfo(this, path, true, null);
412             if (this.mFso == null) {
413                 DialogHelper.showToast(
414                         this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT);
415                 return;
416             }
417         } catch (Exception e) {
418             Log.e(TAG, "Failed to get file reference", e); //$NON-NLS-1$
419             DialogHelper.showToast(
420                     this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT);
421             return;
422         }
423
424         // Check that we can handle the length of the file (by device)
425         if (this.mMaxFileSize < this.mFso.getSize()) {
426             DialogHelper.showToast(
427                     this, R.string.editor_file_exceed_size_msg, Toast.LENGTH_SHORT);
428             return;
429         }
430
431         // Do the load of the
432         AsyncTask<FileSystemObject, Integer, Boolean> mOpenTask =
433                             new AsyncTask<FileSystemObject, Integer, Boolean>() {
434
435             private Exception mCause;
436             private AsyncReader mReader;
437
438             @Override
439             protected void onPreExecute() {
440                 // Show the progress
441                 doProgress(true, 0);
442             }
443
444             @Override
445             protected Boolean doInBackground(FileSystemObject... params) {
446                 // Only one argument (the file to open)
447                 FileSystemObject fso = params[0];
448                 this.mCause = null;
449
450                 // Read the file in an async listener
451                 try {
452                     // Configure the reader
453                     this.mReader = new AsyncReader();
454                     this.mReader.mFso = fso;
455                     this.mReader.mListener = new OnProgressListener() {
456                         @Override
457                         @SuppressWarnings("synthetic-access")
458                         public void onProgress(int progress) {
459                             publishProgress(Integer.valueOf(progress));
460                         }
461                     };
462
463                     // Execute the command (read the file)
464                     CommandHelper.read(
465                             EditorActivity.this, fso.getFullPath(), this.mReader, null);
466
467                     // Wait for
468                     synchronized (this.mReader.mSync) {
469                         this.mReader.mSync.wait();
470                     }
471
472                     // 100%
473                     doProgress(true, 100);
474
475                     // Check if the read was successfully
476                     if (this.mReader.mCause != null) {
477                         this.mCause = this.mReader.mCause;
478                         return Boolean.FALSE;
479                     }
480
481                 } catch (Exception e) {
482                     this.mCause = e;
483                     return Boolean.FALSE;
484                 }
485
486                 return Boolean.TRUE;
487             }
488
489             @Override
490             protected void onProgressUpdate(Integer... values) {
491                 // Do progress
492                 doProgress(true, values[0].intValue());
493             }
494
495             @Override
496             protected void onPostExecute(Boolean result) {
497                 // Hide the progress
498                 doProgress(false, 0);
499
500                 // Is error?
501                 if (!result.booleanValue()) {
502                     if (this.mCause != null) {
503                         ExceptionUtil.translateException(EditorActivity.this, this.mCause);
504                     }
505                 } else {
506                     // Now we have the buffer, set the text of the editor
507                     EditorActivity.this.mEditor.setText(
508                             this.mReader.mBuffer, BufferType.EDITABLE);
509                     this.mReader.mBuffer = null; //Cleanup
510                     setDirty(false);
511                     EditorActivity.this.mEditor.setEnabled(!EditorActivity.this.mReadOnly);
512
513                     // Notify read-only mode
514                     if (EditorActivity.this.mReadOnly) {
515                         DialogHelper.showToast(
516                                 EditorActivity.this,
517                                 R.string.editor_read_only_mode,
518                                 Toast.LENGTH_SHORT);
519                     }
520                 }
521             }
522
523             @Override
524             protected void onCancelled() {
525                 // Hide the progress
526                 doProgress(false, 0);
527             }
528
529             /**
530              * Method that update the progress status
531              *
532              * @param visible If the progress bar need to be hidden
533              * @param progress The progress
534              */
535             private void doProgress(boolean visible, int progress) {
536                 // Show the progress bar
537                 EditorActivity.this.mProgressBar.setProgress(progress);
538                 EditorActivity.this.mProgress.setVisibility(
539                             visible ? View.VISIBLE : View.GONE);
540             }
541         };
542         mOpenTask.execute(this.mFso);
543     }
544
545     /**
546      * Method that reads the requested file.
547      */
548     private void writeFile() {
549         try {
550             // Configure the writer
551             AsyncWriter writer = new AsyncWriter();
552
553             // Create the writable command
554             WriteExecutable cmd =
555                     CommandHelper.write(this, this.mFso.getFullPath(), writer, null);
556
557             // Obtain access to the buffer (IMP! don't close the buffer here, it's manage
558             // by the command)
559             OutputStream os = cmd.createOutputStream();
560             try {
561                 // Retrieve the text from the editor
562                 String text = this.mEditor.getText().toString();
563                 ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes());
564                 text = null;
565                 try {
566                     // Buffered write
567                     byte[] data = new byte[this.mBufferSize];
568                     int read = 0;
569                     while ((read = bais.read(data, 0, this.mBufferSize)) != -1) {
570                         os.write(data, 0, read);
571                     }
572                 } finally {
573                     try {
574                         bais.close();
575                     } catch (Exception e) {/**NON BLOCK**/}
576                 }
577
578             } finally {
579                 // Ok. Data is written or ensure buffer close
580                 cmd.end();
581             }
582
583             // Sleep a bit
584             Thread.sleep(150L);
585
586             // Is error?
587             if (writer.mCause != null) {
588                 // Something was wrong. The file probably is corrupted
589                 DialogHelper.showToast(
590                         this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
591             } else {
592                 // Success. The file was saved
593                 DialogHelper.showToast(
594                         this, R.string.editor_successfully_saved, Toast.LENGTH_SHORT);
595                 setDirty(false);
596
597                 // Send a message that allow other activities to update his data
598                 Intent intent = new Intent(FileManagerSettings.INTENT_FILE_CHANGED);
599                 intent.putExtra(
600                         FileManagerSettings.EXTRA_FILE_CHANGED_KEY, this.mFso.getFullPath());
601                 sendBroadcast(intent);
602             }
603
604         } catch (Exception e) {
605             // Something was wrong, but the file was NOT written
606             DialogHelper.showToast(
607                     this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
608             return;
609         }
610     }
611
612     /**
613      * {@inheritDoc}
614      */
615     @Override
616     public void beforeTextChanged(
617             CharSequence s, int start, int count, int after) {/**NON BLOCK**/}
618
619     /**
620      * {@inheritDoc}
621      */
622     @Override
623     public void onTextChanged(CharSequence s, int start, int before, int count) {/**NON BLOCK**/}
624
625     /**
626      * {@inheritDoc}
627      */
628     @Override
629     public void afterTextChanged(Editable s) {
630         setDirty(true);
631     }
632
633     /**
634      * Method that sets if the editor is dirty (has changed)
635      *
636      * @param dirty If the editor is dirty
637      * @hide
638      */
639     void setDirty(boolean dirty) {
640         this.mDirty = dirty;
641         this.mSave.setVisibility(dirty ? View.VISIBLE : View.GONE);
642     }
643
644     /**
645      * Check the dirty state of the editor, and ask the user to save the changes
646      * prior to exit.
647      */
648     public void checkDirtyState() {
649         if (this.mDirty) {
650             AlertDialog dlg = DialogHelper.createYesNoDialog(
651                     this, R.string.editor_dirty_ask, new OnClickListener() {
652                         @Override
653                         public void onClick(DialogInterface dialog, int which) {
654                             if (which == DialogInterface.BUTTON_POSITIVE) {
655                                 dialog.dismiss();
656                                 setResult(Activity.RESULT_OK);
657                                 finish();
658                             }
659                         }
660                     });
661             dlg.show();
662             return;
663         }
664         setResult(Activity.RESULT_OK);
665         finish();
666     }
667
668     /**
669      * Method that check if a character is valid printable character
670      *
671      * @param c The character to check
672      * @return boolean If the character is printable
673      * @hide
674      */
675     static boolean isPrintableCharacter(char c) {
676         int cc = VALID_NON_PRINTABLE_CHARS.length;
677         for (int i = 0; i < cc; i++) {
678             if (c == VALID_NON_PRINTABLE_CHARS[i]) {
679                 return true;
680             }
681         }
682         return TextUtils.isGraphic(c);
683     }
684
685 }