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.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;
52 import java.io.ByteArrayInputStream;
54 import java.io.OutputStream;
57 * An internal activity for view and edit files.
59 public class EditorActivity extends Activity implements TextWatcher {
61 private static final String TAG = "EditorActivity"; //$NON-NLS-1$
63 private static boolean DEBUG = false;
65 private static final char[] VALID_NON_PRINTABLE_CHARS = {' ', '\t', '\r', '\n'};
68 * Internal interface to notify progress update
70 private interface OnProgressListener {
71 void onProgress(int progress);
75 * An internal listener for read a file
77 @SuppressWarnings("hiding")
78 private class AsyncReader implements AsyncResultListener {
80 final Object mSync = new Object();
81 StringBuilder mBuffer = new StringBuilder();
84 FileSystemObject mFso;
85 OnProgressListener mListener;
88 * Constructor of <code>AsyncReader</code>. For enclosing access.
90 public AsyncReader() {
98 public void onAsyncStart() {
99 this.mBuffer = new StringBuilder();
107 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
113 public void onAsyncExitCode(int exitCode) {
114 synchronized (this.mSync) {
123 public void onPartialResult(Object result) {
125 byte[] partial = (byte[])result;
127 // Check if the file is a binary file. In this case the editor
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;
138 this.mBuffer.append(new String(partial));
139 this.mSize += this.mBuffer.length();
140 if (this.mListener != null && this.mFso != null) {
142 if (this.mFso.getSize() != 0) {
143 progress = (int)((this.mSize*100) / this.mFso.getSize());
145 this.mListener.onProgress(progress);
147 } catch (Exception e) {
156 public void onException(Exception cause) {
162 * An internal listener for write a file
164 private class AsyncWriter implements AsyncResultListener {
169 * Constructor of <code>AsyncWriter</code>. For enclosing access.
171 public AsyncWriter() {
179 public void onAsyncStart() {/**NON BLOCK**/}
185 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
191 public void onAsyncExitCode(int exitCode) {/**NON BLOCK**/}
197 public void onPartialResult(Object result) {/**NON BLOCK**/}
203 public void onException(Exception cause) {
208 private FileSystemObject mFso;
210 private int mBufferSize;
211 private int mMaxFileSize;
237 ProgressBar mProgressBar;
244 * Intent extra parameter for the path of the file to open.
246 public static final String EXTRA_OPEN_FILE = "extra_open_file"; //$NON-NLS-1$
252 protected void onCreate(Bundle state) {
254 Log.d(TAG, "EditorActivity.onCreate"); //$NON-NLS-1$
258 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
260 //Set the main layout of the activity
261 setContentView(R.layout.editor);
263 // Get the limit vars
265 getApplicationContext().getResources().getInteger(R.integer.buffer_size);
267 getApplicationContext().getResources().getInteger(R.integer.editor_max_file_size);
270 initTitleActionBar();
276 super.onCreate(state);
280 * Method that initializes the titlebar of the activity.
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);
298 getActionBar().setCustomView(customTitle);
302 * Method that initializes the layout and components of the activity.
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);
310 this.mProgress = findViewById(R.id.editor_progress);
311 this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar);
318 public boolean onKeyUp(int keyCode, KeyEvent event) {
319 if (keyCode == KeyEvent.KEYCODE_BACK) {
323 return super.onKeyUp(keyCode, event);
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) {
339 return super.onOptionsItemSelected(item);
344 * Method invoked when an action item is clicked.
346 * @param view The button pushed
348 public void onActionBarItemClick(View view) {
349 switch (view.getId()) {
350 case R.id.ab_button1:
361 * Method that initializes a console
363 private boolean initializeConsole() {
365 // Is there a console allocate
366 if (!ConsoleBuilder.isAlloc()) {
368 ConsoleBuilder.getConsole(this);
370 // There is a console allocated. Use it.
372 } catch (Throwable _throw) {
373 // Capture the exception
374 ExceptionUtil.translateException(this, _throw, false, true);
380 * Method that reads the requested file
382 private void readFile() {
383 // For now editor is not dirty and editable.
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);
395 this.mReadOnly = (action.compareTo(Intent.ACTION_VIEW) == 0);
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);
405 // Set the title of the dialog
406 File f = new File(path);
407 this.mTitle.setText(f.getName());
409 // Check that we have access to the file (the real file, not the symlink)
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);
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);
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);
431 // Do the load of the
432 AsyncTask<FileSystemObject, Integer, Boolean> mOpenTask =
433 new AsyncTask<FileSystemObject, Integer, Boolean>() {
435 private Exception mCause;
436 private AsyncReader mReader;
439 protected void onPreExecute() {
445 protected Boolean doInBackground(FileSystemObject... params) {
446 // Only one argument (the file to open)
447 FileSystemObject fso = params[0];
450 // Read the file in an async listener
452 // Configure the reader
453 this.mReader = new AsyncReader();
454 this.mReader.mFso = fso;
455 this.mReader.mListener = new OnProgressListener() {
457 @SuppressWarnings("synthetic-access")
458 public void onProgress(int progress) {
459 publishProgress(Integer.valueOf(progress));
463 // Execute the command (read the file)
465 EditorActivity.this, fso.getFullPath(), this.mReader, null);
468 synchronized (this.mReader.mSync) {
469 this.mReader.mSync.wait();
473 doProgress(true, 100);
475 // Check if the read was successfully
476 if (this.mReader.mCause != null) {
477 this.mCause = this.mReader.mCause;
478 return Boolean.FALSE;
481 } catch (Exception e) {
483 return Boolean.FALSE;
490 protected void onProgressUpdate(Integer... values) {
492 doProgress(true, values[0].intValue());
496 protected void onPostExecute(Boolean result) {
498 doProgress(false, 0);
501 if (!result.booleanValue()) {
502 if (this.mCause != null) {
503 ExceptionUtil.translateException(EditorActivity.this, this.mCause);
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
511 EditorActivity.this.mEditor.setEnabled(!EditorActivity.this.mReadOnly);
513 // Notify read-only mode
514 if (EditorActivity.this.mReadOnly) {
515 DialogHelper.showToast(
517 R.string.editor_read_only_mode,
524 protected void onCancelled() {
526 doProgress(false, 0);
530 * Method that update the progress status
532 * @param visible If the progress bar need to be hidden
533 * @param progress The progress
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);
542 mOpenTask.execute(this.mFso);
546 * Method that reads the requested file.
548 private void writeFile() {
550 // Configure the writer
551 AsyncWriter writer = new AsyncWriter();
553 // Create the writable command
554 WriteExecutable cmd =
555 CommandHelper.write(this, this.mFso.getFullPath(), writer, null);
557 // Obtain access to the buffer (IMP! don't close the buffer here, it's manage
559 OutputStream os = cmd.createOutputStream();
561 // Retrieve the text from the editor
562 String text = this.mEditor.getText().toString();
563 ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes());
567 byte[] data = new byte[this.mBufferSize];
569 while ((read = bais.read(data, 0, this.mBufferSize)) != -1) {
570 os.write(data, 0, read);
575 } catch (Exception e) {/**NON BLOCK**/}
579 // Ok. Data is written or ensure buffer close
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);
592 // Success. The file was saved
593 DialogHelper.showToast(
594 this, R.string.editor_successfully_saved, Toast.LENGTH_SHORT);
597 // Send a message that allow other activities to update his data
598 Intent intent = new Intent(FileManagerSettings.INTENT_FILE_CHANGED);
600 FileManagerSettings.EXTRA_FILE_CHANGED_KEY, this.mFso.getFullPath());
601 sendBroadcast(intent);
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);
616 public void beforeTextChanged(
617 CharSequence s, int start, int count, int after) {/**NON BLOCK**/}
623 public void onTextChanged(CharSequence s, int start, int before, int count) {/**NON BLOCK**/}
629 public void afterTextChanged(Editable s) {
634 * Method that sets if the editor is dirty (has changed)
636 * @param dirty If the editor is dirty
639 void setDirty(boolean dirty) {
641 this.mSave.setVisibility(dirty ? View.VISIBLE : View.GONE);
645 * Check the dirty state of the editor, and ask the user to save the changes
648 public void checkDirtyState() {
650 AlertDialog dlg = DialogHelper.createYesNoDialog(
651 this, R.string.editor_dirty_ask, new OnClickListener() {
653 public void onClick(DialogInterface dialog, int which) {
654 if (which == DialogInterface.BUTTON_POSITIVE) {
656 setResult(Activity.RESULT_OK);
664 setResult(Activity.RESULT_OK);
669 * Method that check if a character is valid printable character
671 * @param c The character to check
672 * @return boolean If the character is printable
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]) {
682 return TextUtils.isGraphic(c);