/* * 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.os.AsyncTask; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.TextView.BufferType; import android.widget.Toast; import com.cyanogenmod.filemanager.FileManagerApplication; import com.cyanogenmod.filemanager.R; import com.cyanogenmod.filemanager.commands.AsyncResultListener; import com.cyanogenmod.filemanager.commands.WriteExecutable; import com.cyanogenmod.filemanager.console.ConsoleBuilder; import com.cyanogenmod.filemanager.console.InsufficientPermissionsException; import com.cyanogenmod.filemanager.console.RelaunchableException; import com.cyanogenmod.filemanager.model.FileSystemObject; import com.cyanogenmod.filemanager.preferences.AccessMode; import com.cyanogenmod.filemanager.preferences.FileManagerSettings; import com.cyanogenmod.filemanager.ui.ThemeManager; import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; import com.cyanogenmod.filemanager.ui.widgets.ButtonItem; import com.cyanogenmod.filemanager.util.CommandHelper; import com.cyanogenmod.filemanager.util.DialogHelper; import com.cyanogenmod.filemanager.util.ExceptionUtil; import java.io.ByteArrayInputStream; import java.io.File; import java.io.OutputStream; /** * 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 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(); } } } }; private static final char[] VALID_NON_PRINTABLE_CHARS = {' ', '\t', '\r', '\n'}; /** * Internal interface to notify progress update */ private interface OnProgressListener { void onProgress(int progress); } /** * An internal listener for read a file */ @SuppressWarnings("hiding") private class AsyncReader implements AsyncResultListener { final Object mSync = new Object(); StringBuilder mBuffer = new StringBuilder(); Exception mCause; long mSize; FileSystemObject mFso; OnProgressListener mListener; /** * Constructor of <code>AsyncReader</code>. For enclosing access. */ public AsyncReader() { super(); } /** * {@inheritDoc} */ @Override public void onAsyncStart() { this.mBuffer = new StringBuilder(); this.mSize = 0; } /** * {@inheritDoc} */ @Override public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/} /** * {@inheritDoc} */ @Override public void onAsyncExitCode(int exitCode) { synchronized (this.mSync) { this.mSync.notify(); } } /** * {@inheritDoc} */ @Override public void onPartialResult(Object result) { try { byte[] partial = (byte[])result; // Check if the file is a binary file. In this case the editor // is read-only if (!EditorActivity.this.mReadOnly && partial != null) { for (int i = 0; i < partial.length-1; i++) { if (!isPrintableCharacter((char)partial[i])) { EditorActivity.this.mBinary = true; EditorActivity.this.mReadOnly = true; break; } } } this.mBuffer.append(new String(partial)); this.mSize += this.mBuffer.length(); if (this.mListener != null && this.mFso != null) { int progress = 0; if (this.mFso.getSize() != 0) { progress = (int)((this.mSize*100) / this.mFso.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 <code>AsyncWriter</code>. 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; } } /** * @hide */ FileSystemObject mFso; private int mBufferSize; private int mMaxFileSize; /** * @hide */ boolean mDirty; /** * @hide */ boolean mReadOnly; /** * @hide */ boolean mBinary; /** * @hide */ TextView mTitle; /** * @hide */ EditText mEditor; /** * @hide */ View mProgress; /** * @hide */ ProgressBar mProgressBar; /** * @hide */ ButtonItem mSave; /** * 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$ } // Register the broadcast receiver IntentFilter filter = new IntentFilter(); filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED); registerReceiver(this.mNotificationReceiver, filter); //Set the main layout of the activity setContentView(R.layout.editor); // Get the limit vars this.mBufferSize = getApplicationContext().getResources().getInteger(R.integer.buffer_size); this.mMaxFileSize = getApplicationContext().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_holo_titlebar)); getActionBar().setDisplayOptions( ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME); 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_button1); this.mSave.setImageResource(R.drawable.ic_holo_light_save); this.mSave.setContentDescription(getString(R.string.actionbar_button_save_cd)); this.mSave.setVisibility(View.INVISIBLE); 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.mProgress = findViewById(R.id.editor_progress); this.mProgressBar = (ProgressBar)findViewById(R.id.editor_progress_bar); } /** * {@inheritDoc} */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { checkDirtyState(); return false; } 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 invoked when an action item is clicked. * * @param view The button pushed */ public void onActionBarItemClick(View view) { switch (view.getId()) { case R.id.ab_button1: // Save the file writeFile(); break; default: break; } } /** * Method that initializes a console */ private boolean initializeConsole() { try { // Is there a console allocate if (!ConsoleBuilder.isAlloc()) { // Create a console 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 String path = getIntent().getData().getPath(); 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 we have access to the file (the real file, not the symlink) try { this.mFso = CommandHelper.getFileInfo(this, path, true, null); if (this.mFso == null) { DialogHelper.showToast( this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT); return; } } catch (Exception e) { Log.e(TAG, "Failed to get file reference", e); //$NON-NLS-1$ DialogHelper.showToast( this, R.string.editor_file_not_found_msg, Toast.LENGTH_SHORT); return; } // 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; } // Read the file in background asyncRead(); } /** * Method that does the read of the file in background * @hide */ void asyncRead() { // Do the load of the file AsyncTask<FileSystemObject, Integer, Boolean> mReadTask = new AsyncTask<FileSystemObject, Integer, Boolean>() { private Exception mCause; private AsyncReader mReader; @Override protected void onPreExecute() { // Show the progress doProgress(true, 0); } @Override protected Boolean doInBackground(FileSystemObject... params) { // 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(); this.mReader.mFso = 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( EditorActivity.this, fso.getFullPath(), this.mReader, null); // Wait for synchronized (this.mReader.mSync) { this.mReader.mSync.wait(); } // 100% doProgress(true, 100); // Check if the read was successfully if (this.mReader.mCause != null) { // Check if we can't read the file because we don't the require // permissions. If we are in a ChRooted environment, resolve the // error without doing anymore if (this.mReader.mCause instanceof InsufficientPermissionsException) { if (!ConsoleBuilder.isPrivileged() && FileManagerApplication.getAccessMode(). compareTo(AccessMode.SAFE) != 0) { // We don't have a privileged console, we can't ask the user // to gain privileges and relauch the command again askGainAccessAndRead( (RelaunchableException)this.mReader.mCause); return Boolean.TRUE; } } this.mCause = this.mReader.mCause; return Boolean.FALSE; } break; } } 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) { // Hide the progress doProgress(false, 0); // Is error? if (!result.booleanValue()) { if (this.mCause != null) { ExceptionUtil.translateException(EditorActivity.this, this.mCause); EditorActivity.this.mEditor.setEnabled(false); } } else { // Now we have the buffer, set the text of the editor if (EditorActivity.this.mBinary) { EditorActivity.this.mEditor.setText( this.mReader.mBuffer, BufferType.NORMAL); } else { EditorActivity.this.mEditor.setText( this.mReader.mBuffer, BufferType.EDITABLE); } this.mReader.mBuffer = null; //Cleanup setDirty(false); EditorActivity.this.mEditor.setEnabled(!EditorActivity.this.mReadOnly); // Notify read-only mode if (EditorActivity.this.mReadOnly) { DialogHelper.showToast( EditorActivity.this, R.string.editor_read_only_mode, Toast.LENGTH_SHORT); } } } @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) { // Show the progress bar EditorActivity.this.mProgressBar.setProgress(progress); EditorActivity.this.mProgress.setVisibility( visible ? View.VISIBLE : View.GONE); } }; mReadTask.execute(this.mFso); } /** * Method that reads the requested file. */ private void writeFile() { try { // Configure the writer AsyncWriter writer = new AsyncWriter(); // 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 String text = this.mEditor.getText().toString(); ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes()); text = null; try { // Buffered write byte[] data = new byte[this.mBufferSize]; int read = 0; while ((read = bais.read(data, 0, this.mBufferSize)) != -1) { os.write(data, 0, read); } } finally { try { bais.close(); } catch (Exception e) {/**NON BLOCK**/} } } finally { // Ok. Data is written or ensure buffer close cmd.end(); } // Sleep a bit Thread.sleep(150L); // Is error? if (writer.mCause != null) { // Something was wrong. The file probably is corrupted DialogHelper.showToast( this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); } else { // 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); } } catch (Exception e) { // Something was wrong, but the file was NOT written DialogHelper.showToast( this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); return; } } /** * Method that asks the user for gain access and reexecute the read command * * @param cause The cause of the reexecution * @hide */ void askGainAccessAndRead(final RelaunchableException cause) { // We cannot use the ExceptionUtil class because the read command is asynchronous // and doesn't have the common mechanism of capture exception. we do our self one. //Create a yes/no dialog and ask the user runOnUiThread(new Runnable() { @Override public void run() { AlertDialog alert = DialogHelper.createYesNoDialog( EditorActivity.this, R.string.confirm_operation, cause.getQuestionResourceId(), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { // Change to privileged console if (!ConsoleBuilder. changeToPrivilegedConsole(EditorActivity.this)) { // Capture the exception ExceptionUtil.translateException( EditorActivity.this, cause); EditorActivity.this.mEditor.setEnabled(false); return; } //Read the file again asyncRead(); } else { // Finish the application EditorActivity.this.finish(); } } }); DialogHelper.delegateDialogShow(EditorActivity.this, alert); } }); } /** * {@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) {/**NON BLOCK**/} /** * {@inheritDoc} */ @Override public void afterTextChanged(Editable s) { setDirty(true); } /** * 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 check if a character is valid printable character * * @param c The character to check * @return boolean If the character is printable * @hide */ static boolean isPrintableCharacter(char c) { int cc = VALID_NON_PRINTABLE_CHARS.length; for (int i = 0; i < cc; i++) { if (c == VALID_NON_PRINTABLE_CHARS[i]) { return true; } } return TextUtils.isGraphic(c); } /** * 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, "text_color"); //$NON-NLS-1$ v = findViewById(R.id.ab_button1); theme.setImageDrawable(this, (ImageView)v, "ab_save_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$ } }