2 * Copyright (C) 2012 The CyanogenMod Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.cyanogenmod.filemanager.activities;
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;
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;
54 import java.io.ByteArrayInputStream;
56 import java.io.OutputStream;
59 * An internal activity for view and edit files.
61 public class EditorActivity extends Activity implements TextWatcher {
63 private static final String TAG = "EditorActivity"; //$NON-NLS-1$
65 private static boolean DEBUG = false;
67 private static final char[] VALID_NON_PRINTABLE_CHARS = {' ', '\t', '\r', '\n'};
70 * Internal interface to notify progress update
72 private interface OnProgressListener {
73 void onProgress(int progress);
77 * An internal listener for read a file
79 @SuppressWarnings("hiding")
80 private class AsyncReader implements AsyncResultListener {
82 final Object mSync = new Object();
83 StringBuilder mBuffer = new StringBuilder();
86 FileSystemObject mFso;
87 OnProgressListener mListener;
90 * Constructor of <code>AsyncReader</code>. For enclosing access.
92 public AsyncReader() {
100 public void onAsyncStart() {
101 this.mBuffer = new StringBuilder();
109 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
115 public void onAsyncExitCode(int exitCode) {
116 synchronized (this.mSync) {
125 public void onPartialResult(Object result) {
127 byte[] partial = (byte[])result;
129 // Check if the file is a binary file. In this case the editor
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;
140 this.mBuffer.append(new String(partial));
141 this.mSize += this.mBuffer.length();
142 if (this.mListener != null && this.mFso != null) {
144 if (this.mFso.getSize() != 0) {
145 progress = (int)((this.mSize*100) / this.mFso.getSize());
147 this.mListener.onProgress(progress);
149 } catch (Exception e) {
158 public void onException(Exception cause) {
164 * An internal listener for write a file
166 private class AsyncWriter implements AsyncResultListener {
171 * Constructor of <code>AsyncWriter</code>. For enclosing access.
173 public AsyncWriter() {
181 public void onAsyncStart() {/**NON BLOCK**/}
187 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
193 public void onAsyncExitCode(int exitCode) {/**NON BLOCK**/}
199 public void onPartialResult(Object result) {/**NON BLOCK**/}
205 public void onException(Exception cause) {
210 private FileSystemObject mFso;
212 private int mBufferSize;
213 private int mMaxFileSize;
239 ProgressBar mProgressBar;
246 * Intent extra parameter for the path of the file to open.
248 public static final String EXTRA_OPEN_FILE = "extra_open_file"; //$NON-NLS-1$
254 protected void onCreate(Bundle state) {
256 Log.d(TAG, "EditorActivity.onCreate"); //$NON-NLS-1$
260 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
262 //Set the main layout of the activity
263 setContentView(R.layout.editor);
265 // Get the limit vars
267 getApplicationContext().getResources().getInteger(R.integer.buffer_size);
269 getApplicationContext().getResources().getInteger(R.integer.editor_max_file_size);
272 initTitleActionBar();
278 super.onCreate(state);
282 * Method that initializes the titlebar of the activity.
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);
300 getActionBar().setCustomView(customTitle);
304 * Method that initializes the layout and components of the activity.
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);
312 this.mProgress = findViewById(R.id.editor_progress);
313 this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar);
320 public boolean onKeyUp(int keyCode, KeyEvent event) {
321 if (keyCode == KeyEvent.KEYCODE_BACK) {
325 return super.onKeyUp(keyCode, event);
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) {
341 return super.onOptionsItemSelected(item);
346 * Method invoked when an action item is clicked.
348 * @param view The button pushed
350 public void onActionBarItemClick(View view) {
351 switch (view.getId()) {
352 case R.id.ab_button1:
363 * Method that initializes a console
365 private boolean initializeConsole() {
367 // Is there a console allocate
368 if (!ConsoleBuilder.isAlloc()) {
370 ConsoleBuilder.getConsole(this);
372 // There is a console allocated. Use it.
374 } catch (Throwable _throw) {
375 // Capture the exception
376 ExceptionUtil.translateException(this, _throw, false, true);
382 * Method that reads the requested file
384 private void readFile() {
385 // For now editor is not dirty and editable.
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);
397 this.mReadOnly = (action.compareTo(Intent.ACTION_VIEW) == 0);
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);
407 // Set the title of the dialog
408 File f = new File(path);
409 this.mTitle.setText(f.getName());
411 // Check that we have access to the file (the real file, not the symlink)
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);
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);
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);
433 // Read the file in background
438 * Method that does the read of the file in background
442 // Do the load of the file
443 AsyncTask<FileSystemObject, Integer, Boolean> mReadTask =
444 new AsyncTask<FileSystemObject, Integer, Boolean>() {
446 private Exception mCause;
447 private AsyncReader mReader;
450 protected void onPreExecute() {
456 protected Boolean doInBackground(FileSystemObject... params) {
457 // Only one argument (the file to open)
458 FileSystemObject fso = params[0];
461 // Read the file in an async listener
464 // Configure the reader
465 this.mReader = new AsyncReader();
466 this.mReader.mFso = fso;
467 this.mReader.mListener = new OnProgressListener() {
469 @SuppressWarnings("synthetic-access")
470 public void onProgress(int progress) {
471 publishProgress(Integer.valueOf(progress));
475 // Execute the command (read the file)
477 EditorActivity.this, fso.getFullPath(), this.mReader, null);
480 synchronized (this.mReader.mSync) {
481 this.mReader.mSync.wait();
485 doProgress(true, 100);
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
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);
501 this.mCause = this.mReader.mCause;
502 return Boolean.FALSE;
507 } catch (Exception e) {
509 return Boolean.FALSE;
516 protected void onProgressUpdate(Integer... values) {
518 doProgress(true, values[0].intValue());
522 protected void onPostExecute(Boolean result) {
524 doProgress(false, 0);
527 if (!result.booleanValue()) {
528 if (this.mCause != null) {
529 ExceptionUtil.translateException(EditorActivity.this, this.mCause);
530 EditorActivity.this.mEditor.setEnabled(false);
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
538 EditorActivity.this.mEditor.setEnabled(!EditorActivity.this.mReadOnly);
540 // Notify read-only mode
541 if (EditorActivity.this.mReadOnly) {
542 DialogHelper.showToast(
544 R.string.editor_read_only_mode,
551 protected void onCancelled() {
553 doProgress(false, 0);
557 * Method that update the progress status
559 * @param visible If the progress bar need to be hidden
560 * @param progress The progress
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);
569 mReadTask.execute(this.mFso);
573 * Method that reads the requested file.
575 private void writeFile() {
577 // Configure the writer
578 AsyncWriter writer = new AsyncWriter();
580 // Create the writable command
581 WriteExecutable cmd =
582 CommandHelper.write(this, this.mFso.getFullPath(), writer, null);
584 // Obtain access to the buffer (IMP! don't close the buffer here, it's manage
586 OutputStream os = cmd.createOutputStream();
588 // Retrieve the text from the editor
589 String text = this.mEditor.getText().toString();
590 ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes());
594 byte[] data = new byte[this.mBufferSize];
596 while ((read = bais.read(data, 0, this.mBufferSize)) != -1) {
597 os.write(data, 0, read);
602 } catch (Exception e) {/**NON BLOCK**/}
606 // Ok. Data is written or ensure buffer close
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);
619 // Success. The file was saved
620 DialogHelper.showToast(
621 this, R.string.editor_successfully_saved, Toast.LENGTH_SHORT);
624 // Send a message that allow other activities to update his data
625 Intent intent = new Intent(FileManagerSettings.INTENT_FILE_CHANGED);
627 FileManagerSettings.EXTRA_FILE_CHANGED_KEY, this.mFso.getFullPath());
628 sendBroadcast(intent);
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);
640 * Method that asks the user for gain access and reexecute the read command
642 * @param cause The cause of the reexecution
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.
649 //Create a yes/no dialog and ask the user
650 runOnUiThread(new Runnable() {
653 AlertDialog alert = DialogHelper.createYesNoDialog(
655 R.string.confirm_operation,
656 cause.getQuestionResourceId(),
657 new DialogInterface.OnClickListener() {
659 public void onClick(DialogInterface dialog, int which) {
660 if (which == DialogInterface.BUTTON_POSITIVE) {
661 // Change to privileged console
663 changeToPrivilegedConsole(EditorActivity.this)) {
665 // Capture the exception
666 ExceptionUtil.translateException(
669 EditorActivity.this.mEditor.setEnabled(false);
673 //Read the file again
676 // Finish the application
677 EditorActivity.this.finish();
690 public void beforeTextChanged(
691 CharSequence s, int start, int count, int after) {/**NON BLOCK**/}
697 public void onTextChanged(CharSequence s, int start, int before, int count) {/**NON BLOCK**/}
703 public void afterTextChanged(Editable s) {
708 * Method that sets if the editor is dirty (has changed)
710 * @param dirty If the editor is dirty
713 void setDirty(boolean dirty) {
715 this.mSave.setVisibility(dirty ? View.VISIBLE : View.GONE);
719 * Check the dirty state of the editor, and ask the user to save the changes
722 public void checkDirtyState() {
724 AlertDialog dlg = DialogHelper.createYesNoDialog(
726 R.string.editor_dirty_ask_title,
727 R.string.editor_dirty_ask_msg,
728 new OnClickListener() {
730 public void onClick(DialogInterface dialog, int which) {
731 if (which == DialogInterface.BUTTON_POSITIVE) {
733 setResult(Activity.RESULT_OK);
741 setResult(Activity.RESULT_OK);
746 * Method that check if a character is valid printable character
748 * @param c The character to check
749 * @return boolean If the character is printable
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]) {
759 return TextUtils.isGraphic(c);