diff options
-rw-r--r-- | AndroidManifest.xml | 12 | ||||
-rw-r--r-- | res/drawable-hdpi/ic_holo_light_save.png | bin | 0 -> 1428 bytes | |||
-rw-r--r-- | res/drawable-hdpi/ic_launcher_editor.png | bin | 0 -> 3890 bytes | |||
-rw-r--r-- | res/drawable-mdpi/ic_holo_light_save.png | bin | 0 -> 1379 bytes | |||
-rw-r--r-- | res/drawable-mdpi/ic_launcher_editor.png | bin | 0 -> 2126 bytes | |||
-rw-r--r-- | res/drawable-xhdpi/ic_holo_light_save.png | bin | 0 -> 1576 bytes | |||
-rw-r--r-- | res/drawable-xhdpi/ic_launcher_editor.png | bin | 0 -> 5464 bytes | |||
-rw-r--r-- | res/layout/editor.xml | 76 | ||||
-rw-r--r-- | res/values/dimen.xml | 3 | ||||
-rw-r--r-- | res/values/overlay.xml | 8 | ||||
-rw-r--r-- | res/values/strings.xml | 17 | ||||
-rw-r--r-- | src/com/cyanogenmod/explorer/activities/EditorActivity.java | 645 | ||||
-rw-r--r-- | src/com/cyanogenmod/explorer/console/shell/ShellConsole.java | 113 | ||||
-rw-r--r-- | src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java | 16 | ||||
-rw-r--r-- | src/com/cyanogenmod/explorer/ui/policy/IntentsActionPolicy.java | 90 | ||||
-rw-r--r-- | src/com/cyanogenmod/explorer/util/CommandHelper.java | 2 |
16 files changed, 942 insertions, 40 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 09b016f9..18c2aeb6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -131,6 +131,18 @@ </intent-filter> </activity> + <activity + android:name=".activities.EditorActivity" + android:label="@string/editor" + android:icon="@drawable/ic_launcher_editor" + android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.EDIT" /> + <category android:name="com.cyanogenmod.explorer.category.INTERNAL_VIEWER" /> + <category android:name="com.cyanogenmod.explorer.category.EDITOR" /> + </intent-filter> + </activity> + </application> </manifest> diff --git a/res/drawable-hdpi/ic_holo_light_save.png b/res/drawable-hdpi/ic_holo_light_save.png Binary files differnew file mode 100644 index 00000000..07c2817b --- /dev/null +++ b/res/drawable-hdpi/ic_holo_light_save.png diff --git a/res/drawable-hdpi/ic_launcher_editor.png b/res/drawable-hdpi/ic_launcher_editor.png Binary files differnew file mode 100644 index 00000000..4321cad2 --- /dev/null +++ b/res/drawable-hdpi/ic_launcher_editor.png diff --git a/res/drawable-mdpi/ic_holo_light_save.png b/res/drawable-mdpi/ic_holo_light_save.png Binary files differnew file mode 100644 index 00000000..d12760bb --- /dev/null +++ b/res/drawable-mdpi/ic_holo_light_save.png diff --git a/res/drawable-mdpi/ic_launcher_editor.png b/res/drawable-mdpi/ic_launcher_editor.png Binary files differnew file mode 100644 index 00000000..9e44ba63 --- /dev/null +++ b/res/drawable-mdpi/ic_launcher_editor.png diff --git a/res/drawable-xhdpi/ic_holo_light_save.png b/res/drawable-xhdpi/ic_holo_light_save.png Binary files differnew file mode 100644 index 00000000..9460b391 --- /dev/null +++ b/res/drawable-xhdpi/ic_holo_light_save.png diff --git a/res/drawable-xhdpi/ic_launcher_editor.png b/res/drawable-xhdpi/ic_launcher_editor.png Binary files differnew file mode 100644 index 00000000..8a0ea907 --- /dev/null +++ b/res/drawable-xhdpi/ic_launcher_editor.png diff --git a/res/layout/editor.xml b/res/layout/editor.xml new file mode 100644 index 00000000..b10ead5d --- /dev/null +++ b/res/layout/editor.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ** 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.
--> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ScrollView + android:id="@+id/editor_scroller" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_alignParentRight="true" + android:layout_alignParentBottom="true" + android:layout_margin="@dimen/default_margin" + android:scrollbars="vertical" + android:fillViewport="true"> + + <EditText + android:id="@+id/editor" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="false" + android:maxLength="@integer/editor_max_file_size" + android:gravity="top|left" + android:cursorVisible="true" + android:background="@null" + android:inputType="textMultiLine|textImeMultiLine" + android:text="@null" + android:textAppearance="@style/secondary_text_appearance" /> + + </ScrollView> + + + <RelativeLayout + android:id="@+id/editor_progress" + android:layout_width="match_parent" + android:layout_height="@dimen/default_progress_height" + android:maxWidth="400dip" + android:layout_margin="@dimen/extra_large_margin" + android:layout_centerHorizontal="true" + android:layout_alignParentBottom="true" + android:visibility="gone"> + + <ProgressBar + android:id="@+id/editor_progress_bar" + android:layout_width="match_parent" + android:layout_height="match_parent" + style="@android:style/Widget.ProgressBar.Horizontal" + android:indeterminate="false" /> + + <TextView + android:id="@+id/editor_progress_msg" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:textAppearance="@style/primary_text_appearance" + android:text="@string/loading_message" + android:gravity="center_vertical|center_horizontal" + android:contentDescription="@null" /> + + </RelativeLayout> + +</RelativeLayout>
\ No newline at end of file diff --git a/res/values/dimen.xml b/res/values/dimen.xml index be6a3b45..0796c483 100644 --- a/res/values/dimen.xml +++ b/res/values/dimen.xml @@ -98,4 +98,7 @@ <!-- The console height --> <dimen name="console_height">250dp</dimen> + <!-- The progress height --> + <dimen name="default_progress_height">32dp</dimen> + </resources>
\ No newline at end of file diff --git a/res/values/overlay.xml b/res/values/overlay.xml index f32e9b35..3727e02f 100644 --- a/res/values/overlay.xml +++ b/res/values/overlay.xml @@ -16,9 +16,8 @@ --> <!-- - This file contains values that could be overlayed. This allow - configure special values for each device. Use overlay building folder on device tree - for overlay this values + This file contains values that could be overlayed. This allow configure special values + for each device. Use overlay building folder on device tree for overlay this values --> <resources xmlns:android="http://schemas.android.com/apk/res/android"> @@ -46,4 +45,7 @@ <!-- The number of characters per line to show in the console dialog --> <integer name="console_max_chars_per_line">150</integer> + <!-- The max limit of file size that the internal editor can open. Default: 4Mb --> + <integer name="editor_max_file_size">4194304</integer> + </resources>
\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index f4f0654b..e33ff93d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -76,7 +76,7 @@ is to switch to non-privileged mode.\n\nApply this change?</string> <!-- A console couldn't be created. Without a console, the application won't work --> - <string name="msgs_cant_create_console">The application is unable to create a console and + <string name="msgs_cant_create_console">The application is unable to create a console and will not work without a console.</string> <!-- The message shown when a allocation of a privileged console fails, and a non privileged is allocated --> @@ -179,6 +179,8 @@ <string name="actionbar_button_overflow_cd">More options</string> <!-- ActionBar Buttons * Storage volumes --> <string name="actionbar_button_storage_cd">Storage volumes</string> + <!-- ActionBar Buttons * Save --> + <string name="actionbar_button_save_cd">Save</string> <!-- Navigation View * Sort * Sort by name (ascending) --> <string name="sort_by_name_asc">By name ▲</string> @@ -351,6 +353,19 @@ <!-- Picker Activity * Dialog title --> <string name="picker_title">Pick a file</string> + <!-- Editor * Editor activity title --> + <string name="editor">Editor</string> + <!-- Editor * Invalid file message --> + <string name="editor_invalid_file_msg">Invalid file.</string> + <!-- Editor * File not found message --> + <string name="editor_file_not_found_msg">File not found.</string> + <!-- Editor * File size exceed the limit --> + <string name="editor_file_exceed_size_msg">The file is too big to be open inside this device.</string> + <!-- Editor * Editor is dirty, ask the user --> + <string name="editor_dirty_ask">There are unsaved changes.\n\nExit without saving?</string> + <!-- Editor * Save operation success --> + <string name="editor_successfully_saved">The file was successfully saved.</string> + <!-- Bookmarks * Bookmarks activity title --> <string name="bookmarks">Bookmarks</string> <!-- Bookmarks * Bookmark name * Home --> diff --git a/src/com/cyanogenmod/explorer/activities/EditorActivity.java b/src/com/cyanogenmod/explorer/activities/EditorActivity.java new file mode 100644 index 00000000..f8c0ce3a --- /dev/null +++ b/src/com/cyanogenmod/explorer/activities/EditorActivity.java @@ -0,0 +1,645 @@ +/* + * 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.explorer.activities; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Editable; +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.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.TextView.BufferType; +import android.widget.Toast; + +import com.cyanogenmod.explorer.R; +import com.cyanogenmod.explorer.commands.AsyncResultListener; +import com.cyanogenmod.explorer.commands.WriteExecutable; +import com.cyanogenmod.explorer.console.ConsoleBuilder; +import com.cyanogenmod.explorer.model.FileSystemObject; +import com.cyanogenmod.explorer.ui.widgets.ButtonItem; +import com.cyanogenmod.explorer.util.CommandHelper; +import com.cyanogenmod.explorer.util.DialogHelper; +import com.cyanogenmod.explorer.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; + + /** + * 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; + 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; + } + } + + private FileSystemObject mFso; + + private int mBufferSize; + private int mMaxFileSize; + + /** + * @hide + */ + boolean mDirty; + /** + * @hide + */ + boolean mReadOnly; + + /** + * @hide + */ + TextView mTitle; + /** + * @hide + */ + ScrollView mScroll; + /** + * @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$ + } + + //Request features + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + + //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(); + initializeConsole(); + readFile(); + + //Save state + super.onCreate(state); + } + + /** + * 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.mScroll = (ScrollView)findViewById(R.id.editor_scroller); + this.mScroll.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, true); + } + // 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); + + // 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.mReadOnly = (action.compareTo(Intent.ACTION_EDIT) == 0); + + // 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; + } + + // Do the load of the + AsyncTask<FileSystemObject, Integer, Boolean> mOpenTask = + 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 { + // 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) { + this.mCause = this.mReader.mCause; + return Boolean.FALSE; + } + + } 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); + } + } else { + // Now we have the buffer, set the text of the editor + EditorActivity.this.mEditor.setText( + this.mReader.mBuffer, BufferType.EDITABLE); + this.mReader.mBuffer = null; //Cleanup + setDirty(false); + EditorActivity.this.mScroll.setEnabled(EditorActivity.this.mReadOnly); + } + } + + @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); + } + }; + mOpenTask.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); + } + + } catch (Exception e) { + // Something was wrong, but the file was NOT written + DialogHelper.showToast( + this, R.string.msgs_operation_failure, Toast.LENGTH_SHORT); + return; + } + } + + /** + * {@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, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + dialog.dismiss(); + setResult(Activity.RESULT_OK); + finish(); + } + } + }); + dlg.show(); + return; + } + setResult(Activity.RESULT_OK); + finish(); + } + +} diff --git a/src/com/cyanogenmod/explorer/console/shell/ShellConsole.java b/src/com/cyanogenmod/explorer/console/shell/ShellConsole.java index aa68c65d..31515be2 100644 --- a/src/com/cyanogenmod/explorer/console/shell/ShellConsole.java +++ b/src/com/cyanogenmod/explorer/console/shell/ShellConsole.java @@ -618,6 +618,7 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis @Override public void run() { int read = 0; + try { while (ShellConsole.this.mActive) { //Read only one byte with active wait @@ -625,6 +626,12 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis if (r == -1) { break; } + + // Type of command + boolean async = + ShellConsole.this.mActiveCommand != null && + ShellConsole.this.mActiveCommand instanceof AsyncResultProgram; + StringBuffer sb = new StringBuffer(); if (!ShellConsole.this.mCancelled) { ShellConsole.this.mSbIn.append((char)r); @@ -633,8 +640,7 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis isCommandStarted(ShellConsole.this.mSbIn); if (ShellConsole.this.mStarted) { sb = new StringBuffer(ShellConsole.this.mSbIn.toString()); - if (ShellConsole.this.mActiveCommand - instanceof AsyncResultProgram) { + if (async) { synchronized (ShellConsole.this.mPartialSync) { ((AsyncResultProgram)ShellConsole. this.mActiveCommand). @@ -649,18 +655,22 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis } //Notify asynchronous partial data - if (ShellConsole.this.mStarted && - ShellConsole.this.mActiveCommand != null && - ShellConsole.this.mActiveCommand instanceof AsyncResultProgram) { + if (ShellConsole.this.mStarted && async) { AsyncResultProgram program = ((AsyncResultProgram)ShellConsole.this.mActiveCommand); - program.onRequestParsePartialResult(sb.toString()); + String partial = sb.toString(); + program.onRequestParsePartialResult(partial); + ShellConsole.this.toStdIn(partial); // Reset the temp buffer sb = new StringBuffer(); } } + if (!async) { + ShellConsole.this.toStdIn(sb.toString()); + } + //Has more data? Read with available as more as exists //or maximum loop count is rebased int count = 0; @@ -671,6 +681,11 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis byte[] data = new byte[available]; read = in.read(data); + // Type of command + async = + ShellConsole.this.mActiveCommand != null && + ShellConsole.this.mActiveCommand instanceof AsyncResultProgram; + // Exit if active command is cancelled if (ShellConsole.this.mCancelled) continue; @@ -681,8 +696,7 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis isCommandStarted(ShellConsole.this.mSbIn); if (ShellConsole.this.mStarted) { sb = new StringBuffer(ShellConsole.this.mSbIn.toString()); - if (ShellConsole.this.mActiveCommand - instanceof AsyncResultProgram) { + if (async) { synchronized (ShellConsole.this.mPartialSync) { ((AsyncResultProgram)ShellConsole. this.mActiveCommand). @@ -700,23 +714,31 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis boolean finished = isCommandFinished(ShellConsole.this.mSbIn, sb); //Notify asynchronous partial data - if (ShellConsole.this.mActiveCommand != null && - ShellConsole.this.mActiveCommand - instanceof AsyncResultProgram) { + if (async) { AsyncResultProgram program = ((AsyncResultProgram)ShellConsole.this.mActiveCommand); - program.onRequestParsePartialResult(sb.toString()); + String partial = sb.toString(); + program.onRequestParsePartialResult(partial); + ShellConsole.this.toStdIn(partial); // Reset the temp buffer sb = new StringBuffer(); } if (finished) { + if (!async) { + ShellConsole.this.toStdIn(sb.toString()); + } + //Notify the end notifyProcessFinished(); break; } + if (!async) { + ShellConsole.this.toStdIn(sb.toString()); + } + //Wait for buffer to be filled try { Thread.sleep(50L); @@ -725,16 +747,9 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis } } - //Audit (if not cancelled) - if (!ShellConsole.this.mCancelled && isTrace()) { - Log.v(TAG, - String.format("stdin: %s", sb.toString())); //$NON-NLS-1$ - } - //Asynchronous programs can cause a lot of output, control buffers //for a low memory footprint - if (ShellConsole.this.mActiveCommand != null && - ShellConsole.this.mActiveCommand instanceof AsyncResultProgram) { + if (async) { trimBuffer(ShellConsole.this.mSbIn); trimBuffer(ShellConsole.this.mSbErr); } @@ -753,6 +768,21 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis } /** + * Method that echoes the stdin + * + * @param stdin The buffer of the stdin + * @hide + */ + void toStdIn(String stdin) { + //Audit (if not cancelled) + if (!this.mCancelled && isTrace() && stdin.length() > 0) { + Log.v(TAG, + String.format( + "stdin: %s", stdin)); //$NON-NLS-1$ + } + } + + /** * Method that creates the standard error thread for read program response. * * @param err The standard error buffer @@ -763,6 +793,7 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis @Override public void run() { int read = 0; + try { while (ShellConsole.this.mActive) { //Read only one byte with active wait @@ -770,19 +801,25 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis if (r == -1) { break; } + + // Type of command + boolean async = + ShellConsole.this.mActiveCommand != null && + ShellConsole.this.mActiveCommand instanceof AsyncResultProgram; + StringBuffer sb = new StringBuffer(); if (!ShellConsole.this.mCancelled) { ShellConsole.this.mSbErr.append((char)r); sb.append((char)r); //Notify asynchronous partial data - if (ShellConsole.this.mStarted && - ShellConsole.this.mActiveCommand != null && - ShellConsole.this.mActiveCommand instanceof AsyncResultProgram) { + if (ShellConsole.this.mStarted && async) { AsyncResultProgram program = ((AsyncResultProgram)ShellConsole.this.mActiveCommand); program.parsePartialErrResult(new String(new char[]{(char)r})); } + + toStdErr(sb.toString()); } //Has more data? Read with available as more as exists @@ -795,6 +832,11 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis byte[] data = new byte[available]; read = err.read(data); + // Type of command + async = + ShellConsole.this.mActiveCommand != null && + ShellConsole.this.mActiveCommand instanceof AsyncResultProgram; + // Exit if active command is cancelled if (ShellConsole.this.mCancelled) continue; @@ -803,12 +845,12 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis sb.append(s); //Notify asynchronous partial data - if (ShellConsole.this.mActiveCommand != null && - ShellConsole.this.mActiveCommand instanceof AsyncResultProgram) { + if (async) { AsyncResultProgram program = ((AsyncResultProgram)ShellConsole.this.mActiveCommand); program.parsePartialErrResult(s); } + toStdErr(sb.toString()); //Wait for buffer to be filled try { @@ -818,12 +860,6 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis } } - //Audit (if not cancelled) - if (!ShellConsole.this.mCancelled && isTrace()) { - Log.v(TAG, - String.format("stderr: %s", sb.toString())); //$NON-NLS-1$ - } - //Asynchronous programs can cause a lot of output, control buffers //for a low memory footprint if (ShellConsole.this.mActiveCommand != null && @@ -843,6 +879,21 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis } /** + * Method that echoes the stderr + * + * @param stdin The buffer of the stderr + * @hide + */ + void toStdErr(String stderr) { + //Audit (if not cancelled) + if (!this.mCancelled && isTrace()) { + Log.v(TAG, + String.format( + "stderr: %s", stderr)); //$NON-NLS-1$ + } + } + + /** * Method that checks the console status and restart the console * if this is unusable. * diff --git a/src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java b/src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java index 67839df2..965f0c00 100644 --- a/src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java +++ b/src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java @@ -43,6 +43,7 @@ import android.widget.Toast; import com.cyanogenmod.explorer.ExplorerApplication; import com.cyanogenmod.explorer.R; import com.cyanogenmod.explorer.adapters.AssociationsAdapter; +import com.cyanogenmod.explorer.ui.policy.IntentsActionPolicy; import com.cyanogenmod.explorer.util.DialogHelper; import com.cyanogenmod.explorer.util.ExceptionUtil; @@ -403,7 +404,7 @@ public class AssociationsDialog implements OnItemClickListener { boolean isPlatformSigned = ExplorerApplication.isAppPlatformSignature(this.mContext); if (isPlatformSigned && this.mAllowPreferred) { - if (filter != null && !isPreferredSelected()) { + if (filter != null && !isPreferredSelected() && !isInternalEditor(ri)) { try { AssociationsAdapter adapter = (AssociationsAdapter)this.mGrid.getAdapter(); final int cc = adapter.getCount(); @@ -447,4 +448,17 @@ public class AssociationsDialog implements OnItemClickListener { this.mContext.startActivity(intent); } } + + /** + * Method that returns if the selected resolve info is about an internal viewer + * + * @param ri The resolve info + * @return boolean If the selected resolve info is about an internal viewer + */ + @SuppressWarnings("static-method") + private boolean isInternalEditor(ResolveInfo ri) { + return ri.activityInfo.metaData != null && + ri.activityInfo.metaData.getBoolean( + IntentsActionPolicy.CATEGORY_INTERNAL_VIEWER, false); + } } diff --git a/src/com/cyanogenmod/explorer/ui/policy/IntentsActionPolicy.java b/src/com/cyanogenmod/explorer/ui/policy/IntentsActionPolicy.java index 607f0301..fb32eff9 100644 --- a/src/com/cyanogenmod/explorer/ui/policy/IntentsActionPolicy.java +++ b/src/com/cyanogenmod/explorer/ui/policy/IntentsActionPolicy.java @@ -23,19 +23,23 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Bundle; import android.util.Log; import android.widget.Toast; import com.cyanogenmod.explorer.R; import com.cyanogenmod.explorer.activities.ShortcutActivity; import com.cyanogenmod.explorer.model.FileSystemObject; +import com.cyanogenmod.explorer.model.RegularFile; import com.cyanogenmod.explorer.ui.dialogs.AssociationsDialog; import com.cyanogenmod.explorer.util.DialogHelper; import com.cyanogenmod.explorer.util.ExceptionUtil; import com.cyanogenmod.explorer.util.FileHelper; import com.cyanogenmod.explorer.util.MimeTypeHelper; +import com.cyanogenmod.explorer.util.MimeTypeHelper.MimeTypeCategory; import java.io.File; +import java.util.ArrayList; import java.util.List; /** @@ -48,6 +52,18 @@ public final class IntentsActionPolicy extends ActionsPolicy { private static boolean DEBUG = false; /** + * Category for all the internal CMExplorer viewers + */ + public static final String CATEGORY_INTERNAL_VIEWER = + "com.cyanogenmod.explorer.category.INTERNAL_VIEWER"; //$NON-NLS-1$ + + /** + * Category for all the CMExplorer editor + */ + public static final String CATEGORY_EDITOR = + "com.cyanogenmod.explorer.category.EDITOR"; //$NON-NLS-1$ + + /** * Method that opens a {@link FileSystemObject} with the default registered application * by the system, or ask the user for select a registered application. * @@ -79,6 +95,7 @@ public final class IntentsActionPolicy extends ActionsPolicy { ctx, intent, choose, + createInternalIntents(ctx, fso), R.drawable.ic_holo_light_open, R.string.associations_dialog_openwith_title, R.string.associations_dialog_openwith_action, @@ -115,6 +132,7 @@ public final class IntentsActionPolicy extends ActionsPolicy { ctx, intent, false, + null, R.drawable.ic_holo_light_send, R.string.associations_dialog_sendwith_title, R.string.associations_dialog_sendwith_action, @@ -132,6 +150,7 @@ public final class IntentsActionPolicy extends ActionsPolicy { * @param intent The intent to resolve * @param choose If allow the user to select the application to select the registered * application. If no preferred app or more than one exists the dialog is shown. + * @param internals The list of internals intents that can handle the action * @param icon The icon of the dialog * @param title The title of the dialog * @param action The button title of the dialog @@ -140,7 +159,7 @@ public final class IntentsActionPolicy extends ActionsPolicy { * @param onDismissListener The dismiss listener */ private static void resolveIntent( - Context ctx, Intent intent, boolean choose, + Context ctx, Intent intent, boolean choose, List<Intent> internals, int icon, int title, int action, boolean allowPreferred, OnCancelListener onCancelListener, OnDismissListener onDismissListener) { //Retrieve the activities that can handle the file @@ -152,6 +171,29 @@ public final class IntentsActionPolicy extends ActionsPolicy { packageManager. queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + // Add the internal editors + int count = 0; + if (internals != null) { + int cc = internals.size(); + for (int i = 0; i < cc; i++) { + List<ResolveInfo> ris = + packageManager. + queryIntentActivities(internals.get(i), 0); + if (ris.size() > 0) { + ResolveInfo ri = ris.get(0); + // Mark as internal + if (ri.activityInfo.metaData == null) { + ri.activityInfo.metaData = new Bundle(); + ri.activityInfo.metaData.putBoolean(CATEGORY_INTERNAL_VIEWER, true); + } + + // Only one result must be matched + info.add(count, ri); + count++; + } + } + } + // Retrieve the preferred activity that can handle the file final ResolveInfo mPreferredInfo = packageManager.resolveActivity(intent, 0); @@ -164,8 +206,13 @@ public final class IntentsActionPolicy extends ActionsPolicy { // Is a simple open and we have an application that can handle the file? if (!choose && ((mPreferredInfo != null && mPreferredInfo.match != 0) || info.size() == 1)) { - ctx.startActivity(intent); - return; + // But not if the only match is the an internal editor + ResolveInfo ri = info.get(0); + if (ri.activityInfo.metaData == null || + !ri.activityInfo.metaData.getBoolean(CATEGORY_INTERNAL_VIEWER, false)) { + ctx.startActivity(intent); + return; + } } // Otherwise, we have to show the open with dialog @@ -225,4 +272,41 @@ public final class IntentsActionPolicy extends ActionsPolicy { ctx, R.string.shortcut_creation_failed_msg, Toast.LENGTH_SHORT); } } + + /** + * This method creates a list of internal activities that could handle the fso. + * + * @param ctx The current context + * @param fso The file system object to open + */ + private static List<Intent> createInternalIntents(Context ctx, FileSystemObject fso) { + List<Intent> intents = new ArrayList<Intent>(); + intents.addAll(createEditorIntent(ctx, fso)); + return intents; + } + + /** + * This method creates a list of internal activities for editing files + * + * @param ctx The current context + * @param fso FileSystemObject + */ + private static List<Intent> createEditorIntent(Context ctx, FileSystemObject fso) { + List<Intent> intents = new ArrayList<Intent>(); + MimeTypeCategory category = MimeTypeHelper.getCategory(ctx, fso); + + //- Internal Editor. This editor can handle TEXT and NONE mime categories but + // not system files, directories, ..., only regular files (no symlinks) + if (fso instanceof RegularFile && + (category.compareTo(MimeTypeCategory.NONE) == 0 || + category.compareTo(MimeTypeCategory.TEXT) == 0)) { + Intent editorIntent = new Intent(); + editorIntent.setAction(Intent.ACTION_EDIT); + editorIntent.addCategory(CATEGORY_INTERNAL_VIEWER); + editorIntent.addCategory(CATEGORY_EDITOR); + intents.add(editorIntent); + } + + return intents; + } }
\ No newline at end of file diff --git a/src/com/cyanogenmod/explorer/util/CommandHelper.java b/src/com/cyanogenmod/explorer/util/CommandHelper.java index e4202f6b..6a704d02 100644 --- a/src/com/cyanogenmod/explorer/util/CommandHelper.java +++ b/src/com/cyanogenmod/explorer/util/CommandHelper.java @@ -1669,7 +1669,7 @@ public final class CommandHelper { } } - // If the needed unmount was executed + // If the needed unmount was executed return mountExecuted && leaveDeviceMounted; } |