/* * Copyright (C) 2012 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.cyanogenmod.filemanager.activities; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceActivity; import android.text.Editable; import android.text.InputType; import android.text.SpannableStringBuilder; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListPopupWindow; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.TextView.BufferType; import android.widget.Toast; import com.android.internal.util.HexDump; import com.cyanogenmod.filemanager.R; import com.cyanogenmod.filemanager.activities.preferences.EditorPreferenceFragment; import com.cyanogenmod.filemanager.activities.preferences.EditorSHColorSchemePreferenceFragment; import com.cyanogenmod.filemanager.activities.preferences.SettingsPreferences; import com.cyanogenmod.filemanager.adapters.HighlightedSimpleMenuListAdapter; import com.cyanogenmod.filemanager.adapters.SimpleMenuListAdapter; import com.cyanogenmod.filemanager.ash.HighlightColors; import com.cyanogenmod.filemanager.ash.ISyntaxHighlightResourcesResolver; import com.cyanogenmod.filemanager.ash.SyntaxHighlightFactory; import com.cyanogenmod.filemanager.ash.SyntaxHighlightProcessor; import com.cyanogenmod.filemanager.commands.AsyncResultListener; import com.cyanogenmod.filemanager.commands.WriteExecutable; import com.cyanogenmod.filemanager.commands.shell.InvalidCommandDefinitionException; import com.cyanogenmod.filemanager.console.Console; import com.cyanogenmod.filemanager.console.ConsoleAllocException; import com.cyanogenmod.filemanager.console.ConsoleBuilder; import com.cyanogenmod.filemanager.console.InsufficientPermissionsException; import com.cyanogenmod.filemanager.console.java.JavaConsole; import com.cyanogenmod.filemanager.model.FileSystemObject; import com.cyanogenmod.filemanager.preferences.FileManagerSettings; import com.cyanogenmod.filemanager.preferences.Preferences; import com.cyanogenmod.filemanager.ui.ThemeManager; import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; import com.cyanogenmod.filemanager.ui.policy.PrintActionPolicy; import com.cyanogenmod.filemanager.ui.widgets.ButtonItem; import com.cyanogenmod.filemanager.util.AndroidHelper; import com.cyanogenmod.filemanager.util.CommandHelper; import com.cyanogenmod.filemanager.util.DialogHelper; import com.cyanogenmod.filemanager.util.ExceptionUtil; import com.cyanogenmod.filemanager.util.ExceptionUtil.OnRelaunchCommandResult; import com.cyanogenmod.filemanager.util.FileHelper; import com.cyanogenmod.filemanager.util.MediaHelper; import com.cyanogenmod.filemanager.util.ResourcesHelper; import com.cyanogenmod.filemanager.util.StringHelper; import org.mozilla.universalchardet.UniversalDetector; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; /** * An internal activity for view and edit files. */ public class EditorActivity extends Activity implements TextWatcher { private static final String TAG = "EditorActivity"; //$NON-NLS-1$ private static boolean DEBUG = false; private static final int WRITE_RETRIES = 3; private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent != null) { if (intent.getAction().compareTo(FileManagerSettings.INTENT_THEME_CHANGED) == 0) { applyTheme(); return; } if (intent.getAction().compareTo(FileManagerSettings.INTENT_SETTING_CHANGED) == 0) { // The settings has changed String key = intent.getStringExtra(FileManagerSettings.EXTRA_SETTING_CHANGED_KEY); if (key != null) { final EditorActivity activity = EditorActivity.this; // No suggestions if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId()) == 0) { // Ignore in binary files if (activity.mBinary) return; // Do we have a different setting? boolean noSuggestionsSetting = Preferences.getSharedPreferences().getBoolean( FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(), ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS. getDefaultValue()).booleanValue()); if (noSuggestionsSetting != activity.mNoSuggestions) { activity.mHandler.post(new Runnable() { @Override public void run() { toggleNoSuggestions(); } }); } // Word wrap } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId()) == 0) { // Ignore in binary files if (activity.mBinary) return; // Do we have a different setting? boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean( FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(), ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP. getDefaultValue()).booleanValue()); if (wordWrapSetting != activity.mWordWrap) { activity.mHandler.post(new Runnable() { @Override public void run() { toggleWordWrap(); } }); } // Syntax highlight // Default theme color scheme // Color scheme } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId()) == 0) { // Ignore in binary files if (activity.mBinary) return; // Do we have a different setting? boolean syntaxHighlightSetting = Preferences.getSharedPreferences().getBoolean( FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(), ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT. getDefaultValue()).booleanValue()); if (syntaxHighlightSetting != activity.mSyntaxHighlight) { activity.mHandler.post(new Runnable() { @Override public void run() { toggleSyntaxHighlight(); } }); } } else if (key.compareTo(FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId()) == 0 ) { // Ignore in binary files if (activity.mBinary) return; // Reload the syntax highlight activity.mHandler.post(new Runnable() { @Override public void run() { reloadSyntaxHighlight(); } }); } } return; } } } }; private class HexDumpAdapter extends ArrayAdapter { private class ViewHolder { TextView mTextView; } public HexDumpAdapter(Context context, List data) { super(context, R.layout.hexdump_line, data); } @Override public View getView(int position, View convertView, ViewGroup parent) { View v = convertView; if (v == null) { final Context context = getContext(); LayoutInflater inflater = LayoutInflater.from(context); Theme theme = ThemeManager.getCurrentTheme(context); v = inflater.inflate(R.layout.hexdump_line, parent, false); ViewHolder viewHolder = new EditorActivity.HexDumpAdapter.ViewHolder(); viewHolder.mTextView = (TextView)v.findViewById(android.R.id.text1); viewHolder.mTextView.setTextAppearance(context, R.style.hexeditor_text_appearance); viewHolder.mTextView.setTypeface(mHexTypeface); theme.setTextColor(context, viewHolder.mTextView, "text_color"); //$NON-NLS-1$ v.setTag(viewHolder); } String text = getItem(position); ViewHolder viewHolder = (ViewHolder)v.getTag(); viewHolder.mTextView.setText(text); return v; } /** * Return the view as a document * * @return StringBuilder a buffer to the document */ public StringBuilder toStringDocument() { StringBuilder sb = new StringBuilder(); int c = getCount(); for (int i = 0; i < c; i++) { sb.append(getItem(i)); sb.append("\n"); } return sb; } } /** * Internal interface to notify progress update */ private interface OnProgressListener { void onProgress(int progress); } /** * An internal listener for read a file */ private class AsyncReader implements AsyncResultListener { final Object mSync = new Object(); boolean mReadDoneLocked = false; ByteArrayOutputStream mByteBuffer = null; ArrayList mBinaryBuffer = null; SpannableStringBuilder mBuffer = null; Exception mCause; long mSize; FileSystemObject mReadFso; OnProgressListener mListener; boolean mDetectEncoding = false; UniversalDetector mDetector; String mDetectedEncoding; /** * Constructor of AsyncReader. For enclosing access. */ public AsyncReader(boolean detectEncoding) { super(); mDetectEncoding = detectEncoding; if (mDetectEncoding) { mDetector = new UniversalDetector(null); } } /** * {@inheritDoc} */ @Override public void onAsyncStart() { this.mByteBuffer = new ByteArrayOutputStream((int)this.mReadFso.getSize()); this.mSize = 0; } /** * {@inheritDoc} */ @Override public void onAsyncEnd(boolean cancelled) { if (!cancelled && StringHelper.isBinaryData(mByteBuffer.toByteArray())) { EditorActivity.this.mBinary = true; EditorActivity.this.mReadOnly = true; } else if (mDetector != null) { mDetector.dataEnd(); mDetectedEncoding = mDetector.getDetectedCharset(); } } /** * {@inheritDoc} */ @Override public void onAsyncExitCode(int exitCode) { synchronized (this.mSync) { mReadDoneLocked = true; this.mSync.notify(); } } /** * {@inheritDoc} */ @Override public void onPartialResult(Object result) { try { if (result == null) return; byte[] partial = (byte[]) result; if (mDetectEncoding) { mDetector.handleData(partial, 0, partial.length); } this.mByteBuffer.write(partial, 0, partial.length); this.mSize += partial.length; if (this.mListener != null && this.mReadFso != null) { int progress = 0; if (this.mReadFso.getSize() != 0) { progress = (int)((this.mSize*100) / this.mReadFso.getSize()); } this.mListener.onProgress(progress); } } catch (Exception e) { this.mCause = e; } } /** * {@inheritDoc} */ @Override public void onException(Exception cause) { this.mCause = cause; } } /** * An internal listener for write a file */ private class AsyncWriter implements AsyncResultListener { Exception mCause; /** * Constructor of AsyncWriter. For enclosing access. */ public AsyncWriter() { super(); } /** * {@inheritDoc} */ @Override public void onAsyncStart() {/**NON BLOCK**/} /** * {@inheritDoc} */ @Override public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/} /** * {@inheritDoc} */ @Override public void onAsyncExitCode(int exitCode) {/**NON BLOCK**/} /** * {@inheritDoc} */ @Override public void onPartialResult(Object result) {/**NON BLOCK**/} /** * {@inheritDoc} */ @Override public void onException(Exception cause) { this.mCause = cause; } } /** * An internal class to resolve resources for the syntax highlight library. * @hide */ class ResourcesResolver implements ISyntaxHighlightResourcesResolver { @Override public CharSequence getString(String id, String resid) { return EditorActivity.this.getString( ResourcesHelper.getIdentifier( EditorActivity.this.getResources(), "string", resid)); //$NON-NLS-1$ } @Override public int getInteger(String id, String resid, int def) { return EditorActivity.this.getResources(). getInteger( ResourcesHelper.getIdentifier( EditorActivity.this.getResources(), "integer", resid)); //$NON-NLS-1$ } @Override public int getColor(String id, String resid, int def) { final Context ctx = EditorActivity.this; try { // Use the user-defined settings int[] colors = getUserColorScheme(); HighlightColors[] schemeColors = HighlightColors.values(); int cc = schemeColors.length; int cc2 = colors.length; for (int i = 0; i < cc; i++) { if (schemeColors[i].getId().compareTo(id) == 0) { if (cc2 >= i) { // User-defined return colors[i]; } // Theme default return ThemeManager.getCurrentTheme(ctx).getColor(ctx, resid); } } } catch (Exception ex) { // Resource not found } return def; } /** * Method that returns the user-defined color scheme * * @return int[] The user-defined color scheme */ private int[] getUserColorScheme() { String defaultValue = (String)FileManagerSettings. SETTINGS_EDITOR_SH_COLOR_SCHEME.getDefaultValue(); String value = Preferences.getSharedPreferences().getString( FileManagerSettings.SETTINGS_EDITOR_SH_COLOR_SCHEME.getId(), defaultValue); return EditorSHColorSchemePreferenceFragment.toColorShemeArray(value); } } /** * @hide */ FileSystemObject mFso; private int mBufferSize; private long mMaxFileSize; /** * @hide */ boolean mDirty; /** * @hide */ boolean mReadOnly; /** * @hide */ boolean mBinary; /** * @hide */ TextView mTitle; /** * @hide */ EditText mEditor; /** * @hide */ ListView mBinaryEditor; /** * @hide */ View mProgress; /** * @hide */ ProgressBar mProgressBar; /** * @hide */ TextView mProgressBarMsg; /** * @hide */ ButtonItem mSave; /** * @hide */ ButtonItem mPrint; // No suggestions status /** * @hide */ boolean mNoSuggestions; // Word wrap status private ViewGroup mWordWrapView; private ViewGroup mNoWordWrapView; /** * @hide */ boolean mWordWrap; // Syntax highlight status /** * @hide */ boolean mSyntaxHighlight; /** * @hide */ SyntaxHighlightProcessor mSyntaxHighlightProcessor; private int mEditStart; private int mEditEnd; private View mOptionsAnchorView; private Typeface mHexTypeface; private final Object mExecSync = new Object(); /** * @hide */ Handler mHandler; /** * @hide */ String mHexLineSeparator; private boolean mHexDump; /** * Intent extra parameter for the path of the file to open. */ public static final String EXTRA_OPEN_FILE = "extra_open_file"; //$NON-NLS-1$ /** * {@inheritDoc} */ @Override protected void onCreate(Bundle state) { if (DEBUG) { Log.d(TAG, "EditorActivity.onCreate"); //$NON-NLS-1$ } this.mHandler = new Handler(); // Load typeface for hex editor mHexTypeface = Typeface.createFromAsset(getAssets(), "fonts/Courier-Prime.ttf"); // Save hexdump user preference mHexDump = Preferences.getSharedPreferences().getBoolean( FileManagerSettings.SETTINGS_EDITOR_HEXDUMP.getId(), ((Boolean)FileManagerSettings.SETTINGS_EDITOR_HEXDUMP. getDefaultValue()).booleanValue()); // Register the broadcast receiver IntentFilter filter = new IntentFilter(); filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED); filter.addAction(FileManagerSettings.INTENT_SETTING_CHANGED); registerReceiver(this.mNotificationReceiver, filter); // Generate a random separator this.mHexLineSeparator = UUID.randomUUID().toString() + UUID.randomUUID().toString(); // Set the theme before setContentView Theme theme = ThemeManager.getCurrentTheme(this); theme.setBaseTheme(this, false); //Set the main layout of the activity setContentView(R.layout.editor); // Get the limit vars this.mBufferSize = getResources().getInteger(R.integer.buffer_size); long availMem = AndroidHelper.getAvailableMemory(this); this.mMaxFileSize = Math.min(availMem, getResources().getInteger(R.integer.editor_max_file_size)); //Initialize initTitleActionBar(); initLayout(); // Apply the theme applyTheme(); // Initialize the console initializeConsole(); // Read the file readFile(); //Save state super.onCreate(state); } /** * {@inheritDoc} */ @Override protected void onDestroy() { if (DEBUG) { Log.d(TAG, "EditorActivity.onDestroy"); //$NON-NLS-1$ } // Unregister the receiver try { unregisterReceiver(this.mNotificationReceiver); } catch (Throwable ex) { /**NON BLOCK**/ } //All destroy. Continue super.onDestroy(); } /** * {@inheritDoc} */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } /** * Method that initializes the titlebar of the activity. */ private void initTitleActionBar() { //Configure the action bar options getActionBar().setBackgroundDrawable( getResources().getDrawable(R.drawable.bg_material_titlebar)); getActionBar().setDisplayOptions( ActionBar.DISPLAY_SHOW_CUSTOM); getActionBar().setDisplayHomeAsUpEnabled(true); View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false); this.mTitle = (TextView)customTitle.findViewById(R.id.customtitle_title); this.mTitle.setText(R.string.editor); this.mTitle.setContentDescription(getString(R.string.editor)); this.mSave = (ButtonItem)customTitle.findViewById(R.id.ab_button0); this.mSave.setImageResource(R.drawable.ic_material_light_save); this.mSave.setContentDescription(getString(R.string.actionbar_button_save_cd)); this.mSave.setVisibility(View.GONE); this.mPrint = (ButtonItem)customTitle.findViewById(R.id.ab_button1); this.mPrint.setImageResource(R.drawable.ic_material_light_print); this.mPrint.setContentDescription(getString(R.string.actionbar_button_print_cd)); this.mPrint.setVisibility(View.VISIBLE); ButtonItem configuration = (ButtonItem)customTitle.findViewById(R.id.ab_button2); configuration.setImageResource(R.drawable.ic_material_light_overflow); configuration.setContentDescription(getString(R.string.actionbar_button_overflow_cd)); View status = findViewById(R.id.editor_status); boolean showOptionsMenu = AndroidHelper.showOptionsMenu(this); configuration.setVisibility(showOptionsMenu ? View.VISIBLE : View.GONE); this.mOptionsAnchorView = showOptionsMenu ? configuration : status; getActionBar().setCustomView(customTitle); } /** * Method that initializes the layout and components of the activity. */ private void initLayout() { this.mEditor = (EditText)findViewById(R.id.editor); this.mEditor.setText(null); this.mEditor.addTextChangedListener(this); this.mEditor.setEnabled(false); this.mWordWrapView = (ViewGroup)findViewById(R.id.editor_word_wrap_view); this.mNoWordWrapView = (ViewGroup)findViewById(R.id.editor_no_word_wrap_view); this.mWordWrapView.setVisibility(View.VISIBLE); this.mNoWordWrapView.setVisibility(View.GONE); this.mBinaryEditor = (ListView)findViewById(R.id.editor_binary); this.mNoSuggestions = false; this.mWordWrap = true; this.mSyntaxHighlight = true; // Load the no suggestions setting boolean noSuggestionsSetting = Preferences.getSharedPreferences().getBoolean( FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS.getId(), ((Boolean)FileManagerSettings.SETTINGS_EDITOR_NO_SUGGESTIONS. getDefaultValue()).booleanValue()); if (noSuggestionsSetting != this.mNoSuggestions) { toggleNoSuggestions(); } // Load the word wrap setting boolean wordWrapSetting = Preferences.getSharedPreferences().getBoolean( FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP.getId(), ((Boolean)FileManagerSettings.SETTINGS_EDITOR_WORD_WRAP. getDefaultValue()).booleanValue()); if (wordWrapSetting != this.mWordWrap) { toggleWordWrap(); } // Load the syntax highlight setting boolean syntaxHighlighSetting = Preferences.getSharedPreferences().getBoolean( FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT.getId(), ((Boolean)FileManagerSettings.SETTINGS_EDITOR_SYNTAX_HIGHLIGHT. getDefaultValue()).booleanValue()); if (syntaxHighlighSetting != this.mSyntaxHighlight) { toggleSyntaxHighlight(); } this.mProgress = findViewById(R.id.editor_progress); this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar); this.mProgressBarMsg = (TextView)findViewById(R.id.editor_progress_msg); } /** * Method that toggle the no suggestions property of the editor * @hide */ /**package**/ void toggleNoSuggestions() { synchronized (this.mExecSync) { int type = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE; if (!this.mNoSuggestions) { type |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; } this.mEditor.setInputType(type); this.mNoSuggestions = !this.mNoSuggestions; } } /** * Method that toggle the word wrap property of the editor * @hide */ /**package**/ void toggleWordWrap() { synchronized (this.mExecSync) { ViewGroup vSrc = this.mWordWrap ? this.mWordWrapView : this.mNoWordWrapView; ViewGroup vDst = this.mWordWrap ? this.mNoWordWrapView : this.mWordWrapView; ViewGroup vSrcParent = this.mWordWrap ? this.mWordWrapView : (ViewGroup)this.mNoWordWrapView.getChildAt(0); ViewGroup vDstParent = this.mWordWrap ? (ViewGroup)this.mNoWordWrapView.getChildAt(0) : this.mWordWrapView; vSrc.setVisibility(View.GONE); vSrcParent.removeView(this.mEditor); vDstParent.addView(this.mEditor); vDst.setVisibility(View.VISIBLE); vDst.scrollTo(0, 0); this.mWordWrap = !this.mWordWrap; } } /** * Method that toggles the syntax highlight property of the editor * @hide */ /**package**/ void toggleSyntaxHighlight() { synchronized (this.mExecSync) { if (this.mSyntaxHighlightProcessor != null) { try { if (this.mSyntaxHighlight) { this.mSyntaxHighlightProcessor.clear(this.mEditor.getText()); } else { this.mSyntaxHighlightProcessor.process(this.mEditor.getText()); } } catch (Exception ex) { // An error in a syntax library, should not break down app. Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$ } } this.mSyntaxHighlight = !this.mSyntaxHighlight; } } /** * Method that reloads the syntax highlight of the current file * @hide */ /**package**/ void reloadSyntaxHighlight() { synchronized (this.mExecSync) { if (this.mSyntaxHighlightProcessor != null) { try { this.mSyntaxHighlightProcessor.initialize(); this.mSyntaxHighlightProcessor.process(this.mEditor.getText()); } catch (Exception ex) { // An error in a syntax library, should not break down app. Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$ } } } } /** * {@inheritDoc} */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_MENU: showOverflowPopUp(this.mOptionsAnchorView); return true; case KeyEvent.KEYCODE_BACK: checkDirtyState(); return true; default: return super.onKeyUp(keyCode, event); } } /** * {@inheritDoc} */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: if ((getActionBar().getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) == ActionBar.DISPLAY_HOME_AS_UP) { checkDirtyState(); } return true; default: return super.onOptionsItemSelected(item); } } /** * Method that shows a popup with the activity main menu. * * @param anchor The anchor of the popup */ private void showOverflowPopUp(View anchor) { SimpleMenuListAdapter adapter = new HighlightedSimpleMenuListAdapter(this, R.menu.editor, true); MenuItem noSuggestions = adapter.getMenu().findItem(R.id.mnu_no_suggestions); if (noSuggestions != null) { if (this.mBinary) { adapter.getMenu().removeItem(R.id.mnu_no_suggestions); } else { noSuggestions.setChecked(this.mNoSuggestions); } } MenuItem wordWrap = adapter.getMenu().findItem(R.id.mnu_word_wrap); if (wordWrap != null) { if (this.mBinary) { adapter.getMenu().removeItem(R.id.mnu_word_wrap); } else { wordWrap.setChecked(this.mWordWrap); } } MenuItem syntaxHighlight = adapter.getMenu().findItem(R.id.mnu_syntax_highlight); if (syntaxHighlight != null) { if (this.mBinary) { adapter.getMenu().removeItem(R.id.mnu_syntax_highlight); } else { syntaxHighlight.setChecked(this.mSyntaxHighlight); } } final ListPopupWindow popup = DialogHelper.createListPopupWindow(this, adapter, anchor); popup.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick( final AdapterView parent, final View v, final int position, final long id) { final int itemId = (int)id; switch (itemId) { case R.id.mnu_no_suggestions: toggleNoSuggestions(); break; case R.id.mnu_word_wrap: DialogHelper.showToast(EditorActivity.this, R.string.toggle_word_wrap_msg, Toast.LENGTH_SHORT); mHandler.postDelayed(new Runnable() { @Override public void run() { toggleWordWrap(); } }, 50); break; case R.id.mnu_syntax_highlight: toggleSyntaxHighlight(); break; case R.id.mnu_settings: //Settings Intent settings = new Intent(EditorActivity.this, SettingsPreferences.class); settings.putExtra( PreferenceActivity.EXTRA_SHOW_FRAGMENT, EditorPreferenceFragment.class.getName()); startActivity(settings); break; } popup.dismiss(); } }); popup.show(); } /** * Method invoked when an action item is clicked. * * @param view The button pushed */ public void onActionBarItemClick(View view) { switch (view.getId()) { case R.id.ab_button0: // Save the file checkAndWrite(); break; case R.id.ab_button1: // Print the file StringBuilder sb = mBinary ? ((HexDumpAdapter)mBinaryEditor.getAdapter()).toStringDocument() : new StringBuilder(mEditor.getText().toString()); PrintActionPolicy.printStringDocument(this, mFso, sb); break; case R.id.ab_button2: // Show overflow menu showOverflowPopUp(this.mOptionsAnchorView); break; default: break; } } /** * Method that initializes a console */ private boolean initializeConsole() { try { ConsoleBuilder.getConsole(this); // There is a console allocated. Use it. return true; } catch (Throwable _throw) { // Capture the exception ExceptionUtil.translateException(this, _throw, false, true); } return false; } /** * Method that reads the requested file */ private void readFile() { // For now editor is not dirty and editable. setDirty(false); this.mBinary = false; // Check for a valid action String action = getIntent().getAction(); if (action == null || (action.compareTo(Intent.ACTION_VIEW) != 0) && (action.compareTo(Intent.ACTION_EDIT) != 0)) { DialogHelper.showToast( this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT); return; } // This var should be set depending on ACTION_VIEW or ACTION_EDIT action, but for // better compatibility, IntentsActionPolicy use always ACTION_VIEW, so we have // to ignore this check here this.mReadOnly = false; // Read the intent and check that is has a valid request Intent fileIntent = getIntent(); if (fileIntent.getData().getScheme().equals("content")) { asyncReadContentURI(fileIntent.getData()); this.mTitle.setText(fileIntent.getDataString()); } else { // File Scheme URI's String path = uriToPath(this, getIntent().getData()); if (path == null || path.length() == 0) { DialogHelper.showToast( this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT); return; } // Set the title of the dialog File f = new File(path); this.mTitle.setText(f.getName()); // Check that the file exists (the real file, not the symlink) try { this.mFso = CommandHelper.getFileInfo(this, path, true, null); if (this.mFso == null) { } } catch (Exception e) { Log.e(TAG, "Failed to get file reference", e); //$NON-NLS-1$ } // Check that we can handle the length of the file (by device) if (this.mMaxFileSize < this.mFso.getSize()) { DialogHelper.showToast( this, R.string.editor_file_exceed_size_msg, Toast.LENGTH_SHORT); return; } // Get the syntax highlight processor SyntaxHighlightFactory shpFactory = SyntaxHighlightFactory.getDefaultFactory(new ResourcesResolver()); this.mSyntaxHighlightProcessor = shpFactory.getSyntaxHighlightProcessor(f); if (this.mSyntaxHighlightProcessor != null) { this.mSyntaxHighlightProcessor.initialize(); } // Check that we have read access try { FileHelper.ensureReadAccess( ConsoleBuilder.getConsole(this), this.mFso, null); // Read the file in background asyncRead(); } catch (Exception ex) { ExceptionUtil.translateException( this, ex, false, true, new OnRelaunchCommandResult() { @Override public void onSuccess() { // Read the file in background asyncRead(); } @Override public void onFailed(Throwable cause) { finish(); } @Override public void onCancelled() { finish(); } }); } } } /** * Method that does the read of a content uri in the background * @hide */ void asyncReadContentURI(Uri uri) { // Do the load of the file AsyncTask mReadTask = new AsyncTask() { private Exception mCause; private boolean changeToBinaryMode; private boolean changeToDisplaying; private String tempText; @Override protected void onPreExecute() { // Show the progress this.changeToBinaryMode = false; this.changeToDisplaying = false; doProgress(true, 0); } @Override protected Boolean doInBackground(Uri... params) { // Only one argument (the file to open) Uri fso = params[0]; this.mCause = null; // Read the file in an async listener try { publishProgress(Integer.valueOf(0)); InputStream is = getContentResolver().openInputStream(fso); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int readBytes; byte[] buffer = new byte[1024]; try { while ((readBytes = is.read(buffer, 0, buffer.length)) != -1) { baos.write(buffer, 0, readBytes); publishProgress( Integer.valueOf((readBytes * 100) / buffer.length)); } } catch (IOException e) { e.printStackTrace(); return Boolean.FALSE; } // 100% publishProgress(new Integer(100)); tempText = new String(baos.toByteArray(), "UTF-8"); Log.i(TAG, "Bytes read: " + baos.toByteArray().length); //$NON-NLS-1$ // 100% this.changeToDisplaying = true; publishProgress(new Integer(0)); } catch (Exception e) { this.mCause = e; return Boolean.FALSE; } return Boolean.TRUE; } @Override protected void onProgressUpdate(Integer... values) { // Do progress doProgress(true, values[0].intValue()); } @Override protected void onPostExecute(Boolean result) { final EditorActivity activity = EditorActivity.this; // Is error? if (!result.booleanValue()) { if (this.mCause != null) { ExceptionUtil.translateException(activity, this.mCause); activity.mEditor.setEnabled(false); } } else { // Now we have the buffer, set the text of the editor activity.mEditor.setText( tempText, BufferType.EDITABLE); // Highlight editor text syntax if (activity.mSyntaxHighlight && activity.mSyntaxHighlightProcessor != null) { try { activity.mSyntaxHighlightProcessor.process( activity.mEditor.getText()); } catch (Exception ex) { // An error in a syntax library, should not break down app. Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$ } } setDirty(false); activity.mEditor.setEnabled(!activity.mReadOnly); // Notify read-only mode if (activity.mReadOnly) { DialogHelper.showToast( activity, R.string.editor_read_only_mode, Toast.LENGTH_SHORT); } } doProgress(false, 0); } @Override protected void onCancelled() { // Hide the progress doProgress(false, 0); } /** * Method that update the progress status * * @param visible If the progress bar need to be hidden * @param progress The progress */ private void doProgress(boolean visible, int progress) { final EditorActivity activity = EditorActivity.this; // Show the progress bar activity.mProgressBar.setProgress(progress); activity.mProgress.setVisibility(visible ? View.VISIBLE : View.GONE); if (this.changeToBinaryMode) { mWordWrapView.setVisibility(View.GONE); mNoWordWrapView.setVisibility(View.GONE); mBinaryEditor.setVisibility(View.VISIBLE); // Show hex dumping text activity.mProgressBarMsg.setText(R.string.dumping_message); this.changeToBinaryMode = false; } else if (this.changeToDisplaying) { activity.mProgressBarMsg.setText(R.string.displaying_message); this.changeToDisplaying = false; } } }; mReadTask.execute(uri); } /** * Method that does the read of the file in background * @hide */ void asyncRead() { // Do the load of the file AsyncTask mReadTask = new AsyncTask() { private Exception mCause; private AsyncReader mReader; private boolean changeToBinaryMode; private boolean changeToDisplaying; @Override protected void onPreExecute() { // Show the progress this.changeToBinaryMode = false; this.changeToDisplaying = false; doProgress(true, 0); } @Override protected Boolean doInBackground(FileSystemObject... params) { final EditorActivity activity = EditorActivity.this; // Only one argument (the file to open) FileSystemObject fso = params[0]; this.mCause = null; // Read the file in an async listener try { while (true) { // Configure the reader this.mReader = new AsyncReader(true); this.mReader.mReadFso = fso; this.mReader.mListener = new OnProgressListener() { @Override @SuppressWarnings("synthetic-access") public void onProgress(int progress) { publishProgress(Integer.valueOf(progress)); } }; // Execute the command (read the file) CommandHelper.read(activity, fso.getFullPath(), this.mReader, null); // Wait for synchronized (this.mReader.mSync) { while (!this.mReader.mReadDoneLocked) { this.mReader.mSync.wait(); } } // 100% publishProgress(new Integer(100)); // Check if the read was successfully if (this.mReader.mCause != null) { this.mCause = this.mReader.mCause; return Boolean.FALSE; } break; } // Now we have the byte array with all the data. is a binary file? // Then dump them byte array to hex dump string (only if users settings // to dump file) if (activity.mBinary && mHexDump) { // we do not use the Hexdump helper class, because we need to show the // progress of the dump process final String data = toHexPrintableString(toHexDump( this.mReader.mByteBuffer.toByteArray())); this.mReader.mBinaryBuffer = new ArrayList(); BufferedReader reader = new BufferedReader(new StringReader(data)); String line; while ((line = reader.readLine()) != null) { this.mReader.mBinaryBuffer.add(line); } Log.i(TAG, "Bytes read: " + data.length()); //$NON-NLS-1$ } else { String data; if (this.mReader.mDetectedEncoding != null) { data = new String(this.mReader.mByteBuffer.toByteArray(), this.mReader.mDetectedEncoding); } else { data = new String(this.mReader.mByteBuffer.toByteArray()); } this.mReader.mBuffer = new SpannableStringBuilder(data); Log.i(TAG, "Bytes read: " + data.getBytes().length); //$NON-NLS-1$ } this.mReader.mByteBuffer = null; // 100% this.changeToDisplaying = true; publishProgress(new Integer(0)); } catch (Exception e) { this.mCause = e; return Boolean.FALSE; } return Boolean.TRUE; } @Override protected void onProgressUpdate(Integer... values) { // Do progress doProgress(true, values[0].intValue()); } @Override protected void onPostExecute(Boolean result) { final EditorActivity activity = EditorActivity.this; // Is error? if (!result.booleanValue()) { if (this.mCause != null) { ExceptionUtil.translateException(activity, this.mCause); activity.mEditor.setEnabled(false); } } else { // Now we have the buffer, set the text of the editor if (activity.mBinary && mHexDump) { HexDumpAdapter adapter = new HexDumpAdapter(EditorActivity.this, this.mReader.mBinaryBuffer); mBinaryEditor.setAdapter(adapter); // Cleanup this.mReader.mBinaryBuffer = null; } else { activity.mEditor.setText( this.mReader.mBuffer, BufferType.EDITABLE); // Highlight editor text syntax if (activity.mSyntaxHighlight && activity.mSyntaxHighlightProcessor != null) { try { activity.mSyntaxHighlightProcessor.process( activity.mEditor.getText()); } catch (Exception ex) { // An error in a syntax library, should not break down app. Log.e(TAG, "Syntax highlight failed.", ex); //$NON-NLS-1$ } } //Cleanup this.mReader.mBuffer = null; } setDirty(false); activity.mEditor.setEnabled(!activity.mReadOnly); // Notify read-only mode if (activity.mReadOnly) { DialogHelper.showToast( activity, R.string.editor_read_only_mode, Toast.LENGTH_SHORT); } } doProgress(false, 0); } @Override protected void onCancelled() { // Hide the progress doProgress(false, 0); } /** * Method that update the progress status * * @param visible If the progress bar need to be hidden * @param progress The progress */ private void doProgress(boolean visible, int progress) { final EditorActivity activity = EditorActivity.this; // Show the progress bar activity.mProgressBar.setProgress(progress); activity.mProgress.setVisibility(visible ? View.VISIBLE : View.GONE); if (this.changeToBinaryMode) { mWordWrapView.setVisibility(View.GONE); mNoWordWrapView.setVisibility(View.GONE); mBinaryEditor.setVisibility(View.VISIBLE); // Show hex dumping text activity.mProgressBarMsg.setText(R.string.dumping_message); this.changeToBinaryMode = false; } else if (this.changeToDisplaying) { activity.mProgressBarMsg.setText(R.string.displaying_message); this.changeToDisplaying = false; } } /** * Create a hex dump of the data while show progress to user * * @param data The data to hex dump * @return StringBuilder The hex dump buffer */ private String toHexDump(byte[] data) { //Change to binary mode this.changeToBinaryMode = true; // Start progress publishProgress(Integer.valueOf(0)); // Calculate max dir size int length = data.length; final int DISPLAY_SIZE = 16; // Bytes per line ByteArrayInputStream bais = new ByteArrayInputStream(data); byte[] line = new byte[DISPLAY_SIZE]; int read = 0; int offset = 0; StringBuilder sb = new StringBuilder(); while ((read = bais.read(line, 0, DISPLAY_SIZE)) != -1) { //offset dump(16) data\n String linedata = new String(line, 0, read); sb.append(HexDump.toHexString(offset)); sb.append(" "); //$NON-NLS-1$ String hexDump = HexDump.toHexString(line, 0, read); if (hexDump.length() != (DISPLAY_SIZE * 2)) { char[] array = new char[(DISPLAY_SIZE * 2) - hexDump.length()]; Arrays.fill(array, ' '); hexDump += new String(array); } sb.append(hexDump); sb.append(" "); //$NON-NLS-1$ sb.append(linedata); sb.append(EditorActivity.this.mHexLineSeparator); offset += DISPLAY_SIZE; if (offset % 5 == 0) { publishProgress(Integer.valueOf((offset * 100) / length)); } } // End of the dump process publishProgress(Integer.valueOf(100)); return sb.toString(); } /** * Method that converts to a visual printable hex string * * @param string The string to check */ private String toHexPrintableString(String string) { // Remove characters without visual representation final String REPLACED_SYMBOL = "."; //$NON-NLS-1$ final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$ String printable = string.replaceAll("\\p{Cntrl}", REPLACED_SYMBOL); //$NON-NLS-1$ printable = printable.replaceAll("[^\\p{Print}]", REPLACED_SYMBOL); //$NON-NLS-1$ printable = printable.replaceAll("\\p{C}", REPLACED_SYMBOL); //$NON-NLS-1$ printable = printable.replaceAll(EditorActivity.this.mHexLineSeparator, NEWLINE); return printable; } }; mReadTask.execute(this.mFso); } private void checkAndWrite() { // Check that we have write access try { FileHelper.ensureWriteAccess( ConsoleBuilder.getConsole(this), this.mFso, null); // Write the file ensureSyncWrite(); } catch (Exception ex) { ExceptionUtil.translateException( this, ex, false, true, new OnRelaunchCommandResult() { @Override public void onSuccess() { // Write the file ensureSyncWrite(); } @Override public void onFailed(Throwable cause) {/**NON BLOCK**/} @Override public void onCancelled() {/**NON BLOCK**/} }); } } /** * Method that checks that the write to disk operation was successfully and the * expected bytes are written to disk. * @hide */ void ensureSyncWrite() { try { for (int i = 0; i < WRITE_RETRIES; i++) { // Configure the writer AsyncWriter writer = new AsyncWriter(); // Write to disk final byte[] data = this.mEditor.getText().toString().getBytes(); long expected = data.length; syncWrite(writer, data); // Sleep a bit Thread.sleep(150L); // Is error? if (writer.mCause != null) { Log.e(TAG, "Write operation failed. Retries: " + i, writer.mCause); if (i == (WRITE_RETRIES-1)) { // Something was wrong. The file probably is corrupted DialogHelper.showToast( this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); break; } // Retry continue; } // Check that all the bytes were written FileSystemObject fso = CommandHelper.getFileInfo(this, this.mFso.getFullPath(), true, null); if (fso == null || fso.getSize() != expected) { Log.e(TAG, String.format( "Size is not the same. Expected: %d, Written: %d. Retries: %d", expected, fso == null ? -1 : fso.getSize(), i)); if (i == (WRITE_RETRIES-1)) { // Something was wrong. The destination data is not the same // as the source data DialogHelper.showToast( this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); break; } // Retry continue; } // Success. The file was saved DialogHelper.showToast( this, R.string.editor_successfully_saved, Toast.LENGTH_SHORT); setDirty(false); // Send a message that allow other activities to update his data Intent intent = new Intent(FileManagerSettings.INTENT_FILE_CHANGED); intent.putExtra( FileManagerSettings.EXTRA_FILE_CHANGED_KEY, this.mFso.getFullPath()); sendBroadcast(intent); // Done break; } } catch (Exception ex) { // Something was wrong, but the file was NOT written Log.e(TAG, "The file wasn't written.", ex); DialogHelper.showToast( this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); } } /** * Method that write the file. * * @param writer The command listener * @param bytes The bytes to write * @throws Exception If something was wrong */ private void syncWrite(AsyncWriter writer, byte[] bytes) throws Exception { // Create the writable command WriteExecutable cmd = CommandHelper.write(this, this.mFso.getFullPath(), writer, null); // Obtain access to the buffer (IMP! don't close the buffer here, it's manage // by the command) OutputStream os = cmd.createOutputStream(); try { // Retrieve the text from the editor ByteArrayInputStream bais = new ByteArrayInputStream(bytes); try { // Buffered write byte[] data = new byte[this.mBufferSize]; int read = 0, written = 0; while ((read = bais.read(data, 0, this.mBufferSize)) != -1) { os.write(data, 0, read); written += read; } Log.i(TAG, "Bytes written: " + written); //$NON-NLS-1$ } finally { try { bais.close(); } catch (Exception e) {/**NON BLOCK**/} } } finally { // Ok. Data is written or ensure buffer close cmd.end(); } } /** * {@inheritDoc} */ @Override public void beforeTextChanged( CharSequence s, int start, int count, int after) {/**NON BLOCK**/} /** * {@inheritDoc} */ @Override public void onTextChanged(CharSequence s, int start, int before, int count) { this.mEditStart = start; this.mEditEnd = start + count; } /** * {@inheritDoc} */ @Override public void afterTextChanged(Editable s) { setDirty(true); if (this.mSyntaxHighlightProcessor != null) { this.mSyntaxHighlightProcessor.process(s, this.mEditStart, this.mEditEnd); } } /** * Method that sets if the editor is dirty (has changed) * * @param dirty If the editor is dirty * @hide */ void setDirty(boolean dirty) { this.mDirty = dirty; this.mSave.setVisibility(dirty ? View.VISIBLE : View.GONE); } /** * Check the dirty state of the editor, and ask the user to save the changes * prior to exit. */ public void checkDirtyState() { if (this.mDirty) { AlertDialog dlg = DialogHelper.createYesNoDialog( this, R.string.editor_dirty_ask_title, R.string.editor_dirty_ask_msg, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { dialog.dismiss(); setResult(Activity.RESULT_OK); finish(); } } }); DialogHelper.delegateDialogShow(this, dlg); return; } setResult(Activity.RESULT_OK); finish(); } /** * Method that applies the current theme to the activity * @hide */ void applyTheme() { Theme theme = ThemeManager.getCurrentTheme(this); theme.setBaseTheme(this, false); //- ActionBar theme.setTitlebarDrawable(this, getActionBar(), "titlebar_drawable"); //$NON-NLS-1$ View v = getActionBar().getCustomView().findViewById(R.id.customtitle_title); theme.setTextColor(this, (TextView)v, "action_bar_text_color"); //$NON-NLS-1$ v = findViewById(R.id.ab_button0); theme.setImageDrawable(this, (ImageView)v, "ab_save_drawable"); //$NON-NLS-1$ v = findViewById(R.id.ab_button1); theme.setImageDrawable(this, (ImageView)v, "ab_print_drawable"); //$NON-NLS-1$ v = findViewById(R.id.ab_button2); theme.setImageDrawable(this, (ImageView)v, "ab_overflow_drawable"); //$NON-NLS-1$ //- View v = findViewById(R.id.editor_layout); theme.setBackgroundDrawable(this, v, "background_drawable"); //$NON-NLS-1$ v = findViewById(R.id.editor); theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ //- ProgressBar Drawable dw = theme.getDrawable(this, "horizontal_progress_bar"); //$NON-NLS-1$ this.mProgressBar.setProgressDrawable(dw); v = findViewById(R.id.editor_progress_msg); theme.setTextColor(this, (TextView)v, "text_color"); //$NON-NLS-1$ // Need a full process of syntax highlight if (!this.mBinary && this.mSyntaxHighlight && this.mSyntaxHighlightProcessor != null) { reloadSyntaxHighlight(); } } /** * Method that resolves the content uri to a valid system path * * @param ctx The current context * @param uri The content uri * @return String The system path */ private static String uriToPath(Context ctx, Uri uri) { File file = MediaHelper.contentUriToFile(ctx.getContentResolver(), uri); if (file == null) { file = new File(uri.getPath()); } return file.getAbsolutePath(); } }