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.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.DialogInterface.OnClickListener;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.res.Configuration;
29 import android.graphics.Typeface;
30 import android.graphics.drawable.Drawable;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.preference.PreferenceActivity;
35 import android.text.Editable;
36 import android.text.InputType;
37 import android.text.SpannableStringBuilder;
38 import android.text.TextUtils;
39 import android.text.TextWatcher;
40 import android.util.Log;
41 import android.view.KeyEvent;
42 import android.view.MenuItem;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.AdapterView;
46 import android.widget.AdapterView.OnItemClickListener;
47 import android.widget.EditText;
48 import android.widget.ImageView;
49 import android.widget.ListPopupWindow;
50 import android.widget.ProgressBar;
51 import android.widget.TextView;
52 import android.widget.TextView.BufferType;
53 import android.widget.Toast;
55 import com.android.internal.util.HexDump;
56 import com.cyanogenmod.filemanager.R;
57 import com.cyanogenmod.filemanager.activities.preferences.EditorPreferenceFragment;
58 import com.cyanogenmod.filemanager.activities.preferences.EditorSHColorSchemePreferenceFragment;
59 import com.cyanogenmod.filemanager.activities.preferences.SettingsPreferences;
60 import com.cyanogenmod.filemanager.adapters.HighlightedSimpleMenuListAdapter;
61 import com.cyanogenmod.filemanager.adapters.SimpleMenuListAdapter;
62 import com.cyanogenmod.filemanager.ash.HighlightColors;
63 import com.cyanogenmod.filemanager.ash.ISyntaxHighlightResourcesResolver;
64 import com.cyanogenmod.filemanager.ash.SyntaxHighlightFactory;
65 import com.cyanogenmod.filemanager.ash.SyntaxHighlightProcessor;
66 import com.cyanogenmod.filemanager.commands.AsyncResultListener;
67 import com.cyanogenmod.filemanager.commands.WriteExecutable;
68 import com.cyanogenmod.filemanager.console.ConsoleBuilder;
69 import com.cyanogenmod.filemanager.model.FileSystemObject;
70 import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
71 import com.cyanogenmod.filemanager.preferences.Preferences;
72 import com.cyanogenmod.filemanager.ui.ThemeManager;
73 import com.cyanogenmod.filemanager.ui.ThemeManager.Theme;
74 import com.cyanogenmod.filemanager.ui.widgets.ButtonItem;
75 import com.cyanogenmod.filemanager.util.AndroidHelper;
76 import com.cyanogenmod.filemanager.util.CommandHelper;
77 import com.cyanogenmod.filemanager.util.DialogHelper;
78 import com.cyanogenmod.filemanager.util.ExceptionUtil;
79 import com.cyanogenmod.filemanager.util.ExceptionUtil.OnRelaunchCommandResult;
80 import com.cyanogenmod.filemanager.util.FileHelper;
81 import com.cyanogenmod.filemanager.util.ResourcesHelper;
83 import java.io.ByteArrayInputStream;
84 import java.io.ByteArrayOutputStream;
86 import java.io.OutputStream;
87 import java.util.Arrays;
88 import java.util.UUID;
91 * An internal activity for view and edit files.
93 public class EditorActivity extends Activity implements TextWatcher {
95 private static final String TAG = "EditorActivity"; //$NON-NLS-1$
97 private static boolean DEBUG = false;
99 private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() {
101 public void onReceive(Context context, Intent intent) {
102 if (intent != null) {
103 if (intent.getAction().compareTo(FileManagerSettings.INTENT_THEME_CHANGED) == 0) {
107 if (intent.getAction().compareTo(FileManagerSettings.INTENT_SETTING_CHANGED) == 0) {
108 // The settings has changed
109 String key = intent.getStringExtra(FileManagerSettings.EXTRA_SETTING_CHANGED_KEY);
111 final EditorActivity activity = EditorActivity.this;
114 if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId()) == 0) {
115 // Ignore in binary files
116 if (activity.mBinary) return;
118 // Do we have a different setting?
119 boolean noSuggestionsSetting =
120 Preferences.getSharedPreferences().getBoolean(
121 FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(),
122 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.
123 getDefaultValue()).booleanValue());
124 if (noSuggestionsSetting != activity.mNoSuggestions) {
125 activity.mHandler.post(new Runnable() {
128 toggleNoSuggestions();
134 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId()) == 0) {
135 // Ignore in binary files
136 if (activity.mBinary) return;
138 // Do we have a different setting?
139 boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean(
140 FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(),
141 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.
142 getDefaultValue()).booleanValue());
143 if (wordWrapSetting != activity.mWordWrap) {
144 activity.mHandler.post(new Runnable() {
153 // Default theme color scheme
155 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId()) == 0) {
156 // Ignore in binary files
157 if (activity.mBinary) return;
159 // Do we have a different setting?
160 boolean syntaxHighlightSetting =
161 Preferences.getSharedPreferences().getBoolean(
162 FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(),
163 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.
164 getDefaultValue()).booleanValue());
165 if (syntaxHighlightSetting != activity.mSyntaxHighlight) {
166 activity.mHandler.post(new Runnable() {
169 toggleSyntaxHighlight();
174 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SH_USE_THEME_DEFAULT.getId()) == 0 ||
175 key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId()) == 0 ) {
176 // Ignore in binary files
177 if (activity.mBinary) return;
179 // Reload the syntax highlight
180 activity.mHandler.post(new Runnable() {
183 reloadSyntaxHighlight();
195 * Internal interface to notify progress update
197 private interface OnProgressListener {
198 void onProgress(int progress);
202 * An internal listener for read a file
204 private class AsyncReader implements AsyncResultListener {
206 final Object mSync = new Object();
207 ByteArrayOutputStream mByteBuffer = null;
208 SpannableStringBuilder mBuffer = null;
211 FileSystemObject mReadFso;
212 OnProgressListener mListener;
215 * Constructor of <code>AsyncReader</code>. For enclosing access.
217 public AsyncReader() {
225 public void onAsyncStart() {
226 this.mByteBuffer = new ByteArrayOutputStream((int)this.mReadFso.getSize());
234 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
240 public void onAsyncExitCode(int exitCode) {
241 synchronized (this.mSync) {
250 public void onPartialResult(Object result) {
252 if (result == null) return;
253 byte[] partial = (byte[])result;
255 // Check if the file is a binary file. In this case the editor
257 if (!EditorActivity.this.mReadOnly) {
258 for (int i = 0; i < partial.length-1; i++) {
259 if (!isPrintableCharacter((char)partial[i])) {
260 EditorActivity.this.mBinary = true;
261 EditorActivity.this.mReadOnly = true;
267 this.mByteBuffer.write(partial, 0, partial.length);
268 this.mSize += partial.length;
269 if (this.mListener != null && this.mReadFso != null) {
271 if (this.mReadFso.getSize() != 0) {
272 progress = (int)((this.mSize*100) / this.mReadFso.getSize());
274 this.mListener.onProgress(progress);
276 } catch (Exception e) {
285 public void onException(Exception cause) {
291 * An internal listener for write a file
293 private class AsyncWriter implements AsyncResultListener {
298 * Constructor of <code>AsyncWriter</code>. For enclosing access.
300 public AsyncWriter() {
308 public void onAsyncStart() {/**NON BLOCK**/}
314 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
320 public void onAsyncExitCode(int exitCode) {/**NON BLOCK**/}
326 public void onPartialResult(Object result) {/**NON BLOCK**/}
332 public void onException(Exception cause) {
338 * An internal class to resolve resources for the syntax highlight library.
341 class ResourcesResolver implements ISyntaxHighlightResourcesResolver {
343 public CharSequence getString(String id, String resid) {
344 return EditorActivity.this.getString(
345 ResourcesHelper.getIdentifier(
346 EditorActivity.this.getResources(), "string", resid)); //$NON-NLS-1$
350 public int getInteger(String id, String resid, int def) {
351 return EditorActivity.this.getResources().
353 ResourcesHelper.getIdentifier(
354 EditorActivity.this.getResources(), "integer", resid)); //$NON-NLS-1$
358 public int getColor(String id, String resid, int def) {
359 final Context ctx = EditorActivity.this;
361 // Is default theme color scheme enabled?
362 if (isDefaultThemeColorScheme()) {
363 return ThemeManager.getCurrentTheme(ctx).getColor(ctx, resid);
366 // Use the user-defined settings
367 int[] colors = getUserColorScheme();
368 HighlightColors[] schemeColors = HighlightColors.values();
369 int cc = schemeColors.length;
370 int cc2 = colors.length;
371 for (int i = 0; i < cc; i++) {
372 if (schemeColors[i].getId().compareTo(id) == 0) {
379 return ThemeManager.getCurrentTheme(ctx).getColor(ctx, resid);
384 } catch (Exception ex) {
385 // Resource not found
391 * Method that returns if we should return the default theme color scheme or not
393 * @return boolean Whether return the default theme color scheme or not
395 private boolean isDefaultThemeColorScheme() {
396 Boolean defaultValue =
397 (Boolean)FileManagerSettings.
398 SETTINGS_EDITOR_SH_USE_THEME_DEFAULT.getDefaultValue();
399 return Preferences.getSharedPreferences().getBoolean(
400 FileManagerSettings.SETTINGS_EDITOR_SH_USE_THEME_DEFAULT.getId(),
401 defaultValue.booleanValue());
405 * Method that returns the user-defined color scheme
407 * @return int[] The user-defined color scheme
409 private int[] getUserColorScheme() {
410 String defaultValue =
411 (String)FileManagerSettings.
412 SETTINGS_EDITOR_SH_COLOR_SCHEME.getDefaultValue();
413 String value = Preferences.getSharedPreferences().getString(
414 FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId(),
416 return EditorSHColorSchemePreferenceFragment.toColorShemeArray(value);
423 FileSystemObject mFso;
425 private int mBufferSize;
426 private int mMaxFileSize;
456 ProgressBar mProgressBar;
460 TextView mProgressBarMsg;
466 // No suggestions status
470 boolean mNoSuggestions;
473 private ViewGroup mWordWrapView;
474 private ViewGroup mNoWordWrapView;
480 // Syntax highlight status
484 boolean mSyntaxHighlight;
488 SyntaxHighlightProcessor mSyntaxHighlightProcessor;
489 private int mEditStart;
490 private int mEditEnd;
492 private View mOptionsAnchorView;
494 private final Object mExecSync = new Object();
501 private static final char[] VALID_NON_PRINTABLE_CHARS = {' ', '\t', '\r', '\n'};
506 String mHexLineSeparator;
509 * Intent extra parameter for the path of the file to open.
511 public static final String EXTRA_OPEN_FILE = "extra_open_file"; //$NON-NLS-1$
517 protected void onCreate(Bundle state) {
519 Log.d(TAG, "EditorActivity.onCreate"); //$NON-NLS-1$
522 this.mHandler = new Handler();
524 // Register the broadcast receiver
525 IntentFilter filter = new IntentFilter();
526 filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED);
527 filter.addAction(FileManagerSettings.INTENT_SETTING_CHANGED);
528 registerReceiver(this.mNotificationReceiver, filter);
530 // Generate a random separator
531 this.mHexLineSeparator = UUID.randomUUID().toString();
533 //Set the main layout of the activity
534 setContentView(R.layout.editor);
536 // Get the limit vars
538 getApplicationContext().getResources().getInteger(R.integer.buffer_size);
540 getApplicationContext().getResources().getInteger(R.integer.editor_max_file_size);
543 initTitleActionBar();
549 // Initialize the console
556 super.onCreate(state);
563 protected void onDestroy() {
565 Log.d(TAG, "EditorActivity.onDestroy"); //$NON-NLS-1$
568 // Unregister the receiver
570 unregisterReceiver(this.mNotificationReceiver);
571 } catch (Throwable ex) {
575 //All destroy. Continue
583 public void onConfigurationChanged(Configuration newConfig) {
584 super.onConfigurationChanged(newConfig);
588 * Method that initializes the titlebar of the activity.
590 private void initTitleActionBar() {
591 //Configure the action bar options
592 getActionBar().setBackgroundDrawable(
593 getResources().getDrawable(R.drawable.bg_holo_titlebar));
594 getActionBar().setDisplayOptions(
595 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME);
596 getActionBar().setDisplayHomeAsUpEnabled(true);
597 View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false);
598 this.mTitle = (TextView)customTitle.findViewById(R.id.customtitle_title);
599 this.mTitle.setText(R.string.editor);
600 this.mTitle.setContentDescription(getString(R.string.editor));
601 this.mSave = (ButtonItem)customTitle.findViewById(R.id.ab_button1);
602 this.mSave.setImageResource(R.drawable.ic_holo_light_save);
603 this.mSave.setContentDescription(getString(R.string.actionbar_button_save_cd));
604 this.mSave.setVisibility(View.GONE);
606 ButtonItem configuration = (ButtonItem)customTitle.findViewById(R.id.ab_button2);
607 configuration.setImageResource(R.drawable.ic_holo_light_overflow);
608 configuration.setContentDescription(getString(R.string.actionbar_button_overflow_cd));
610 View status = findViewById(R.id.editor_status);
611 boolean showOptionsMenu = AndroidHelper.showOptionsMenu(getApplicationContext());
612 configuration.setVisibility(showOptionsMenu ? View.VISIBLE : View.GONE);
613 this.mOptionsAnchorView = showOptionsMenu ? configuration : status;
615 getActionBar().setCustomView(customTitle);
619 * Method that initializes the layout and components of the activity.
621 private void initLayout() {
622 this.mEditor = (EditText)findViewById(R.id.editor);
623 this.mEditor.setText(null);
624 this.mEditor.addTextChangedListener(this);
625 this.mEditor.setEnabled(false);
626 this.mWordWrapView = (ViewGroup)findViewById(R.id.editor_word_wrap_view);
627 this.mNoWordWrapView = (ViewGroup)findViewById(R.id.editor_no_word_wrap_view);
628 this.mWordWrapView.setVisibility(View.VISIBLE);
629 this.mNoWordWrapView.setVisibility(View.GONE);
631 this.mNoSuggestions = false;
632 this.mWordWrap = true;
633 this.mSyntaxHighlight = true;
635 // Load the no suggestions setting
636 boolean noSuggestionsSetting = Preferences.getSharedPreferences().getBoolean(
637 FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(),
638 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.
639 getDefaultValue()).booleanValue());
640 if (noSuggestionsSetting != this.mNoSuggestions) {
641 toggleNoSuggestions();
644 // Load the word wrap setting
645 boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean(
646 FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(),
647 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.
648 getDefaultValue()).booleanValue());
649 if (wordWrapSetting != this.mWordWrap) {
653 // Load the syntax highlight setting
654 boolean syntaxHighlighSetting = Preferences.getSharedPreferences().getBoolean(
655 FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(),
656 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.
657 getDefaultValue()).booleanValue());
658 if (syntaxHighlighSetting != this.mSyntaxHighlight) {
659 toggleSyntaxHighlight();
662 this.mProgress = findViewById(R.id.editor_progress);
663 this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar);
664 this.mProgressBarMsg = (TextView)findViewById(R.id.editor_progress_msg);
668 * Method that toggle the no suggestions property of the editor
671 /**package**/ void toggleNoSuggestions() {
672 synchronized (this.mExecSync) {
673 int type = InputType.TYPE_CLASS_TEXT |
674 InputType.TYPE_TEXT_FLAG_MULTI_LINE |
675 InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
676 if (!this.mNoSuggestions) {
677 type |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
679 this.mEditor.setInputType(type);
680 this.mNoSuggestions = !this.mNoSuggestions;
685 * Method that toggle the word wrap property of the editor
688 /**package**/ void toggleWordWrap() {
689 synchronized (this.mExecSync) {
690 ViewGroup vSrc = this.mWordWrap ? this.mWordWrapView : this.mNoWordWrapView;
691 ViewGroup vDst = this.mWordWrap ? this.mNoWordWrapView : this.mWordWrapView;
692 ViewGroup vSrcParent = this.mWordWrap
694 : (ViewGroup)this.mNoWordWrapView.getChildAt(0);
695 ViewGroup vDstParent = this.mWordWrap
696 ? (ViewGroup)this.mNoWordWrapView.getChildAt(0)
697 : this.mWordWrapView;
698 vSrc.setVisibility(View.GONE);
699 vSrcParent.removeView(this.mEditor);
700 vDstParent.addView(this.mEditor);
701 vDst.setVisibility(View.VISIBLE);
703 this.mWordWrap = !this.mWordWrap;
708 * Method that toggles the syntax highlight property of the editor
711 /**package**/ void toggleSyntaxHighlight() {
712 synchronized (this.mExecSync) {
713 if (this.mSyntaxHighlightProcessor != null) {
715 if (this.mSyntaxHighlight) {
716 this.mSyntaxHighlightProcessor.clear(this.mEditor.getText());
718 this.mSyntaxHighlightProcessor.process(this.mEditor.getText());
720 } catch (Exception ex) {
721 // An error in a syntax library, should not break down app.
722 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
726 this.mSyntaxHighlight = !this.mSyntaxHighlight;
731 * Method that reloads the syntax highlight of the current file
734 /**package**/ void reloadSyntaxHighlight() {
735 synchronized (this.mExecSync) {
736 if (this.mSyntaxHighlightProcessor != null) {
738 this.mSyntaxHighlightProcessor.initialize();
739 this.mSyntaxHighlightProcessor.process(this.mEditor.getText());
740 } catch (Exception ex) {
741 // An error in a syntax library, should not break down app.
742 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
752 public boolean onKeyUp(int keyCode, KeyEvent event) {
754 case KeyEvent.KEYCODE_MENU:
755 showOverflowPopUp(this.mOptionsAnchorView);
757 case KeyEvent.KEYCODE_BACK:
761 return super.onKeyUp(keyCode, event);
769 public boolean onOptionsItemSelected(MenuItem item) {
770 switch (item.getItemId()) {
771 case android.R.id.home:
772 if ((getActionBar().getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP)
773 == ActionBar.DISPLAY_HOME_AS_UP) {
778 return super.onOptionsItemSelected(item);
783 * Method that shows a popup with the activity main menu.
785 * @param anchor The anchor of the popup
787 private void showOverflowPopUp(View anchor) {
788 SimpleMenuListAdapter adapter =
789 new HighlightedSimpleMenuListAdapter(this, R.menu.editor);
790 MenuItem noSuggestions = adapter.getMenu().findItem(R.id.mnu_no_suggestions);
791 if (noSuggestions != null) {
793 adapter.getMenu().removeItem(R.id.mnu_no_suggestions);
795 noSuggestions.setChecked(this.mNoSuggestions);
798 MenuItem wordWrap = adapter.getMenu().findItem(R.id.mnu_word_wrap);
799 if (wordWrap != null) {
801 adapter.getMenu().removeItem(R.id.mnu_word_wrap);
803 wordWrap.setChecked(this.mWordWrap);
806 MenuItem syntaxHighlight = adapter.getMenu().findItem(R.id.mnu_syntax_highlight);
807 if (syntaxHighlight != null) {
809 adapter.getMenu().removeItem(R.id.mnu_syntax_highlight);
811 syntaxHighlight.setChecked(this.mSyntaxHighlight);
815 final ListPopupWindow popup =
816 DialogHelper.createListPopupWindow(this, adapter, anchor);
817 popup.setOnItemClickListener(new OnItemClickListener() {
819 public void onItemClick(
820 final AdapterView<?> parent, final View v,
821 final int position, final long id) {
822 final int itemId = (int)id;
824 case R.id.mnu_no_suggestions:
825 toggleNoSuggestions();
827 case R.id.mnu_word_wrap:
830 case R.id.mnu_syntax_highlight:
831 toggleSyntaxHighlight();
833 case R.id.mnu_settings:
835 Intent settings = new Intent(EditorActivity.this, SettingsPreferences.class);
837 PreferenceActivity.EXTRA_SHOW_FRAGMENT,
838 EditorPreferenceFragment.class.getName());
839 startActivity(settings);
849 * Method invoked when an action item is clicked.
851 * @param view The button pushed
853 public void onActionBarItemClick(View view) {
854 switch (view.getId()) {
855 case R.id.ab_button1:
860 case R.id.ab_button2:
861 // Show overflow menu
862 showOverflowPopUp(this.mOptionsAnchorView);
871 * Method that initializes a console
873 private boolean initializeConsole() {
875 ConsoleBuilder.getConsole(this);
876 // There is a console allocated. Use it.
878 } catch (Throwable _throw) {
879 // Capture the exception
880 ExceptionUtil.translateException(this, _throw, false, true);
886 * Method that reads the requested file
888 private void readFile() {
889 // For now editor is not dirty and editable.
891 this.mBinary = false;
893 // Check for a valid action
894 String action = getIntent().getAction();
895 if (action == null ||
896 (action.compareTo(Intent.ACTION_VIEW) != 0) &&
897 (action.compareTo(Intent.ACTION_EDIT) != 0)) {
898 DialogHelper.showToast(
899 this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
902 // This var should be set depending on ACTION_VIEW or ACTION_EDIT action, but for
903 // better compatibility, IntentsActionPolicy use always ACTION_VIEW, so we have
904 // to ignore this check here
905 this.mReadOnly = false;
907 // Read the intent and check that is has a valid request
908 String path = getIntent().getData().getPath();
909 if (path == null || path.length() == 0) {
910 DialogHelper.showToast(
911 this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
915 // Set the title of the dialog
916 File f = new File(path);
917 this.mTitle.setText(f.getName());
919 // Check that the file exists (the real file, not the symlink)
921 this.mFso = CommandHelper.getFileInfo(this, path, true, null);
922 if (this.mFso == null) {
923 DialogHelper.showToast(
924 this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT);
927 } catch (Exception e) {
928 Log.e(TAG, "Failed to get file reference", e); //$NON-NLS-1$
929 DialogHelper.showToast(
930 this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT);
934 // Check that we can handle the length of the file (by device)
935 if (this.mMaxFileSize < this.mFso.getSize()) {
936 DialogHelper.showToast(
937 this, R.string.editor_file_exceed_size_msg, Toast.LENGTH_SHORT);
941 // Get the syntax highlight processor
942 SyntaxHighlightFactory shpFactory =
943 SyntaxHighlightFactory.getDefaultFactory(new ResourcesResolver());
944 this.mSyntaxHighlightProcessor = shpFactory.getSyntaxHighlightProcessor(f);
945 if (this.mSyntaxHighlightProcessor != null) {
946 this.mSyntaxHighlightProcessor.initialize();
949 // Check that we have read access
951 FileHelper.ensureReadAccess(
952 ConsoleBuilder.getConsole(this),
956 // Read the file in background
959 } catch (Exception ex) {
960 ExceptionUtil.translateException(
961 this, ex, false, true, new OnRelaunchCommandResult() {
963 public void onSuccess() {
964 // Read the file in background
969 public void onFailed(Throwable cause) {
974 public void onCancelled() {
982 * Method that does the read of the file in background
986 // Do the load of the file
987 AsyncTask<FileSystemObject, Integer, Boolean> mReadTask =
988 new AsyncTask<FileSystemObject, Integer, Boolean>() {
990 private Exception mCause;
991 private AsyncReader mReader;
992 private boolean changeToBinaryMode;
993 private boolean changeToDisplaying;
996 protected void onPreExecute() {
998 this.changeToBinaryMode = false;
999 this.changeToDisplaying = false;
1000 doProgress(true, 0);
1004 protected Boolean doInBackground(FileSystemObject... params) {
1005 final EditorActivity activity = EditorActivity.this;
1007 // Only one argument (the file to open)
1008 FileSystemObject fso = params[0];
1011 // Read the file in an async listener
1014 // Configure the reader
1015 this.mReader = new AsyncReader();
1016 this.mReader.mReadFso = fso;
1017 this.mReader.mListener = new OnProgressListener() {
1019 @SuppressWarnings("synthetic-access")
1020 public void onProgress(int progress) {
1021 publishProgress(Integer.valueOf(progress));
1025 // Execute the command (read the file)
1026 CommandHelper.read(activity, fso.getFullPath(), this.mReader, null);
1029 synchronized (this.mReader.mSync) {
1030 this.mReader.mSync.wait();
1034 publishProgress(new Integer(100));
1036 // Check if the read was successfully
1037 if (this.mReader.mCause != null) {
1038 this.mCause = this.mReader.mCause;
1039 return Boolean.FALSE;
1044 // Now we have the byte array with all the data. is a binary file?
1045 // Then dump them byte array to hex dump string (only if users settings
1048 Preferences.getSharedPreferences().getBoolean(
1049 FileManagerSettings.SETTINGS_EDITOR_HEXDUMP.getId(),
1050 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_HEXDUMP.
1051 getDefaultValue()).booleanValue());
1052 if (activity.mBinary && hexDump) {
1053 // we do not use the Hexdump helper class, because we need to show the
1054 // progress of the dump process
1055 this.mReader.mBuffer =
1056 new SpannableStringBuilder(
1057 toHexPrintableString(
1059 this.mReader.mByteBuffer.toByteArray())));
1061 this.mReader.mBuffer =
1062 new SpannableStringBuilder(
1063 new String(this.mReader.mByteBuffer.toByteArray()));
1065 this.mReader.mByteBuffer = null;
1068 this.changeToDisplaying = true;
1069 publishProgress(new Integer(0));
1071 } catch (Exception e) {
1073 return Boolean.FALSE;
1076 return Boolean.TRUE;
1080 protected void onProgressUpdate(Integer... values) {
1082 doProgress(true, values[0].intValue());
1086 protected void onPostExecute(Boolean result) {
1087 final EditorActivity activity = EditorActivity.this;
1090 if (!result.booleanValue()) {
1091 if (this.mCause != null) {
1092 ExceptionUtil.translateException(activity, this.mCause);
1093 activity.mEditor.setEnabled(false);
1096 // Now we have the buffer, set the text of the editor
1097 if (activity.mBinary) {
1098 activity.mEditor.setText(
1099 this.mReader.mBuffer, BufferType.NORMAL);
1101 activity.mEditor.setText(
1102 this.mReader.mBuffer, BufferType.EDITABLE);
1104 // Highlight editor text syntax
1105 if (activity.mSyntaxHighlight &&
1106 activity.mSyntaxHighlightProcessor != null) {
1108 activity.mSyntaxHighlightProcessor.process(
1109 activity.mEditor.getText());
1110 } catch (Exception ex) {
1111 // An error in a syntax library, should not break down app.
1112 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
1116 this.mReader.mBuffer = null; //Cleanup
1118 activity.mEditor.setEnabled(!activity.mReadOnly);
1120 // Notify read-only mode
1121 if (activity.mReadOnly) {
1122 DialogHelper.showToast(
1124 R.string.editor_read_only_mode,
1125 Toast.LENGTH_SHORT);
1129 doProgress(false, 0);
1133 protected void onCancelled() {
1134 // Hide the progress
1135 doProgress(false, 0);
1139 * Method that update the progress status
1141 * @param visible If the progress bar need to be hidden
1142 * @param progress The progress
1144 private void doProgress(boolean visible, int progress) {
1145 final EditorActivity activity = EditorActivity.this;
1147 // Show the progress bar
1148 activity.mProgressBar.setProgress(progress);
1149 activity.mProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
1151 if (this.changeToBinaryMode) {
1152 // Hexdump always in nowrap mode
1153 if (activity.mWordWrap) {
1154 activity.toggleWordWrap();
1156 // Hexdump always has no syntax highlight
1157 if (activity.mSyntaxHighlight) {
1158 activity.toggleSyntaxHighlight();
1161 // Show hex dumping text
1162 activity.mProgressBarMsg.setText(R.string.dumping_message);
1163 activity.mEditor.setTextAppearance(activity, R.style.hexeditor_text_appearance);
1164 activity.mEditor.setTypeface(Typeface.MONOSPACE);
1165 this.changeToBinaryMode = false;
1167 else if (this.changeToDisplaying) {
1168 activity.mProgressBarMsg.setText(R.string.displaying_message);
1169 this.changeToDisplaying = false;
1174 * Create a hex dump of the data while show progress to user
1176 * @param data The data to hex dump
1177 * @return StringBuilder The hex dump buffer
1179 private String toHexDump(byte[] data) {
1180 //Change to binary mode
1181 this.changeToBinaryMode = true;
1184 publishProgress(Integer.valueOf(0));
1186 // Calculate max dir size
1187 int length = data.length;
1189 final int DISPLAY_SIZE = 16; // Bytes per line
1190 ByteArrayInputStream bais = new ByteArrayInputStream(data);
1191 byte[] line = new byte[DISPLAY_SIZE];
1194 StringBuilder sb = new StringBuilder();
1195 while ((read = bais.read(line, 0, DISPLAY_SIZE)) != -1) {
1196 //offset dump(16) data\n
1197 String linedata = new String(line, 0, read);
1198 sb.append(HexDump.toHexString(offset));
1199 sb.append(" "); //$NON-NLS-1$
1200 String hexDump = HexDump.toHexString(line, 0, read);
1201 if (hexDump.length() != (DISPLAY_SIZE * 2)) {
1202 char[] array = new char[(DISPLAY_SIZE * 2) - hexDump.length()];
1203 Arrays.fill(array, ' ');
1204 hexDump += new String(array);
1207 sb.append(" "); //$NON-NLS-1$
1208 sb.append(linedata);
1209 sb.append(EditorActivity.this.mHexLineSeparator);
1210 offset += DISPLAY_SIZE;
1211 if (offset % 5 == 0) {
1212 publishProgress(Integer.valueOf((offset * 100) / length));
1216 // End of the dump process
1217 publishProgress(Integer.valueOf(100));
1219 return sb.toString();
1223 * Method that converts to a visual printable hex string
1225 * @param string The string to check
1227 private String toHexPrintableString(String string) {
1228 // Remove characters without visual representation
1229 final String REPLACED_SYMBOL = "."; //$NON-NLS-1$
1230 final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$
1231 String printable = string.replaceAll("\\p{Cntrl}", REPLACED_SYMBOL); //$NON-NLS-1$
1232 printable = printable.replaceAll("[^\\p{Print}]", REPLACED_SYMBOL); //$NON-NLS-1$
1233 printable = printable.replaceAll("\\p{C}", REPLACED_SYMBOL); //$NON-NLS-1$
1234 printable = printable.replaceAll(EditorActivity.this.mHexLineSeparator, NEWLINE);
1238 mReadTask.execute(this.mFso);
1241 private void checkAndWrite() {
1242 // Check that we have write access
1244 FileHelper.ensureWriteAccess(
1245 ConsoleBuilder.getConsole(this),
1252 } catch (Exception ex) {
1253 ExceptionUtil.translateException(
1254 this, ex, false, true, new OnRelaunchCommandResult() {
1256 public void onSuccess() {
1262 public void onFailed(Throwable cause) {/**NON BLOCK**/}
1265 public void onCancelled() {/**NON BLOCK**/}
1271 * Method that write the file.
1276 // Configure the writer
1277 AsyncWriter writer = new AsyncWriter();
1279 // Create the writable command
1280 WriteExecutable cmd =
1281 CommandHelper.write(this, this.mFso.getFullPath(), writer, null);
1283 // Obtain access to the buffer (IMP! don't close the buffer here, it's manage
1285 OutputStream os = cmd.createOutputStream();
1287 // Retrieve the text from the editor
1288 String text = this.mEditor.getText().toString();
1289 ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes());
1293 byte[] data = new byte[this.mBufferSize];
1295 while ((read = bais.read(data, 0, this.mBufferSize)) != -1) {
1296 os.write(data, 0, read);
1301 } catch (Exception e) {/**NON BLOCK**/}
1305 // Ok. Data is written or ensure buffer close
1313 if (writer.mCause != null) {
1314 // Something was wrong. The file probably is corrupted
1315 DialogHelper.showToast(
1316 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1318 // Success. The file was saved
1319 DialogHelper.showToast(
1320 this, R.string.editor_successfully_saved, Toast.LENGTH_SHORT);
1323 // Send a message that allow other activities to update his data
1324 Intent intent = new Intent(FileManagerSettings.INTENT_FILE_CHANGED);
1326 FileManagerSettings.EXTRA_FILE_CHANGED_KEY, this.mFso.getFullPath());
1327 sendBroadcast(intent);
1330 } catch (Exception e) {
1331 // Something was wrong, but the file was NOT written
1332 DialogHelper.showToast(
1333 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1342 public void beforeTextChanged(
1343 CharSequence s, int start, int count, int after) {/**NON BLOCK**/}
1349 public void onTextChanged(CharSequence s, int start, int before, int count) {
1350 this.mEditStart = start;
1351 this.mEditEnd = start + count;
1358 public void afterTextChanged(Editable s) {
1360 if (this.mSyntaxHighlightProcessor != null) {
1361 this.mSyntaxHighlightProcessor.process(s, this.mEditStart, this.mEditEnd);
1366 * Method that sets if the editor is dirty (has changed)
1368 * @param dirty If the editor is dirty
1371 void setDirty(boolean dirty) {
1372 this.mDirty = dirty;
1373 this.mSave.setVisibility(dirty ? View.VISIBLE : View.GONE);
1377 * Check the dirty state of the editor, and ask the user to save the changes
1380 public void checkDirtyState() {
1382 AlertDialog dlg = DialogHelper.createYesNoDialog(
1384 R.string.editor_dirty_ask_title,
1385 R.string.editor_dirty_ask_msg,
1386 new OnClickListener() {
1388 public void onClick(DialogInterface dialog, int which) {
1389 if (which == DialogInterface.BUTTON_POSITIVE) {
1391 setResult(Activity.RESULT_OK);
1396 DialogHelper.delegateDialogShow(this, dlg);
1399 setResult(Activity.RESULT_OK);
1404 * Method that check if a character is valid printable character
1406 * @param c The character to check
1407 * @return boolean If the character is printable
1410 static boolean isPrintableCharacter(char c) {
1411 int cc = VALID_NON_PRINTABLE_CHARS.length;
1412 for (int i = 0; i < cc; i++) {
1413 if (c == VALID_NON_PRINTABLE_CHARS[i]) {
1417 return TextUtils.isGraphic(c);
1421 * Method that applies the current theme to the activity
1425 Theme theme = ThemeManager.getCurrentTheme(this);
1426 theme.setBaseTheme(this, false);
1429 theme.setTitlebarDrawable(this, getActionBar(), "titlebar_drawable"); //$NON-NLS-1$
1430 View v = getActionBar().getCustomView().findViewById(R.id.customtitle_title);
1431 theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$
1432 v = findViewById(R.id.ab_button1);
1433 theme.setImageDrawable(this, (ImageView)v, "ab_save_drawable"); //$NON-NLS-1$
1435 v = findViewById(R.id.editor_layout);
1436 theme.setBackgroundDrawable(this, v, "background_drawable"); //$NON-NLS-1$
1437 v = findViewById(R.id.editor);
1438 theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$
1440 Drawable dw = theme.getDrawable(this, "horizontal_progress_bar"); //$NON-NLS-1$
1441 this.mProgressBar.setProgressDrawable(dw);
1443 // Need a full process of syntax highlight
1444 if (!this.mBinary && this.mSyntaxHighlight && this.mSyntaxHighlightProcessor != null) {
1445 reloadSyntaxHighlight();