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.net.Uri;
32 import android.os.AsyncTask;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.preference.PreferenceActivity;
36 import android.text.Editable;
37 import android.text.InputType;
38 import android.text.SpannableStringBuilder;
39 import android.text.TextWatcher;
40 import android.util.Log;
41 import android.view.KeyEvent;
42 import android.view.LayoutInflater;
43 import android.view.MenuItem;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.AdapterView;
47 import android.widget.AdapterView.OnItemClickListener;
48 import android.widget.ArrayAdapter;
49 import android.widget.EditText;
50 import android.widget.ImageView;
51 import android.widget.ListPopupWindow;
52 import android.widget.ListView;
53 import android.widget.ProgressBar;
54 import android.widget.TextView;
55 import android.widget.TextView.BufferType;
56 import android.widget.Toast;
58 import com.android.internal.util.HexDump;
59 import com.cyanogenmod.filemanager.R;
60 import com.cyanogenmod.filemanager.activities.preferences.EditorPreferenceFragment;
61 import com.cyanogenmod.filemanager.activities.preferences.EditorSHColorSchemePreferenceFragment;
62 import com.cyanogenmod.filemanager.activities.preferences.SettingsPreferences;
63 import com.cyanogenmod.filemanager.adapters.HighlightedSimpleMenuListAdapter;
64 import com.cyanogenmod.filemanager.adapters.SimpleMenuListAdapter;
65 import com.cyanogenmod.filemanager.ash.HighlightColors;
66 import com.cyanogenmod.filemanager.ash.ISyntaxHighlightResourcesResolver;
67 import com.cyanogenmod.filemanager.ash.SyntaxHighlightFactory;
68 import com.cyanogenmod.filemanager.ash.SyntaxHighlightProcessor;
69 import com.cyanogenmod.filemanager.commands.AsyncResultListener;
70 import com.cyanogenmod.filemanager.commands.WriteExecutable;
71 import com.cyanogenmod.filemanager.console.ConsoleBuilder;
72 import com.cyanogenmod.filemanager.model.FileSystemObject;
73 import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
74 import com.cyanogenmod.filemanager.preferences.Preferences;
75 import com.cyanogenmod.filemanager.ui.ThemeManager;
76 import com.cyanogenmod.filemanager.ui.ThemeManager.Theme;
77 import com.cyanogenmod.filemanager.ui.widgets.ButtonItem;
78 import com.cyanogenmod.filemanager.util.AndroidHelper;
79 import com.cyanogenmod.filemanager.util.CommandHelper;
80 import com.cyanogenmod.filemanager.util.DialogHelper;
81 import com.cyanogenmod.filemanager.util.ExceptionUtil;
82 import com.cyanogenmod.filemanager.util.ExceptionUtil.OnRelaunchCommandResult;
83 import com.cyanogenmod.filemanager.util.FileHelper;
84 import com.cyanogenmod.filemanager.util.MediaHelper;
85 import com.cyanogenmod.filemanager.util.ResourcesHelper;
86 import com.cyanogenmod.filemanager.util.StringHelper;
88 import java.io.BufferedReader;
89 import java.io.ByteArrayInputStream;
90 import java.io.ByteArrayOutputStream;
92 import java.io.OutputStream;
93 import java.io.StringReader;
94 import java.util.ArrayList;
95 import java.util.Arrays;
96 import java.util.List;
97 import java.util.UUID;
100 * An internal activity for view and edit files.
102 public class EditorActivity extends Activity implements TextWatcher {
104 private static final String TAG = "EditorActivity"; //$NON-NLS-1$
106 private static boolean DEBUG = false;
108 private static final int WRITE_RETRIES = 3;
110 private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() {
112 public void onReceive(Context context, Intent intent) {
113 if (intent != null) {
114 if (intent.getAction().compareTo(FileManagerSettings.INTENT_THEME_CHANGED) == 0) {
118 if (intent.getAction().compareTo(FileManagerSettings.INTENT_SETTING_CHANGED) == 0) {
119 // The settings has changed
120 String key = intent.getStringExtra(FileManagerSettings.EXTRA_SETTING_CHANGED_KEY);
122 final EditorActivity activity = EditorActivity.this;
125 if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId()) == 0) {
126 // Ignore in binary files
127 if (activity.mBinary) return;
129 // Do we have a different setting?
130 boolean noSuggestionsSetting =
131 Preferences.getSharedPreferences().getBoolean(
132 FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(),
133 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.
134 getDefaultValue()).booleanValue());
135 if (noSuggestionsSetting != activity.mNoSuggestions) {
136 activity.mHandler.post(new Runnable() {
139 toggleNoSuggestions();
145 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId()) == 0) {
146 // Ignore in binary files
147 if (activity.mBinary) return;
149 // Do we have a different setting?
150 boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean(
151 FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(),
152 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.
153 getDefaultValue()).booleanValue());
154 if (wordWrapSetting != activity.mWordWrap) {
155 activity.mHandler.post(new Runnable() {
164 // Default theme color scheme
166 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId()) == 0) {
167 // Ignore in binary files
168 if (activity.mBinary) return;
170 // Do we have a different setting?
171 boolean syntaxHighlightSetting =
172 Preferences.getSharedPreferences().getBoolean(
173 FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(),
174 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.
175 getDefaultValue()).booleanValue());
176 if (syntaxHighlightSetting != activity.mSyntaxHighlight) {
177 activity.mHandler.post(new Runnable() {
180 toggleSyntaxHighlight();
185 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SH_USE_THEME_DEFAULT.getId()) == 0 ||
186 key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId()) == 0 ) {
187 // Ignore in binary files
188 if (activity.mBinary) return;
190 // Reload the syntax highlight
191 activity.mHandler.post(new Runnable() {
194 reloadSyntaxHighlight();
205 private class HexDumpAdapter extends ArrayAdapter<String> {
206 private class ViewHolder {
210 public HexDumpAdapter(Context context, List<String> data) {
211 super(context, R.layout.hexdump_line, data);
215 public View getView(int position, View convertView, ViewGroup parent) {
216 View v = convertView;
218 final Context context = getContext();
219 LayoutInflater inflater = LayoutInflater.from(context);
220 Theme theme = ThemeManager.getCurrentTheme(context);
222 v = inflater.inflate(R.layout.hexdump_line, parent, false);
223 ViewHolder viewHolder = new EditorActivity.HexDumpAdapter.ViewHolder();
224 viewHolder.mTextView = (TextView)v.findViewById(android.R.id.text1);
226 viewHolder.mTextView.setTextAppearance(context, R.style.hexeditor_text_appearance);
227 viewHolder.mTextView.setTypeface(mHexTypeface);
228 theme.setTextColor(context, viewHolder.mTextView, "text_color"); //$NON-NLS-1$
230 v.setTag(viewHolder);
233 String text = getItem(position);
234 ViewHolder viewHolder = (ViewHolder)v.getTag();
235 viewHolder.mTextView.setText(text);
242 * Internal interface to notify progress update
244 private interface OnProgressListener {
245 void onProgress(int progress);
249 * An internal listener for read a file
251 private class AsyncReader implements AsyncResultListener {
253 final Object mSync = new Object();
254 ByteArrayOutputStream mByteBuffer = null;
255 ArrayList<String> mBinaryBuffer = null;
256 SpannableStringBuilder mBuffer = null;
259 FileSystemObject mReadFso;
260 OnProgressListener mListener;
263 * Constructor of <code>AsyncReader</code>. For enclosing access.
265 public AsyncReader() {
273 public void onAsyncStart() {
274 this.mByteBuffer = new ByteArrayOutputStream((int)this.mReadFso.getSize());
282 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
288 public void onAsyncExitCode(int exitCode) {
289 synchronized (this.mSync) {
298 public void onPartialResult(Object result) {
300 if (result == null) return;
301 byte[] partial = (byte[])result;
303 // Check if the file is a binary file. In this case the editor
305 if (!EditorActivity.this.mReadOnly) {
306 for (int i = 0; i < partial.length-1; i++) {
307 if (!StringHelper.isPrintableCharacter((char)partial[i])) {
308 EditorActivity.this.mBinary = true;
309 EditorActivity.this.mReadOnly = true;
315 this.mByteBuffer.write(partial, 0, partial.length);
316 this.mSize += partial.length;
317 if (this.mListener != null && this.mReadFso != null) {
319 if (this.mReadFso.getSize() != 0) {
320 progress = (int)((this.mSize*100) / this.mReadFso.getSize());
322 this.mListener.onProgress(progress);
324 } catch (Exception e) {
333 public void onException(Exception cause) {
339 * An internal listener for write a file
341 private class AsyncWriter implements AsyncResultListener {
346 * Constructor of <code>AsyncWriter</code>. For enclosing access.
348 public AsyncWriter() {
356 public void onAsyncStart() {/**NON BLOCK**/}
362 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
368 public void onAsyncExitCode(int exitCode) {/**NON BLOCK**/}
374 public void onPartialResult(Object result) {/**NON BLOCK**/}
380 public void onException(Exception cause) {
386 * An internal class to resolve resources for the syntax highlight library.
389 class ResourcesResolver implements ISyntaxHighlightResourcesResolver {
391 public CharSequence getString(String id, String resid) {
392 return EditorActivity.this.getString(
393 ResourcesHelper.getIdentifier(
394 EditorActivity.this.getResources(), "string", resid)); //$NON-NLS-1$
398 public int getInteger(String id, String resid, int def) {
399 return EditorActivity.this.getResources().
401 ResourcesHelper.getIdentifier(
402 EditorActivity.this.getResources(), "integer", resid)); //$NON-NLS-1$
406 public int getColor(String id, String resid, int def) {
407 final Context ctx = EditorActivity.this;
409 // Is default theme color scheme enabled?
410 if (isDefaultThemeColorScheme()) {
411 return ThemeManager.getCurrentTheme(ctx).getColor(ctx, resid);
414 // Use the user-defined settings
415 int[] colors = getUserColorScheme();
416 HighlightColors[] schemeColors = HighlightColors.values();
417 int cc = schemeColors.length;
418 int cc2 = colors.length;
419 for (int i = 0; i < cc; i++) {
420 if (schemeColors[i].getId().compareTo(id) == 0) {
427 return ThemeManager.getCurrentTheme(ctx).getColor(ctx, resid);
432 } catch (Exception ex) {
433 // Resource not found
439 * Method that returns if we should return the default theme color scheme or not
441 * @return boolean Whether return the default theme color scheme or not
443 private boolean isDefaultThemeColorScheme() {
444 Boolean defaultValue =
445 (Boolean)FileManagerSettings.
446 SETTINGS_EDITOR_SH_USE_THEME_DEFAULT.getDefaultValue();
447 return Preferences.getSharedPreferences().getBoolean(
448 FileManagerSettings.SETTINGS_EDITOR_SH_USE_THEME_DEFAULT.getId(),
449 defaultValue.booleanValue());
453 * Method that returns the user-defined color scheme
455 * @return int[] The user-defined color scheme
457 private int[] getUserColorScheme() {
458 String defaultValue =
459 (String)FileManagerSettings.
460 SETTINGS_EDITOR_SH_COLOR_SCHEME.getDefaultValue();
461 String value = Preferences.getSharedPreferences().getString(
462 FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId(),
464 return EditorSHColorSchemePreferenceFragment.toColorShemeArray(value);
471 FileSystemObject mFso;
473 private int mBufferSize;
474 private long mMaxFileSize;
500 ListView mBinaryEditor;
508 ProgressBar mProgressBar;
512 TextView mProgressBarMsg;
518 // No suggestions status
522 boolean mNoSuggestions;
525 private ViewGroup mWordWrapView;
526 private ViewGroup mNoWordWrapView;
532 // Syntax highlight status
536 boolean mSyntaxHighlight;
540 SyntaxHighlightProcessor mSyntaxHighlightProcessor;
541 private int mEditStart;
542 private int mEditEnd;
544 private View mOptionsAnchorView;
546 private Typeface mHexTypeface;
548 private final Object mExecSync = new Object();
558 String mHexLineSeparator;
561 * Intent extra parameter for the path of the file to open.
563 public static final String EXTRA_OPEN_FILE = "extra_open_file"; //$NON-NLS-1$
569 protected void onCreate(Bundle state) {
571 Log.d(TAG, "EditorActivity.onCreate"); //$NON-NLS-1$
574 this.mHandler = new Handler();
576 // Load typeface for hex editor
577 mHexTypeface = Typeface.createFromAsset(getAssets(), "fonts/Courier-Prime.ttf");
579 // Register the broadcast receiver
580 IntentFilter filter = new IntentFilter();
581 filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED);
582 filter.addAction(FileManagerSettings.INTENT_SETTING_CHANGED);
583 registerReceiver(this.mNotificationReceiver, filter);
585 // Generate a random separator
586 this.mHexLineSeparator = UUID.randomUUID().toString() + UUID.randomUUID().toString();
588 // Set the theme before setContentView
589 Theme theme = ThemeManager.getCurrentTheme(this);
590 theme.setBaseTheme(this, false);
592 //Set the main layout of the activity
593 setContentView(R.layout.editor);
595 // Get the limit vars
596 this.mBufferSize = getResources().getInteger(R.integer.buffer_size);
597 long availMem = AndroidHelper.getAvailableMemory(this);
598 this.mMaxFileSize = Math.min(availMem,
599 getResources().getInteger(R.integer.editor_max_file_size));
602 initTitleActionBar();
608 // Initialize the console
615 super.onCreate(state);
622 protected void onDestroy() {
624 Log.d(TAG, "EditorActivity.onDestroy"); //$NON-NLS-1$
627 // Unregister the receiver
629 unregisterReceiver(this.mNotificationReceiver);
630 } catch (Throwable ex) {
634 //All destroy. Continue
642 public void onConfigurationChanged(Configuration newConfig) {
643 super.onConfigurationChanged(newConfig);
647 * Method that initializes the titlebar of the activity.
649 private void initTitleActionBar() {
650 //Configure the action bar options
651 getActionBar().setBackgroundDrawable(
652 getResources().getDrawable(R.drawable.bg_holo_titlebar));
653 getActionBar().setDisplayOptions(
654 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME);
655 getActionBar().setDisplayHomeAsUpEnabled(true);
656 View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false);
657 this.mTitle = (TextView)customTitle.findViewById(R.id.customtitle_title);
658 this.mTitle.setText(R.string.editor);
659 this.mTitle.setContentDescription(getString(R.string.editor));
660 this.mSave = (ButtonItem)customTitle.findViewById(R.id.ab_button1);
661 this.mSave.setImageResource(R.drawable.ic_holo_light_save);
662 this.mSave.setContentDescription(getString(R.string.actionbar_button_save_cd));
663 this.mSave.setVisibility(View.GONE);
665 ButtonItem configuration = (ButtonItem)customTitle.findViewById(R.id.ab_button2);
666 configuration.setImageResource(R.drawable.ic_holo_light_overflow);
667 configuration.setContentDescription(getString(R.string.actionbar_button_overflow_cd));
669 View status = findViewById(R.id.editor_status);
670 boolean showOptionsMenu = AndroidHelper.showOptionsMenu(this);
671 configuration.setVisibility(showOptionsMenu ? View.VISIBLE : View.GONE);
672 this.mOptionsAnchorView = showOptionsMenu ? configuration : status;
674 getActionBar().setCustomView(customTitle);
678 * Method that initializes the layout and components of the activity.
680 private void initLayout() {
681 this.mEditor = (EditText)findViewById(R.id.editor);
682 this.mEditor.setText(null);
683 this.mEditor.addTextChangedListener(this);
684 this.mEditor.setEnabled(false);
685 this.mWordWrapView = (ViewGroup)findViewById(R.id.editor_word_wrap_view);
686 this.mNoWordWrapView = (ViewGroup)findViewById(R.id.editor_no_word_wrap_view);
687 this.mWordWrapView.setVisibility(View.VISIBLE);
688 this.mNoWordWrapView.setVisibility(View.GONE);
690 this.mBinaryEditor = (ListView)findViewById(R.id.editor_binary);
692 this.mNoSuggestions = false;
693 this.mWordWrap = true;
694 this.mSyntaxHighlight = true;
696 // Load the no suggestions setting
697 boolean noSuggestionsSetting = Preferences.getSharedPreferences().getBoolean(
698 FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(),
699 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.
700 getDefaultValue()).booleanValue());
701 if (noSuggestionsSetting != this.mNoSuggestions) {
702 toggleNoSuggestions();
705 // Load the word wrap setting
706 boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean(
707 FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(),
708 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.
709 getDefaultValue()).booleanValue());
710 if (wordWrapSetting != this.mWordWrap) {
714 // Load the syntax highlight setting
715 boolean syntaxHighlighSetting = Preferences.getSharedPreferences().getBoolean(
716 FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(),
717 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.
718 getDefaultValue()).booleanValue());
719 if (syntaxHighlighSetting != this.mSyntaxHighlight) {
720 toggleSyntaxHighlight();
723 this.mProgress = findViewById(R.id.editor_progress);
724 this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar);
725 this.mProgressBarMsg = (TextView)findViewById(R.id.editor_progress_msg);
729 * Method that toggle the no suggestions property of the editor
732 /**package**/ void toggleNoSuggestions() {
733 synchronized (this.mExecSync) {
734 int type = InputType.TYPE_CLASS_TEXT |
735 InputType.TYPE_TEXT_FLAG_MULTI_LINE |
736 InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
737 if (!this.mNoSuggestions) {
738 type |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
740 this.mEditor.setInputType(type);
741 this.mNoSuggestions = !this.mNoSuggestions;
746 * Method that toggle the word wrap property of the editor
749 /**package**/ void toggleWordWrap() {
750 synchronized (this.mExecSync) {
751 ViewGroup vSrc = this.mWordWrap ? this.mWordWrapView : this.mNoWordWrapView;
752 ViewGroup vDst = this.mWordWrap ? this.mNoWordWrapView : this.mWordWrapView;
753 ViewGroup vSrcParent = this.mWordWrap
755 : (ViewGroup)this.mNoWordWrapView.getChildAt(0);
756 ViewGroup vDstParent = this.mWordWrap
757 ? (ViewGroup)this.mNoWordWrapView.getChildAt(0)
758 : this.mWordWrapView;
759 vSrc.setVisibility(View.GONE);
760 vSrcParent.removeView(this.mEditor);
761 vDstParent.addView(this.mEditor);
762 vDst.setVisibility(View.VISIBLE);
764 this.mWordWrap = !this.mWordWrap;
769 * Method that toggles the syntax highlight property of the editor
772 /**package**/ void toggleSyntaxHighlight() {
773 synchronized (this.mExecSync) {
774 if (this.mSyntaxHighlightProcessor != null) {
776 if (this.mSyntaxHighlight) {
777 this.mSyntaxHighlightProcessor.clear(this.mEditor.getText());
779 this.mSyntaxHighlightProcessor.process(this.mEditor.getText());
781 } catch (Exception ex) {
782 // An error in a syntax library, should not break down app.
783 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
787 this.mSyntaxHighlight = !this.mSyntaxHighlight;
792 * Method that reloads the syntax highlight of the current file
795 /**package**/ void reloadSyntaxHighlight() {
796 synchronized (this.mExecSync) {
797 if (this.mSyntaxHighlightProcessor != null) {
799 this.mSyntaxHighlightProcessor.initialize();
800 this.mSyntaxHighlightProcessor.process(this.mEditor.getText());
801 } catch (Exception ex) {
802 // An error in a syntax library, should not break down app.
803 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
813 public boolean onKeyUp(int keyCode, KeyEvent event) {
815 case KeyEvent.KEYCODE_MENU:
816 showOverflowPopUp(this.mOptionsAnchorView);
818 case KeyEvent.KEYCODE_BACK:
822 return super.onKeyUp(keyCode, event);
830 public boolean onOptionsItemSelected(MenuItem item) {
831 switch (item.getItemId()) {
832 case android.R.id.home:
833 if ((getActionBar().getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP)
834 == ActionBar.DISPLAY_HOME_AS_UP) {
839 return super.onOptionsItemSelected(item);
844 * Method that shows a popup with the activity main menu.
846 * @param anchor The anchor of the popup
848 private void showOverflowPopUp(View anchor) {
849 SimpleMenuListAdapter adapter =
850 new HighlightedSimpleMenuListAdapter(this, R.menu.editor);
851 MenuItem noSuggestions = adapter.getMenu().findItem(R.id.mnu_no_suggestions);
852 if (noSuggestions != null) {
854 adapter.getMenu().removeItem(R.id.mnu_no_suggestions);
856 noSuggestions.setChecked(this.mNoSuggestions);
859 MenuItem wordWrap = adapter.getMenu().findItem(R.id.mnu_word_wrap);
860 if (wordWrap != null) {
862 adapter.getMenu().removeItem(R.id.mnu_word_wrap);
864 wordWrap.setChecked(this.mWordWrap);
867 MenuItem syntaxHighlight = adapter.getMenu().findItem(R.id.mnu_syntax_highlight);
868 if (syntaxHighlight != null) {
870 adapter.getMenu().removeItem(R.id.mnu_syntax_highlight);
872 syntaxHighlight.setChecked(this.mSyntaxHighlight);
876 final ListPopupWindow popup =
877 DialogHelper.createListPopupWindow(this, adapter, anchor);
878 popup.setOnItemClickListener(new OnItemClickListener() {
880 public void onItemClick(
881 final AdapterView<?> parent, final View v,
882 final int position, final long id) {
883 final int itemId = (int)id;
885 case R.id.mnu_no_suggestions:
886 toggleNoSuggestions();
888 case R.id.mnu_word_wrap:
891 case R.id.mnu_syntax_highlight:
892 toggleSyntaxHighlight();
894 case R.id.mnu_settings:
896 Intent settings = new Intent(EditorActivity.this, SettingsPreferences.class);
898 PreferenceActivity.EXTRA_SHOW_FRAGMENT,
899 EditorPreferenceFragment.class.getName());
900 startActivity(settings);
910 * Method invoked when an action item is clicked.
912 * @param view The button pushed
914 public void onActionBarItemClick(View view) {
915 switch (view.getId()) {
916 case R.id.ab_button1:
921 case R.id.ab_button2:
922 // Show overflow menu
923 showOverflowPopUp(this.mOptionsAnchorView);
932 * Method that initializes a console
934 private boolean initializeConsole() {
936 ConsoleBuilder.getConsole(this);
937 // There is a console allocated. Use it.
939 } catch (Throwable _throw) {
940 // Capture the exception
941 ExceptionUtil.translateException(this, _throw, false, true);
947 * Method that reads the requested file
949 private void readFile() {
950 // For now editor is not dirty and editable.
952 this.mBinary = false;
954 // Check for a valid action
955 String action = getIntent().getAction();
956 if (action == null ||
957 (action.compareTo(Intent.ACTION_VIEW) != 0) &&
958 (action.compareTo(Intent.ACTION_EDIT) != 0)) {
959 DialogHelper.showToast(
960 this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
963 // This var should be set depending on ACTION_VIEW or ACTION_EDIT action, but for
964 // better compatibility, IntentsActionPolicy use always ACTION_VIEW, so we have
965 // to ignore this check here
966 this.mReadOnly = false;
968 // Read the intent and check that is has a valid request
969 String path = uriToPath(this, getIntent().getData());
970 if (path == null || path.length() == 0) {
971 DialogHelper.showToast(
972 this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
976 // Set the title of the dialog
977 File f = new File(path);
978 this.mTitle.setText(f.getName());
980 // Check that the file exists (the real file, not the symlink)
982 this.mFso = CommandHelper.getFileInfo(this, path, true, null);
983 if (this.mFso == null) {
984 DialogHelper.showToast(
985 this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT);
988 } catch (Exception e) {
989 Log.e(TAG, "Failed to get file reference", e); //$NON-NLS-1$
990 DialogHelper.showToast(
991 this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT);
995 // Check that we can handle the length of the file (by device)
996 if (this.mMaxFileSize < this.mFso.getSize()) {
997 DialogHelper.showToast(
998 this, R.string.editor_file_exceed_size_msg, Toast.LENGTH_SHORT);
1002 // Get the syntax highlight processor
1003 SyntaxHighlightFactory shpFactory =
1004 SyntaxHighlightFactory.getDefaultFactory(new ResourcesResolver());
1005 this.mSyntaxHighlightProcessor = shpFactory.getSyntaxHighlightProcessor(f);
1006 if (this.mSyntaxHighlightProcessor != null) {
1007 this.mSyntaxHighlightProcessor.initialize();
1010 // Check that we have read access
1012 FileHelper.ensureReadAccess(
1013 ConsoleBuilder.getConsole(this),
1017 // Read the file in background
1020 } catch (Exception ex) {
1021 ExceptionUtil.translateException(
1022 this, ex, false, true, new OnRelaunchCommandResult() {
1024 public void onSuccess() {
1025 // Read the file in background
1030 public void onFailed(Throwable cause) {
1035 public void onCancelled() {
1043 * Method that does the read of the file in background
1047 // Do the load of the file
1048 AsyncTask<FileSystemObject, Integer, Boolean> mReadTask =
1049 new AsyncTask<FileSystemObject, Integer, Boolean>() {
1051 private Exception mCause;
1052 private AsyncReader mReader;
1053 private boolean changeToBinaryMode;
1054 private boolean changeToDisplaying;
1057 protected void onPreExecute() {
1058 // Show the progress
1059 this.changeToBinaryMode = false;
1060 this.changeToDisplaying = false;
1061 doProgress(true, 0);
1065 protected Boolean doInBackground(FileSystemObject... params) {
1066 final EditorActivity activity = EditorActivity.this;
1068 // Only one argument (the file to open)
1069 FileSystemObject fso = params[0];
1072 // Read the file in an async listener
1075 // Configure the reader
1076 this.mReader = new AsyncReader();
1077 this.mReader.mReadFso = fso;
1078 this.mReader.mListener = new OnProgressListener() {
1080 @SuppressWarnings("synthetic-access")
1081 public void onProgress(int progress) {
1082 publishProgress(Integer.valueOf(progress));
1086 // Execute the command (read the file)
1087 CommandHelper.read(activity, fso.getFullPath(), this.mReader, null);
1090 synchronized (this.mReader.mSync) {
1091 this.mReader.mSync.wait();
1095 publishProgress(new Integer(100));
1097 // Check if the read was successfully
1098 if (this.mReader.mCause != null) {
1099 this.mCause = this.mReader.mCause;
1100 return Boolean.FALSE;
1105 // Now we have the byte array with all the data. is a binary file?
1106 // Then dump them byte array to hex dump string (only if users settings
1109 Preferences.getSharedPreferences().getBoolean(
1110 FileManagerSettings.SETTINGS_EDITOR_HEXDUMP.getId(),
1111 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_HEXDUMP.
1112 getDefaultValue()).booleanValue());
1113 if (activity.mBinary && hexDump) {
1114 // we do not use the Hexdump helper class, because we need to show the
1115 // progress of the dump process
1116 final String data = toHexPrintableString(toHexDump(
1117 this.mReader.mByteBuffer.toByteArray()));
1118 this.mReader.mBinaryBuffer = new ArrayList<String>();
1119 BufferedReader reader = new BufferedReader(new StringReader(data));
1121 while ((line = reader.readLine()) != null) {
1122 this.mReader.mBinaryBuffer.add(line);
1124 Log.i(TAG, "Bytes read: " + data.length()); //$NON-NLS-1$
1126 final String data = new String(this.mReader.mByteBuffer.toByteArray());
1127 this.mReader.mBuffer = new SpannableStringBuilder(data);
1128 Log.i(TAG, "Bytes read: " + data.getBytes().length); //$NON-NLS-1$
1130 this.mReader.mByteBuffer = null;
1133 this.changeToDisplaying = true;
1134 publishProgress(new Integer(0));
1136 } catch (Exception e) {
1138 return Boolean.FALSE;
1141 return Boolean.TRUE;
1145 protected void onProgressUpdate(Integer... values) {
1147 doProgress(true, values[0].intValue());
1151 protected void onPostExecute(Boolean result) {
1152 final EditorActivity activity = EditorActivity.this;
1154 if (!result.booleanValue()) {
1155 if (this.mCause != null) {
1156 ExceptionUtil.translateException(activity, this.mCause);
1157 activity.mEditor.setEnabled(false);
1160 // Now we have the buffer, set the text of the editor
1161 if (activity.mBinary) {
1162 HexDumpAdapter adapter = new HexDumpAdapter(EditorActivity.this,
1163 this.mReader.mBinaryBuffer);
1164 mBinaryEditor.setAdapter(adapter);
1167 this.mReader.mBinaryBuffer = null;
1169 activity.mEditor.setText(
1170 this.mReader.mBuffer, BufferType.EDITABLE);
1172 // Highlight editor text syntax
1173 if (activity.mSyntaxHighlight &&
1174 activity.mSyntaxHighlightProcessor != null) {
1176 activity.mSyntaxHighlightProcessor.process(
1177 activity.mEditor.getText());
1178 } catch (Exception ex) {
1179 // An error in a syntax library, should not break down app.
1180 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
1185 this.mReader.mBuffer = null;
1189 activity.mEditor.setEnabled(!activity.mReadOnly);
1191 // Notify read-only mode
1192 if (activity.mReadOnly) {
1193 DialogHelper.showToast(
1195 R.string.editor_read_only_mode,
1196 Toast.LENGTH_SHORT);
1200 doProgress(false, 0);
1204 protected void onCancelled() {
1205 // Hide the progress
1206 doProgress(false, 0);
1210 * Method that update the progress status
1212 * @param visible If the progress bar need to be hidden
1213 * @param progress The progress
1215 private void doProgress(boolean visible, int progress) {
1216 final EditorActivity activity = EditorActivity.this;
1218 // Show the progress bar
1219 activity.mProgressBar.setProgress(progress);
1220 activity.mProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
1222 if (this.changeToBinaryMode) {
1223 mWordWrapView.setVisibility(View.GONE);
1224 mNoWordWrapView.setVisibility(View.GONE);
1225 mBinaryEditor.setVisibility(View.VISIBLE);
1227 // Show hex dumping text
1228 activity.mProgressBarMsg.setText(R.string.dumping_message);
1229 this.changeToBinaryMode = false;
1231 else if (this.changeToDisplaying) {
1232 activity.mProgressBarMsg.setText(R.string.displaying_message);
1233 this.changeToDisplaying = false;
1238 * Create a hex dump of the data while show progress to user
1240 * @param data The data to hex dump
1241 * @return StringBuilder The hex dump buffer
1243 private String toHexDump(byte[] data) {
1244 //Change to binary mode
1245 this.changeToBinaryMode = true;
1248 publishProgress(Integer.valueOf(0));
1250 // Calculate max dir size
1251 int length = data.length;
1253 final int DISPLAY_SIZE = 16; // Bytes per line
1254 ByteArrayInputStream bais = new ByteArrayInputStream(data);
1255 byte[] line = new byte[DISPLAY_SIZE];
1258 StringBuilder sb = new StringBuilder();
1259 while ((read = bais.read(line, 0, DISPLAY_SIZE)) != -1) {
1260 //offset dump(16) data\n
1261 String linedata = new String(line, 0, read);
1262 sb.append(HexDump.toHexString(offset));
1263 sb.append(" "); //$NON-NLS-1$
1264 String hexDump = HexDump.toHexString(line, 0, read);
1265 if (hexDump.length() != (DISPLAY_SIZE * 2)) {
1266 char[] array = new char[(DISPLAY_SIZE * 2) - hexDump.length()];
1267 Arrays.fill(array, ' ');
1268 hexDump += new String(array);
1271 sb.append(" "); //$NON-NLS-1$
1272 sb.append(linedata);
1273 sb.append(EditorActivity.this.mHexLineSeparator);
1274 offset += DISPLAY_SIZE;
1275 if (offset % 5 == 0) {
1276 publishProgress(Integer.valueOf((offset * 100) / length));
1280 // End of the dump process
1281 publishProgress(Integer.valueOf(100));
1283 return sb.toString();
1287 * Method that converts to a visual printable hex string
1289 * @param string The string to check
1291 private String toHexPrintableString(String string) {
1292 // Remove characters without visual representation
1293 final String REPLACED_SYMBOL = "."; //$NON-NLS-1$
1294 final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$
1295 String printable = string.replaceAll("\\p{Cntrl}", REPLACED_SYMBOL); //$NON-NLS-1$
1296 printable = printable.replaceAll("[^\\p{Print}]", REPLACED_SYMBOL); //$NON-NLS-1$
1297 printable = printable.replaceAll("\\p{C}", REPLACED_SYMBOL); //$NON-NLS-1$
1298 printable = printable.replaceAll(EditorActivity.this.mHexLineSeparator, NEWLINE);
1302 mReadTask.execute(this.mFso);
1305 private void checkAndWrite() {
1306 // Check that we have write access
1308 FileHelper.ensureWriteAccess(
1309 ConsoleBuilder.getConsole(this),
1316 } catch (Exception ex) {
1317 ExceptionUtil.translateException(
1318 this, ex, false, true, new OnRelaunchCommandResult() {
1320 public void onSuccess() {
1326 public void onFailed(Throwable cause) {/**NON BLOCK**/}
1329 public void onCancelled() {/**NON BLOCK**/}
1335 * Method that checks that the write to disk operation was successfully and the
1336 * expected bytes are written to disk.
1339 void ensureSyncWrite() {
1341 for (int i = 0; i < WRITE_RETRIES; i++) {
1342 // Configure the writer
1343 AsyncWriter writer = new AsyncWriter();
1346 final byte[] data = this.mEditor.getText().toString().getBytes();
1347 long expected = data.length;
1348 syncWrite(writer, data);
1354 if (writer.mCause != null) {
1355 Log.e(TAG, "Write operation failed. Retries: " + i, writer.mCause);
1356 if (i == (WRITE_RETRIES-1)) {
1357 // Something was wrong. The file probably is corrupted
1358 DialogHelper.showToast(
1359 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1367 // Check that all the bytes were written
1368 FileSystemObject fso =
1369 CommandHelper.getFileInfo(this, this.mFso.getFullPath(), true, null);
1370 if (fso == null || fso.getSize() != expected) {
1371 Log.e(TAG, String.format(
1372 "Size is not the same. Expected: %d, Written: %d. Retries: %d",
1373 expected, fso == null ? -1 : fso.getSize(), i));
1374 if (i == (WRITE_RETRIES-1)) {
1375 // Something was wrong. The destination data is not the same
1376 // as the source data
1377 DialogHelper.showToast(
1378 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1386 // Success. The file was saved
1387 DialogHelper.showToast(
1388 this, R.string.editor_successfully_saved, Toast.LENGTH_SHORT);
1391 // Send a message that allow other activities to update his data
1392 Intent intent = new Intent(FileManagerSettings.INTENT_FILE_CHANGED);
1394 FileManagerSettings.EXTRA_FILE_CHANGED_KEY, this.mFso.getFullPath());
1395 sendBroadcast(intent);
1401 } catch (Exception ex) {
1402 // Something was wrong, but the file was NOT written
1403 Log.e(TAG, "The file wasn't written.", ex);
1404 DialogHelper.showToast(
1405 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1410 * Method that write the file.
1412 * @param writer The command listener
1413 * @param bytes The bytes to write
1414 * @throws Exception If something was wrong
1416 private void syncWrite(AsyncWriter writer, byte[] bytes) throws Exception {
1417 // Create the writable command
1418 WriteExecutable cmd =
1419 CommandHelper.write(this, this.mFso.getFullPath(), writer, null);
1421 // Obtain access to the buffer (IMP! don't close the buffer here, it's manage
1423 OutputStream os = cmd.createOutputStream();
1425 // Retrieve the text from the editor
1426 ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
1429 byte[] data = new byte[this.mBufferSize];
1430 int read = 0, written = 0;
1431 while ((read = bais.read(data, 0, this.mBufferSize)) != -1) {
1432 os.write(data, 0, read);
1435 Log.i(TAG, "Bytes written: " + written); //$NON-NLS-1$
1439 } catch (Exception e) {/**NON BLOCK**/}
1443 // Ok. Data is written or ensure buffer close
1452 public void beforeTextChanged(
1453 CharSequence s, int start, int count, int after) {/**NON BLOCK**/}
1459 public void onTextChanged(CharSequence s, int start, int before, int count) {
1460 this.mEditStart = start;
1461 this.mEditEnd = start + count;
1468 public void afterTextChanged(Editable s) {
1470 if (this.mSyntaxHighlightProcessor != null) {
1471 this.mSyntaxHighlightProcessor.process(s, this.mEditStart, this.mEditEnd);
1476 * Method that sets if the editor is dirty (has changed)
1478 * @param dirty If the editor is dirty
1481 void setDirty(boolean dirty) {
1482 this.mDirty = dirty;
1483 this.mSave.setVisibility(dirty ? View.VISIBLE : View.GONE);
1487 * Check the dirty state of the editor, and ask the user to save the changes
1490 public void checkDirtyState() {
1492 AlertDialog dlg = DialogHelper.createYesNoDialog(
1494 R.string.editor_dirty_ask_title,
1495 R.string.editor_dirty_ask_msg,
1496 new OnClickListener() {
1498 public void onClick(DialogInterface dialog, int which) {
1499 if (which == DialogInterface.BUTTON_POSITIVE) {
1501 setResult(Activity.RESULT_OK);
1506 DialogHelper.delegateDialogShow(this, dlg);
1509 setResult(Activity.RESULT_OK);
1514 * Method that applies the current theme to the activity
1518 Theme theme = ThemeManager.getCurrentTheme(this);
1519 theme.setBaseTheme(this, false);
1522 theme.setTitlebarDrawable(this, getActionBar(), "titlebar_drawable"); //$NON-NLS-1$
1523 View v = getActionBar().getCustomView().findViewById(R.id.customtitle_title);
1524 theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$
1525 v = findViewById(R.id.ab_button1);
1526 theme.setImageDrawable(this, (ImageView)v, "ab_save_drawable"); //$NON-NLS-1$
1527 v = findViewById(R.id.ab_button2);
1528 theme.setImageDrawable(this, (ImageView)v, "ab_overflow_drawable"); //$NON-NLS-1$
1530 v = findViewById(R.id.editor_layout);
1531 theme.setBackgroundDrawable(this, v, "background_drawable"); //$NON-NLS-1$
1532 v = findViewById(R.id.editor);
1533 theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$
1535 Drawable dw = theme.getDrawable(this, "horizontal_progress_bar"); //$NON-NLS-1$
1536 this.mProgressBar.setProgressDrawable(dw);
1537 v = findViewById(R.id.editor_progress_msg);
1538 theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$
1540 // Need a full process of syntax highlight
1541 if (!this.mBinary && this.mSyntaxHighlight && this.mSyntaxHighlightProcessor != null) {
1542 reloadSyntaxHighlight();
1547 * Method that resolves the content uri to a valid system path
1549 * @param ctx The current context
1550 * @param uri The content uri
1551 * @return String The system path
1553 private static String uriToPath(Context ctx, Uri uri) {
1554 File file = MediaHelper.contentUriToFile(ctx.getContentResolver(), uri);
1556 file = new File(uri.getPath());
1558 return file.getAbsolutePath();