OSDN Git Service

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