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.commands.shell.InvalidCommandDefinitionException;
72 import com.cyanogenmod.filemanager.console.Console;
73 import com.cyanogenmod.filemanager.console.ConsoleAllocException;
74 import com.cyanogenmod.filemanager.console.ConsoleBuilder;
75 import com.cyanogenmod.filemanager.console.InsufficientPermissionsException;
76 import com.cyanogenmod.filemanager.console.java.JavaConsole;
77 import com.cyanogenmod.filemanager.model.FileSystemObject;
78 import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
79 import com.cyanogenmod.filemanager.preferences.Preferences;
80 import com.cyanogenmod.filemanager.ui.ThemeManager;
81 import com.cyanogenmod.filemanager.ui.ThemeManager.Theme;
82 import com.cyanogenmod.filemanager.ui.policy.PrintActionPolicy;
83 import com.cyanogenmod.filemanager.ui.widgets.ButtonItem;
84 import com.cyanogenmod.filemanager.util.AndroidHelper;
85 import com.cyanogenmod.filemanager.util.CommandHelper;
86 import com.cyanogenmod.filemanager.util.DialogHelper;
87 import com.cyanogenmod.filemanager.util.ExceptionUtil;
88 import com.cyanogenmod.filemanager.util.ExceptionUtil.OnRelaunchCommandResult;
89 import com.cyanogenmod.filemanager.util.FileHelper;
90 import com.cyanogenmod.filemanager.util.MediaHelper;
91 import com.cyanogenmod.filemanager.util.ResourcesHelper;
92 import com.cyanogenmod.filemanager.util.StringHelper;
93 import org.mozilla.universalchardet.UniversalDetector;
95 import java.io.BufferedReader;
96 import java.io.ByteArrayInputStream;
97 import java.io.ByteArrayOutputStream;
99 import java.io.IOException;
100 import java.io.InputStream;
101 import java.io.OutputStream;
102 import java.io.StringReader;
103 import java.util.ArrayList;
104 import java.util.Arrays;
105 import java.util.List;
106 import java.util.UUID;
109 * An internal activity for view and edit files.
111 public class EditorActivity extends Activity implements TextWatcher {
113 private static final String TAG = "EditorActivity"; //$NON-NLS-1$
115 private static boolean DEBUG = false;
117 private static final int WRITE_RETRIES = 3;
119 private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() {
121 public void onReceive(Context context, Intent intent) {
122 if (intent != null) {
123 if (intent.getAction().compareTo(FileManagerSettings.INTENT_THEME_CHANGED) == 0) {
127 if (intent.getAction().compareTo(FileManagerSettings.INTENT_SETTING_CHANGED) == 0) {
128 // The settings has changed
129 String key = intent.getStringExtra(FileManagerSettings.EXTRA_SETTING_CHANGED_KEY);
131 final EditorActivity activity = EditorActivity.this;
134 if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId()) == 0) {
135 // Ignore in binary files
136 if (activity.mBinary) return;
138 // Do we have a different setting?
139 boolean noSuggestionsSetting =
140 Preferences.getSharedPreferences().getBoolean(
141 FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(),
142 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.
143 getDefaultValue()).booleanValue());
144 if (noSuggestionsSetting != activity.mNoSuggestions) {
145 activity.mHandler.post(new Runnable() {
148 toggleNoSuggestions();
154 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId()) == 0) {
155 // Ignore in binary files
156 if (activity.mBinary) return;
158 // Do we have a different setting?
159 boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean(
160 FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(),
161 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.
162 getDefaultValue()).booleanValue());
163 if (wordWrapSetting != activity.mWordWrap) {
164 activity.mHandler.post(new Runnable() {
173 // Default theme color scheme
175 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId()) == 0) {
176 // Ignore in binary files
177 if (activity.mBinary) return;
179 // Do we have a different setting?
180 boolean syntaxHighlightSetting =
181 Preferences.getSharedPreferences().getBoolean(
182 FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(),
183 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.
184 getDefaultValue()).booleanValue());
185 if (syntaxHighlightSetting != activity.mSyntaxHighlight) {
186 activity.mHandler.post(new Runnable() {
189 toggleSyntaxHighlight();
194 } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId()) == 0 ) {
195 // Ignore in binary files
196 if (activity.mBinary) return;
198 // Reload the syntax highlight
199 activity.mHandler.post(new Runnable() {
202 reloadSyntaxHighlight();
213 private class HexDumpAdapter extends ArrayAdapter<String> {
214 private class ViewHolder {
218 public HexDumpAdapter(Context context, List<String> data) {
219 super(context, R.layout.hexdump_line, data);
223 public View getView(int position, View convertView, ViewGroup parent) {
224 View v = convertView;
226 final Context context = getContext();
227 LayoutInflater inflater = LayoutInflater.from(context);
228 Theme theme = ThemeManager.getCurrentTheme(context);
230 v = inflater.inflate(R.layout.hexdump_line, parent, false);
231 ViewHolder viewHolder = new EditorActivity.HexDumpAdapter.ViewHolder();
232 viewHolder.mTextView = (TextView)v.findViewById(android.R.id.text1);
234 viewHolder.mTextView.setTextAppearance(context, R.style.hexeditor_text_appearance);
235 viewHolder.mTextView.setTypeface(mHexTypeface);
236 theme.setTextColor(context, viewHolder.mTextView, "text_color"); //$NON-NLS-1$
238 v.setTag(viewHolder);
241 String text = getItem(position);
242 ViewHolder viewHolder = (ViewHolder)v.getTag();
243 viewHolder.mTextView.setText(text);
249 * Return the view as a document
251 * @return StringBuilder a buffer to the document
253 public StringBuilder toStringDocument() {
254 StringBuilder sb = new StringBuilder();
256 for (int i = 0; i < c; i++) {
257 sb.append(getItem(i));
265 * Internal interface to notify progress update
267 private interface OnProgressListener {
268 void onProgress(int progress);
272 * An internal listener for read a file
274 private class AsyncReader implements AsyncResultListener {
276 final Object mSync = new Object();
277 ByteArrayOutputStream mByteBuffer = null;
278 ArrayList<String> mBinaryBuffer = null;
279 SpannableStringBuilder mBuffer = null;
282 FileSystemObject mReadFso;
283 OnProgressListener mListener;
284 boolean mDetectEncoding = false;
285 UniversalDetector mDetector;
286 String mDetectedEncoding;
289 * Constructor of <code>AsyncReader</code>. For enclosing access.
291 public AsyncReader(boolean detectEncoding) {
293 mDetectEncoding = detectEncoding;
294 if (mDetectEncoding) {
295 mDetector = new UniversalDetector(null);
303 public void onAsyncStart() {
304 this.mByteBuffer = new ByteArrayOutputStream((int)this.mReadFso.getSize());
312 public void onAsyncEnd(boolean cancelled) {
313 if (!cancelled && StringHelper.isBinaryData(mByteBuffer.toByteArray())) {
314 EditorActivity.this.mBinary = true;
315 EditorActivity.this.mReadOnly = true;
316 } else if (mDetector != null) {
318 mDetectedEncoding = mDetector.getDetectedCharset();
326 public void onAsyncExitCode(int exitCode) {
327 synchronized (this.mSync) {
336 public void onPartialResult(Object result) {
338 if (result == null) return;
339 byte[] partial = (byte[]) result;
340 if (mDetectEncoding) {
341 mDetector.handleData(partial, 0, partial.length);
343 this.mByteBuffer.write(partial, 0, partial.length);
344 this.mSize += partial.length;
345 if (this.mListener != null && this.mReadFso != null) {
347 if (this.mReadFso.getSize() != 0) {
348 progress = (int)((this.mSize*100) / this.mReadFso.getSize());
350 this.mListener.onProgress(progress);
352 } catch (Exception e) {
361 public void onException(Exception cause) {
367 * An internal listener for write a file
369 private class AsyncWriter implements AsyncResultListener {
374 * Constructor of <code>AsyncWriter</code>. For enclosing access.
376 public AsyncWriter() {
384 public void onAsyncStart() {/**NON BLOCK**/}
390 public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/}
396 public void onAsyncExitCode(int exitCode) {/**NON BLOCK**/}
402 public void onPartialResult(Object result) {/**NON BLOCK**/}
408 public void onException(Exception cause) {
414 * An internal class to resolve resources for the syntax highlight library.
417 class ResourcesResolver implements ISyntaxHighlightResourcesResolver {
419 public CharSequence getString(String id, String resid) {
420 return EditorActivity.this.getString(
421 ResourcesHelper.getIdentifier(
422 EditorActivity.this.getResources(), "string", resid)); //$NON-NLS-1$
426 public int getInteger(String id, String resid, int def) {
427 return EditorActivity.this.getResources().
429 ResourcesHelper.getIdentifier(
430 EditorActivity.this.getResources(), "integer", resid)); //$NON-NLS-1$
434 public int getColor(String id, String resid, int def) {
435 final Context ctx = EditorActivity.this;
437 // Use the user-defined settings
438 int[] colors = getUserColorScheme();
439 HighlightColors[] schemeColors = HighlightColors.values();
440 int cc = schemeColors.length;
441 int cc2 = colors.length;
442 for (int i = 0; i < cc; i++) {
443 if (schemeColors[i].getId().compareTo(id) == 0) {
450 return ThemeManager.getCurrentTheme(ctx).getColor(ctx, resid);
455 } catch (Exception ex) {
456 // Resource not found
462 * Method that returns the user-defined color scheme
464 * @return int[] The user-defined color scheme
466 private int[] getUserColorScheme() {
467 String defaultValue =
468 (String)FileManagerSettings.
469 SETTINGS_EDITOR_SH_COLOR_SCHEME.getDefaultValue();
470 String value = Preferences.getSharedPreferences().getString(
471 FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId(),
473 return EditorSHColorSchemePreferenceFragment.toColorShemeArray(value);
480 FileSystemObject mFso;
482 private int mBufferSize;
483 private long mMaxFileSize;
509 ListView mBinaryEditor;
517 ProgressBar mProgressBar;
521 TextView mProgressBarMsg;
531 // No suggestions status
535 boolean mNoSuggestions;
538 private ViewGroup mWordWrapView;
539 private ViewGroup mNoWordWrapView;
545 // Syntax highlight status
549 boolean mSyntaxHighlight;
553 SyntaxHighlightProcessor mSyntaxHighlightProcessor;
554 private int mEditStart;
555 private int mEditEnd;
557 private View mOptionsAnchorView;
559 private Typeface mHexTypeface;
561 private final Object mExecSync = new Object();
571 String mHexLineSeparator;
573 private boolean mHexDump;
576 * Intent extra parameter for the path of the file to open.
578 public static final String EXTRA_OPEN_FILE = "extra_open_file"; //$NON-NLS-1$
584 protected void onCreate(Bundle state) {
586 Log.d(TAG, "EditorActivity.onCreate"); //$NON-NLS-1$
589 this.mHandler = new Handler();
591 // Load typeface for hex editor
592 mHexTypeface = Typeface.createFromAsset(getAssets(), "fonts/Courier-Prime.ttf");
594 // Save hexdump user preference
595 mHexDump = Preferences.getSharedPreferences().getBoolean(
596 FileManagerSettings.SETTINGS_EDITOR_HEXDUMP.getId(),
597 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_HEXDUMP.
598 getDefaultValue()).booleanValue());
600 // Register the broadcast receiver
601 IntentFilter filter = new IntentFilter();
602 filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED);
603 filter.addAction(FileManagerSettings.INTENT_SETTING_CHANGED);
604 registerReceiver(this.mNotificationReceiver, filter);
606 // Generate a random separator
607 this.mHexLineSeparator = UUID.randomUUID().toString() + UUID.randomUUID().toString();
609 // Set the theme before setContentView
610 Theme theme = ThemeManager.getCurrentTheme(this);
611 theme.setBaseTheme(this, false);
613 //Set the main layout of the activity
614 setContentView(R.layout.editor);
616 // Get the limit vars
617 this.mBufferSize = getResources().getInteger(R.integer.buffer_size);
618 long availMem = AndroidHelper.getAvailableMemory(this);
619 this.mMaxFileSize = Math.min(availMem,
620 getResources().getInteger(R.integer.editor_max_file_size));
623 initTitleActionBar();
629 // Initialize the console
636 super.onCreate(state);
643 protected void onDestroy() {
645 Log.d(TAG, "EditorActivity.onDestroy"); //$NON-NLS-1$
648 // Unregister the receiver
650 unregisterReceiver(this.mNotificationReceiver);
651 } catch (Throwable ex) {
655 //All destroy. Continue
663 public void onConfigurationChanged(Configuration newConfig) {
664 super.onConfigurationChanged(newConfig);
668 * Method that initializes the titlebar of the activity.
670 private void initTitleActionBar() {
671 //Configure the action bar options
672 getActionBar().setBackgroundDrawable(
673 getResources().getDrawable(R.drawable.bg_material_titlebar));
674 getActionBar().setDisplayOptions(
675 ActionBar.DISPLAY_SHOW_CUSTOM);
676 getActionBar().setDisplayHomeAsUpEnabled(true);
677 View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false);
678 this.mTitle = (TextView)customTitle.findViewById(R.id.customtitle_title);
679 this.mTitle.setText(R.string.editor);
680 this.mTitle.setContentDescription(getString(R.string.editor));
682 this.mSave = (ButtonItem)customTitle.findViewById(R.id.ab_button0);
683 this.mSave.setImageResource(R.drawable.ic_material_light_save);
684 this.mSave.setContentDescription(getString(R.string.actionbar_button_save_cd));
685 this.mSave.setVisibility(View.GONE);
687 this.mPrint = (ButtonItem)customTitle.findViewById(R.id.ab_button1);
688 this.mPrint.setImageResource(R.drawable.ic_material_light_print);
689 this.mPrint.setContentDescription(getString(R.string.actionbar_button_print_cd));
690 this.mPrint.setVisibility(View.VISIBLE);
692 ButtonItem configuration = (ButtonItem)customTitle.findViewById(R.id.ab_button2);
693 configuration.setImageResource(R.drawable.ic_material_light_overflow);
694 configuration.setContentDescription(getString(R.string.actionbar_button_overflow_cd));
696 View status = findViewById(R.id.editor_status);
697 boolean showOptionsMenu = AndroidHelper.showOptionsMenu(this);
698 configuration.setVisibility(showOptionsMenu ? View.VISIBLE : View.GONE);
699 this.mOptionsAnchorView = showOptionsMenu ? configuration : status;
701 getActionBar().setCustomView(customTitle);
705 * Method that initializes the layout and components of the activity.
707 private void initLayout() {
708 this.mEditor = (EditText)findViewById(R.id.editor);
709 this.mEditor.setText(null);
710 this.mEditor.addTextChangedListener(this);
711 this.mEditor.setEnabled(false);
712 this.mWordWrapView = (ViewGroup)findViewById(R.id.editor_word_wrap_view);
713 this.mNoWordWrapView = (ViewGroup)findViewById(R.id.editor_no_word_wrap_view);
714 this.mWordWrapView.setVisibility(View.VISIBLE);
715 this.mNoWordWrapView.setVisibility(View.GONE);
717 this.mBinaryEditor = (ListView)findViewById(R.id.editor_binary);
719 this.mNoSuggestions = false;
720 this.mWordWrap = true;
721 this.mSyntaxHighlight = true;
723 // Load the no suggestions setting
724 boolean noSuggestionsSetting = Preferences.getSharedPreferences().getBoolean(
725 FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(),
726 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.
727 getDefaultValue()).booleanValue());
728 if (noSuggestionsSetting != this.mNoSuggestions) {
729 toggleNoSuggestions();
732 // Load the word wrap setting
733 boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean(
734 FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(),
735 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.
736 getDefaultValue()).booleanValue());
737 if (wordWrapSetting != this.mWordWrap) {
741 // Load the syntax highlight setting
742 boolean syntaxHighlighSetting = Preferences.getSharedPreferences().getBoolean(
743 FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(),
744 ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.
745 getDefaultValue()).booleanValue());
746 if (syntaxHighlighSetting != this.mSyntaxHighlight) {
747 toggleSyntaxHighlight();
750 this.mProgress = findViewById(R.id.editor_progress);
751 this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar);
752 this.mProgressBarMsg = (TextView)findViewById(R.id.editor_progress_msg);
756 * Method that toggle the no suggestions property of the editor
759 /**package**/ void toggleNoSuggestions() {
760 synchronized (this.mExecSync) {
761 int type = InputType.TYPE_CLASS_TEXT |
762 InputType.TYPE_TEXT_FLAG_MULTI_LINE |
763 InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
764 if (!this.mNoSuggestions) {
765 type |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
767 this.mEditor.setInputType(type);
768 this.mNoSuggestions = !this.mNoSuggestions;
773 * Method that toggle the word wrap property of the editor
776 /**package**/ void toggleWordWrap() {
777 synchronized (this.mExecSync) {
778 ViewGroup vSrc = this.mWordWrap ? this.mWordWrapView : this.mNoWordWrapView;
779 ViewGroup vDst = this.mWordWrap ? this.mNoWordWrapView : this.mWordWrapView;
780 ViewGroup vSrcParent = this.mWordWrap
782 : (ViewGroup)this.mNoWordWrapView.getChildAt(0);
783 ViewGroup vDstParent = this.mWordWrap
784 ? (ViewGroup)this.mNoWordWrapView.getChildAt(0)
785 : this.mWordWrapView;
786 vSrc.setVisibility(View.GONE);
787 vSrcParent.removeView(this.mEditor);
788 vDstParent.addView(this.mEditor);
789 vDst.setVisibility(View.VISIBLE);
791 this.mWordWrap = !this.mWordWrap;
796 * Method that toggles the syntax highlight property of the editor
799 /**package**/ void toggleSyntaxHighlight() {
800 synchronized (this.mExecSync) {
801 if (this.mSyntaxHighlightProcessor != null) {
803 if (this.mSyntaxHighlight) {
804 this.mSyntaxHighlightProcessor.clear(this.mEditor.getText());
806 this.mSyntaxHighlightProcessor.process(this.mEditor.getText());
808 } catch (Exception ex) {
809 // An error in a syntax library, should not break down app.
810 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
814 this.mSyntaxHighlight = !this.mSyntaxHighlight;
819 * Method that reloads the syntax highlight of the current file
822 /**package**/ void reloadSyntaxHighlight() {
823 synchronized (this.mExecSync) {
824 if (this.mSyntaxHighlightProcessor != null) {
826 this.mSyntaxHighlightProcessor.initialize();
827 this.mSyntaxHighlightProcessor.process(this.mEditor.getText());
828 } catch (Exception ex) {
829 // An error in a syntax library, should not break down app.
830 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
840 public boolean onKeyUp(int keyCode, KeyEvent event) {
842 case KeyEvent.KEYCODE_MENU:
843 showOverflowPopUp(this.mOptionsAnchorView);
845 case KeyEvent.KEYCODE_BACK:
849 return super.onKeyUp(keyCode, event);
857 public boolean onOptionsItemSelected(MenuItem item) {
858 switch (item.getItemId()) {
859 case android.R.id.home:
860 if ((getActionBar().getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP)
861 == ActionBar.DISPLAY_HOME_AS_UP) {
866 return super.onOptionsItemSelected(item);
871 * Method that shows a popup with the activity main menu.
873 * @param anchor The anchor of the popup
875 private void showOverflowPopUp(View anchor) {
876 SimpleMenuListAdapter adapter =
877 new HighlightedSimpleMenuListAdapter(this, R.menu.editor, true);
878 MenuItem noSuggestions = adapter.getMenu().findItem(R.id.mnu_no_suggestions);
879 if (noSuggestions != null) {
881 adapter.getMenu().removeItem(R.id.mnu_no_suggestions);
883 noSuggestions.setChecked(this.mNoSuggestions);
886 MenuItem wordWrap = adapter.getMenu().findItem(R.id.mnu_word_wrap);
887 if (wordWrap != null) {
889 adapter.getMenu().removeItem(R.id.mnu_word_wrap);
891 wordWrap.setChecked(this.mWordWrap);
894 MenuItem syntaxHighlight = adapter.getMenu().findItem(R.id.mnu_syntax_highlight);
895 if (syntaxHighlight != null) {
897 adapter.getMenu().removeItem(R.id.mnu_syntax_highlight);
899 syntaxHighlight.setChecked(this.mSyntaxHighlight);
903 final ListPopupWindow popup =
904 DialogHelper.createListPopupWindow(this, adapter, anchor);
905 popup.setOnItemClickListener(new OnItemClickListener() {
907 public void onItemClick(
908 final AdapterView<?> parent, final View v,
909 final int position, final long id) {
910 final int itemId = (int)id;
912 case R.id.mnu_no_suggestions:
913 toggleNoSuggestions();
915 case R.id.mnu_word_wrap:
918 case R.id.mnu_syntax_highlight:
919 toggleSyntaxHighlight();
921 case R.id.mnu_settings:
923 Intent settings = new Intent(EditorActivity.this, SettingsPreferences.class);
925 PreferenceActivity.EXTRA_SHOW_FRAGMENT,
926 EditorPreferenceFragment.class.getName());
927 startActivity(settings);
937 * Method invoked when an action item is clicked.
939 * @param view The button pushed
941 public void onActionBarItemClick(View view) {
942 switch (view.getId()) {
943 case R.id.ab_button0:
948 case R.id.ab_button1:
950 StringBuilder sb = mBinary
951 ? ((HexDumpAdapter)mBinaryEditor.getAdapter()).toStringDocument()
952 : new StringBuilder(mEditor.getText().toString());
953 PrintActionPolicy.printStringDocument(this, mFso, sb);
956 case R.id.ab_button2:
957 // Show overflow menu
958 showOverflowPopUp(this.mOptionsAnchorView);
967 * Method that initializes a console
969 private boolean initializeConsole() {
971 ConsoleBuilder.getConsole(this);
972 // There is a console allocated. Use it.
974 } catch (Throwable _throw) {
975 // Capture the exception
976 ExceptionUtil.translateException(this, _throw, false, true);
982 * Method that reads the requested file
984 private void readFile() {
985 // For now editor is not dirty and editable.
987 this.mBinary = false;
989 // Check for a valid action
990 String action = getIntent().getAction();
991 if (action == null ||
992 (action.compareTo(Intent.ACTION_VIEW) != 0) &&
993 (action.compareTo(Intent.ACTION_EDIT) != 0)) {
994 DialogHelper.showToast(
995 this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
998 // This var should be set depending on ACTION_VIEW or ACTION_EDIT action, but for
999 // better compatibility, IntentsActionPolicy use always ACTION_VIEW, so we have
1000 // to ignore this check here
1001 this.mReadOnly = false;
1003 // Read the intent and check that is has a valid request
1004 Intent fileIntent = getIntent();
1005 if (fileIntent.getData().getScheme().equals("content")) {
1006 asyncReadContentURI(fileIntent.getData());
1008 // File Scheme URI's
1009 String path = uriToPath(this, getIntent().getData());
1010 if (path == null || path.length() == 0) {
1011 DialogHelper.showToast(
1012 this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
1016 // Set the title of the dialog
1017 File f = new File(path);
1018 this.mTitle.setText(f.getName());
1020 // Check that the file exists (the real file, not the symlink)
1022 this.mFso = CommandHelper.getFileInfo(this, path, true, null);
1023 if (this.mFso == null) {
1025 } catch (Exception e) {
1026 Log.e(TAG, "Failed to get file reference", e); //$NON-NLS-1$
1029 // Check that we can handle the length of the file (by device)
1030 if (this.mMaxFileSize < this.mFso.getSize()) {
1031 DialogHelper.showToast(
1032 this, R.string.editor_file_exceed_size_msg, Toast.LENGTH_SHORT);
1036 // Get the syntax highlight processor
1037 SyntaxHighlightFactory shpFactory =
1038 SyntaxHighlightFactory.getDefaultFactory(new ResourcesResolver());
1039 this.mSyntaxHighlightProcessor = shpFactory.getSyntaxHighlightProcessor(f);
1040 if (this.mSyntaxHighlightProcessor != null) {
1041 this.mSyntaxHighlightProcessor.initialize();
1044 // Check that we have read access
1046 FileHelper.ensureReadAccess(
1047 ConsoleBuilder.getConsole(this),
1051 // Read the file in background
1054 } catch (Exception ex) {
1055 ExceptionUtil.translateException(
1056 this, ex, false, true, new OnRelaunchCommandResult() {
1058 public void onSuccess() {
1059 // Read the file in background
1064 public void onFailed(Throwable cause) {
1069 public void onCancelled() {
1078 * Method that does the read of a content uri in the background
1081 void asyncReadContentURI(Uri uri) {
1082 // Do the load of the file
1083 AsyncTask<Uri, Integer, Boolean> mReadTask =
1084 new AsyncTask<Uri, Integer, Boolean>() {
1086 private Exception mCause;
1087 private boolean changeToBinaryMode;
1088 private boolean changeToDisplaying;
1089 private String tempText;
1092 protected void onPreExecute() {
1093 // Show the progress
1094 this.changeToBinaryMode = false;
1095 this.changeToDisplaying = false;
1096 doProgress(true, 0);
1100 protected Boolean doInBackground(Uri... params) {
1102 // Only one argument (the file to open)
1103 Uri fso = params[0];
1106 // Read the file in an async listener
1109 publishProgress(Integer.valueOf(0));
1111 InputStream is = getContentResolver().openInputStream(fso);
1113 ByteArrayOutputStream baos = new ByteArrayOutputStream();
1115 byte[] buffer = new byte[1024];
1117 while ((readBytes = is.read(buffer, 0, buffer.length)) != -1) {
1118 baos.write(buffer, 0, readBytes);
1120 Integer.valueOf((readBytes * 100) / buffer.length));
1122 } catch (IOException e) {
1123 e.printStackTrace();
1124 return Boolean.FALSE;
1128 publishProgress(new Integer(100));
1129 tempText = new String(baos.toByteArray(), "UTF-8");
1130 Log.i(TAG, "Bytes read: " + baos.toByteArray().length); //$NON-NLS-1$
1133 this.changeToDisplaying = true;
1134 publishProgress(new Integer(0));
1136 } catch (Exception e) {
1138 return Boolean.FALSE;
1141 return Boolean.TRUE;
1146 protected void onProgressUpdate(Integer... values) {
1148 doProgress(true, values[0].intValue());
1152 protected void onPostExecute(Boolean result) {
1153 final EditorActivity activity = EditorActivity.this;
1155 if (!result.booleanValue()) {
1156 if (this.mCause != null) {
1157 ExceptionUtil.translateException(activity, this.mCause);
1158 activity.mEditor.setEnabled(false);
1161 // Now we have the buffer, set the text of the editor
1162 activity.mEditor.setText(
1163 tempText, BufferType.EDITABLE);
1165 // Highlight editor text syntax
1166 if (activity.mSyntaxHighlight &&
1167 activity.mSyntaxHighlightProcessor != null) {
1169 activity.mSyntaxHighlightProcessor.process(
1170 activity.mEditor.getText());
1171 } catch (Exception ex) {
1172 // An error in a syntax library, should not break down app.
1173 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
1178 activity.mEditor.setEnabled(!activity.mReadOnly);
1180 // Notify read-only mode
1181 if (activity.mReadOnly) {
1182 DialogHelper.showToast(
1184 R.string.editor_read_only_mode,
1185 Toast.LENGTH_SHORT);
1189 doProgress(false, 0);
1193 protected void onCancelled() {
1194 // Hide the progress
1195 doProgress(false, 0);
1199 * Method that update the progress status
1201 * @param visible If the progress bar need to be hidden
1202 * @param progress The progress
1204 private void doProgress(boolean visible, int progress) {
1205 final EditorActivity activity = EditorActivity.this;
1207 // Show the progress bar
1208 activity.mProgressBar.setProgress(progress);
1209 activity.mProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
1211 if (this.changeToBinaryMode) {
1212 mWordWrapView.setVisibility(View.GONE);
1213 mNoWordWrapView.setVisibility(View.GONE);
1214 mBinaryEditor.setVisibility(View.VISIBLE);
1216 // Show hex dumping text
1217 activity.mProgressBarMsg.setText(R.string.dumping_message);
1218 this.changeToBinaryMode = false;
1220 else if (this.changeToDisplaying) {
1221 activity.mProgressBarMsg.setText(R.string.displaying_message);
1222 this.changeToDisplaying = false;
1226 mReadTask.execute(uri);
1230 * Method that does the read of the file in background
1234 // Do the load of the file
1235 AsyncTask<FileSystemObject, Integer, Boolean> mReadTask =
1236 new AsyncTask<FileSystemObject, Integer, Boolean>() {
1238 private Exception mCause;
1239 private AsyncReader mReader;
1240 private boolean changeToBinaryMode;
1241 private boolean changeToDisplaying;
1244 protected void onPreExecute() {
1245 // Show the progress
1246 this.changeToBinaryMode = false;
1247 this.changeToDisplaying = false;
1248 doProgress(true, 0);
1252 protected Boolean doInBackground(FileSystemObject... params) {
1253 final EditorActivity activity = EditorActivity.this;
1255 // Only one argument (the file to open)
1256 FileSystemObject fso = params[0];
1259 // Read the file in an async listener
1262 // Configure the reader
1263 this.mReader = new AsyncReader(true);
1264 this.mReader.mReadFso = fso;
1265 this.mReader.mListener = new OnProgressListener() {
1267 @SuppressWarnings("synthetic-access")
1268 public void onProgress(int progress) {
1269 publishProgress(Integer.valueOf(progress));
1273 // Execute the command (read the file)
1274 CommandHelper.read(activity, fso.getFullPath(), this.mReader,
1278 synchronized (this.mReader.mSync) {
1279 this.mReader.mSync.wait();
1283 publishProgress(new Integer(100));
1285 // Check if the read was successfully
1286 if (this.mReader.mCause != null) {
1287 this.mCause = this.mReader.mCause;
1288 return Boolean.FALSE;
1293 // Now we have the byte array with all the data. is a binary file?
1294 // Then dump them byte array to hex dump string (only if users settings
1296 if (activity.mBinary && mHexDump) {
1297 // we do not use the Hexdump helper class, because we need to show the
1298 // progress of the dump process
1299 final String data = toHexPrintableString(toHexDump(
1300 this.mReader.mByteBuffer.toByteArray()));
1301 this.mReader.mBinaryBuffer = new ArrayList<String>();
1302 BufferedReader reader = new BufferedReader(new StringReader(data));
1304 while ((line = reader.readLine()) != null) {
1305 this.mReader.mBinaryBuffer.add(line);
1307 Log.i(TAG, "Bytes read: " + data.length()); //$NON-NLS-1$
1310 if (this.mReader.mDetectedEncoding != null) {
1311 data = new String(this.mReader.mByteBuffer.toByteArray(),
1312 this.mReader.mDetectedEncoding);
1314 data = new String(this.mReader.mByteBuffer.toByteArray());
1316 this.mReader.mBuffer = new SpannableStringBuilder(data);
1317 Log.i(TAG, "Bytes read: " + data.getBytes().length); //$NON-NLS-1$
1319 this.mReader.mByteBuffer = null;
1322 this.changeToDisplaying = true;
1323 publishProgress(new Integer(0));
1325 } catch (Exception e) {
1327 return Boolean.FALSE;
1330 return Boolean.TRUE;
1334 protected void onProgressUpdate(Integer... values) {
1336 doProgress(true, values[0].intValue());
1340 protected void onPostExecute(Boolean result) {
1341 final EditorActivity activity = EditorActivity.this;
1343 if (!result.booleanValue()) {
1344 if (this.mCause != null) {
1345 ExceptionUtil.translateException(activity, this.mCause);
1346 activity.mEditor.setEnabled(false);
1349 // Now we have the buffer, set the text of the editor
1350 if (activity.mBinary && mHexDump) {
1351 HexDumpAdapter adapter = new HexDumpAdapter(EditorActivity.this,
1352 this.mReader.mBinaryBuffer);
1353 mBinaryEditor.setAdapter(adapter);
1356 this.mReader.mBinaryBuffer = null;
1358 activity.mEditor.setText(
1359 this.mReader.mBuffer, BufferType.EDITABLE);
1361 // Highlight editor text syntax
1362 if (activity.mSyntaxHighlight &&
1363 activity.mSyntaxHighlightProcessor != null) {
1365 activity.mSyntaxHighlightProcessor.process(
1366 activity.mEditor.getText());
1367 } catch (Exception ex) {
1368 // An error in a syntax library, should not break down app.
1369 Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$
1374 this.mReader.mBuffer = null;
1378 activity.mEditor.setEnabled(!activity.mReadOnly);
1380 // Notify read-only mode
1381 if (activity.mReadOnly) {
1382 DialogHelper.showToast(
1384 R.string.editor_read_only_mode,
1385 Toast.LENGTH_SHORT);
1389 doProgress(false, 0);
1393 protected void onCancelled() {
1394 // Hide the progress
1395 doProgress(false, 0);
1399 * Method that update the progress status
1401 * @param visible If the progress bar need to be hidden
1402 * @param progress The progress
1404 private void doProgress(boolean visible, int progress) {
1405 final EditorActivity activity = EditorActivity.this;
1407 // Show the progress bar
1408 activity.mProgressBar.setProgress(progress);
1409 activity.mProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
1411 if (this.changeToBinaryMode) {
1412 mWordWrapView.setVisibility(View.GONE);
1413 mNoWordWrapView.setVisibility(View.GONE);
1414 mBinaryEditor.setVisibility(View.VISIBLE);
1416 // Show hex dumping text
1417 activity.mProgressBarMsg.setText(R.string.dumping_message);
1418 this.changeToBinaryMode = false;
1420 else if (this.changeToDisplaying) {
1421 activity.mProgressBarMsg.setText(R.string.displaying_message);
1422 this.changeToDisplaying = false;
1427 * Create a hex dump of the data while show progress to user
1429 * @param data The data to hex dump
1430 * @return StringBuilder The hex dump buffer
1432 private String toHexDump(byte[] data) {
1433 //Change to binary mode
1434 this.changeToBinaryMode = true;
1437 publishProgress(Integer.valueOf(0));
1439 // Calculate max dir size
1440 int length = data.length;
1442 final int DISPLAY_SIZE = 16; // Bytes per line
1443 ByteArrayInputStream bais = new ByteArrayInputStream(data);
1444 byte[] line = new byte[DISPLAY_SIZE];
1447 StringBuilder sb = new StringBuilder();
1448 while ((read = bais.read(line, 0, DISPLAY_SIZE)) != -1) {
1449 //offset dump(16) data\n
1450 String linedata = new String(line, 0, read);
1451 sb.append(HexDump.toHexString(offset));
1452 sb.append(" "); //$NON-NLS-1$
1453 String hexDump = HexDump.toHexString(line, 0, read);
1454 if (hexDump.length() != (DISPLAY_SIZE * 2)) {
1455 char[] array = new char[(DISPLAY_SIZE * 2) - hexDump.length()];
1456 Arrays.fill(array, ' ');
1457 hexDump += new String(array);
1460 sb.append(" "); //$NON-NLS-1$
1461 sb.append(linedata);
1462 sb.append(EditorActivity.this.mHexLineSeparator);
1463 offset += DISPLAY_SIZE;
1464 if (offset % 5 == 0) {
1465 publishProgress(Integer.valueOf((offset * 100) / length));
1469 // End of the dump process
1470 publishProgress(Integer.valueOf(100));
1472 return sb.toString();
1476 * Method that converts to a visual printable hex string
1478 * @param string The string to check
1480 private String toHexPrintableString(String string) {
1481 // Remove characters without visual representation
1482 final String REPLACED_SYMBOL = "."; //$NON-NLS-1$
1483 final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$
1484 String printable = string.replaceAll("\\p{Cntrl}", REPLACED_SYMBOL); //$NON-NLS-1$
1485 printable = printable.replaceAll("[^\\p{Print}]", REPLACED_SYMBOL); //$NON-NLS-1$
1486 printable = printable.replaceAll("\\p{C}", REPLACED_SYMBOL); //$NON-NLS-1$
1487 printable = printable.replaceAll(EditorActivity.this.mHexLineSeparator, NEWLINE);
1491 mReadTask.execute(this.mFso);
1494 private void checkAndWrite() {
1495 // Check that we have write access
1497 FileHelper.ensureWriteAccess(
1498 ConsoleBuilder.getConsole(this),
1505 } catch (Exception ex) {
1506 ExceptionUtil.translateException(
1507 this, ex, false, true, new OnRelaunchCommandResult() {
1509 public void onSuccess() {
1515 public void onFailed(Throwable cause) {/**NON BLOCK**/}
1518 public void onCancelled() {/**NON BLOCK**/}
1524 * Method that checks that the write to disk operation was successfully and the
1525 * expected bytes are written to disk.
1528 void ensureSyncWrite() {
1530 for (int i = 0; i < WRITE_RETRIES; i++) {
1531 // Configure the writer
1532 AsyncWriter writer = new AsyncWriter();
1535 final byte[] data = this.mEditor.getText().toString().getBytes();
1536 long expected = data.length;
1537 syncWrite(writer, data);
1543 if (writer.mCause != null) {
1544 Log.e(TAG, "Write operation failed. Retries: " + i, writer.mCause);
1545 if (i == (WRITE_RETRIES-1)) {
1546 // Something was wrong. The file probably is corrupted
1547 DialogHelper.showToast(
1548 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1556 // Check that all the bytes were written
1557 FileSystemObject fso =
1558 CommandHelper.getFileInfo(this, this.mFso.getFullPath(), true, null);
1559 if (fso == null || fso.getSize() != expected) {
1560 Log.e(TAG, String.format(
1561 "Size is not the same. Expected: %d, Written: %d. Retries: %d",
1562 expected, fso == null ? -1 : fso.getSize(), i));
1563 if (i == (WRITE_RETRIES-1)) {
1564 // Something was wrong. The destination data is not the same
1565 // as the source data
1566 DialogHelper.showToast(
1567 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1575 // Success. The file was saved
1576 DialogHelper.showToast(
1577 this, R.string.editor_successfully_saved, Toast.LENGTH_SHORT);
1580 // Send a message that allow other activities to update his data
1581 Intent intent = new Intent(FileManagerSettings.INTENT_FILE_CHANGED);
1583 FileManagerSettings.EXTRA_FILE_CHANGED_KEY, this.mFso.getFullPath());
1584 sendBroadcast(intent);
1590 } catch (Exception ex) {
1591 // Something was wrong, but the file was NOT written
1592 Log.e(TAG, "The file wasn't written.", ex);
1593 DialogHelper.showToast(
1594 this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT);
1599 * Method that write the file.
1601 * @param writer The command listener
1602 * @param bytes The bytes to write
1603 * @throws Exception If something was wrong
1605 private void syncWrite(AsyncWriter writer, byte[] bytes) throws Exception {
1606 // Create the writable command
1607 WriteExecutable cmd =
1608 CommandHelper.write(this, this.mFso.getFullPath(), writer, null);
1610 // Obtain access to the buffer (IMP! don't close the buffer here, it's manage
1612 OutputStream os = cmd.createOutputStream();
1614 // Retrieve the text from the editor
1615 ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
1618 byte[] data = new byte[this.mBufferSize];
1619 int read = 0, written = 0;
1620 while ((read = bais.read(data, 0, this.mBufferSize)) != -1) {
1621 os.write(data, 0, read);
1624 Log.i(TAG, "Bytes written: " + written); //$NON-NLS-1$
1628 } catch (Exception e) {/**NON BLOCK**/}
1632 // Ok. Data is written or ensure buffer close
1641 public void beforeTextChanged(
1642 CharSequence s, int start, int count, int after) {/**NON BLOCK**/}
1648 public void onTextChanged(CharSequence s, int start, int before, int count) {
1649 this.mEditStart = start;
1650 this.mEditEnd = start + count;
1657 public void afterTextChanged(Editable s) {
1659 if (this.mSyntaxHighlightProcessor != null) {
1660 this.mSyntaxHighlightProcessor.process(s, this.mEditStart, this.mEditEnd);
1665 * Method that sets if the editor is dirty (has changed)
1667 * @param dirty If the editor is dirty
1670 void setDirty(boolean dirty) {
1671 this.mDirty = dirty;
1672 this.mSave.setVisibility(dirty ? View.VISIBLE : View.GONE);
1676 * Check the dirty state of the editor, and ask the user to save the changes
1679 public void checkDirtyState() {
1681 AlertDialog dlg = DialogHelper.createYesNoDialog(
1683 R.string.editor_dirty_ask_title,
1684 R.string.editor_dirty_ask_msg,
1685 new OnClickListener() {
1687 public void onClick(DialogInterface dialog, int which) {
1688 if (which == DialogInterface.BUTTON_POSITIVE) {
1690 setResult(Activity.RESULT_OK);
1695 DialogHelper.delegateDialogShow(this, dlg);
1698 setResult(Activity.RESULT_OK);
1703 * Method that applies the current theme to the activity
1707 Theme theme = ThemeManager.getCurrentTheme(this);
1708 theme.setBaseTheme(this, false);
1711 theme.setTitlebarDrawable(this, getActionBar(), "titlebar_drawable"); //$NON-NLS-1$
1712 View v = getActionBar().getCustomView().findViewById(R.id.customtitle_title);
1713 theme.setTextColor(this, (TextView)v, "action_bar_text_color"); //$NON-NLS-1$
1714 v = findViewById(R.id.ab_button0);
1715 theme.setImageDrawable(this, (ImageView)v, "ab_save_drawable"); //$NON-NLS-1$
1716 v = findViewById(R.id.ab_button1);
1717 theme.setImageDrawable(this, (ImageView)v, "ab_print_drawable"); //$NON-NLS-1$
1718 v = findViewById(R.id.ab_button2);
1719 theme.setImageDrawable(this, (ImageView)v, "ab_overflow_drawable"); //$NON-NLS-1$
1721 v = findViewById(R.id.editor_layout);
1722 theme.setBackgroundDrawable(this, v, "background_drawable"); //$NON-NLS-1$
1723 v = findViewById(R.id.editor);
1724 theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$
1726 Drawable dw = theme.getDrawable(this, "horizontal_progress_bar"); //$NON-NLS-1$
1727 this.mProgressBar.setProgressDrawable(dw);
1728 v = findViewById(R.id.editor_progress_msg);
1729 theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$
1731 // Need a full process of syntax highlight
1732 if (!this.mBinary && this.mSyntaxHighlight && this.mSyntaxHighlightProcessor != null) {
1733 reloadSyntaxHighlight();
1738 * Method that resolves the content uri to a valid system path
1740 * @param ctx The current context
1741 * @param uri The content uri
1742 * @return String The system path
1744 private static String uriToPath(Context ctx, Uri uri) {
1745 File file = MediaHelper.contentUriToFile(ctx.getContentResolver(), uri);
1747 file = new File(uri.getPath());
1749 return file.getAbsolutePath();