/* * Copyright (C) 2017 The Android Open Source 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.googlecode.android_scripting.activity; import android.app.AlertDialog; import android.app.ListActivity; import android.app.SearchManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.database.DataSetObserver; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.googlecode.android_scripting.ActivityFlinger; import com.googlecode.android_scripting.BaseApplication; import com.googlecode.android_scripting.Constants; import com.googlecode.android_scripting.FileUtils; import com.googlecode.android_scripting.IntentBuilders; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.R; import com.googlecode.android_scripting.ScriptListAdapter; import com.googlecode.android_scripting.ScriptStorageAdapter; import com.googlecode.android_scripting.interpreter.Interpreter; import com.googlecode.android_scripting.interpreter.InterpreterConfiguration; import com.googlecode.android_scripting.interpreter.InterpreterConfiguration.ConfigurationObserver; import com.googlecode.android_scripting.interpreter.InterpreterConstants; import com.googlecode.android_scripting.service.ScriptingLayerService; import java.io.File; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map.Entry; /** * Manages creation, deletion, and execution of stored scripts. * */ public class ScriptManager extends ListActivity { private final static String EMPTY = ""; private List mScripts; private ScriptManagerAdapter mAdapter; private SharedPreferences mPreferences; private HashMap mAddMenuIds; private ScriptListObserver mObserver; private InterpreterConfiguration mConfiguration; private SearchManager mManager; private boolean mInSearchResultMode = false; private String mQuery = EMPTY; private File mCurrentDir; private final File mBaseDir = new File(InterpreterConstants.SCRIPTS_ROOT); private final Handler mHandler = new Handler(); private File mCurrent; private static enum RequestCode { INSTALL_INTERPETER, QRCODE_ADD } private static enum MenuId { DELETE, HELP, FOLDER_ADD, QRCODE_ADD, INTERPRETER_MANAGER, PREFERENCES, LOGCAT_VIEWER, TRIGGER_MANAGER, REFRESH, SEARCH, RENAME, EXTERNAL; public int getId() { return ordinal() + Menu.FIRST; } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CustomizeWindow.requestCustomTitle(this, "Scripts", R.layout.script_manager); if (FileUtils.externalStorageMounted()) { File sl4a = mBaseDir.getParentFile(); if (!sl4a.exists()) { sl4a.mkdir(); try { FileUtils.chmod(sl4a, 0755); // Handle the sl4a parent folder first. } catch (Exception e) { // Not much we can do here if it doesn't work. } } if (!FileUtils.makeDirectories(mBaseDir, 0755)) { new AlertDialog.Builder(this) .setTitle("Error") .setMessage( "Failed to create scripts directory.\n" + mBaseDir + "\n" + "Please check the permissions of your external storage media.") .setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton("Ok", null).show(); } } else { new AlertDialog.Builder(this).setTitle("External Storage Unavilable") .setMessage("Scripts will be unavailable as long as external storage is unavailable.") .setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton("Ok", null).show(); } mCurrentDir = mBaseDir; mPreferences = PreferenceManager.getDefaultSharedPreferences(this); mAdapter = new ScriptManagerAdapter(this); mObserver = new ScriptListObserver(); mAdapter.registerDataSetObserver(mObserver); mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration(); mManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); registerForContextMenu(getListView()); updateAndFilterScriptList(mQuery); setListAdapter(mAdapter); ActivityFlinger.attachView(getListView(), this); ActivityFlinger.attachView(getWindow().getDecorView(), this); startService(IntentBuilders.buildTriggerServiceIntent()); handleIntent(getIntent()); } @Override protected void onNewIntent(Intent intent) { handleIntent(intent); } @SuppressWarnings("serial") private void updateAndFilterScriptList(final String query) { List scripts; if (mPreferences.getBoolean("show_all_files", false)) { scripts = ScriptStorageAdapter.listAllScripts(mCurrentDir); } else { scripts = ScriptStorageAdapter.listExecutableScripts(mCurrentDir, mConfiguration); } mScripts = Lists.newArrayList(Collections2.filter(scripts, new Predicate() { @Override public boolean apply(File file) { return file.getName().toLowerCase().contains(query.toLowerCase()); } })); // TODO(tturney): Add a text view that shows the queried text. synchronized (mQuery) { if (!mQuery.equals(query)) { if (query != null || !query.equals(EMPTY)) { mQuery = query; } } } if ((mScripts.size() == 0) && findViewById(android.R.id.empty) != null) { ((TextView) findViewById(android.R.id.empty)).setText("No matches found."); } // TODO(damonkohler): Extending the File class here seems odd. if (!mCurrentDir.equals(mBaseDir)) { mScripts.add(0, new File(mCurrentDir.getParent()) { @Override public boolean isDirectory() { return true; } @Override public String getName() { return ".."; } }); } } private void handleIntent(Intent intent) { if (Intent.ACTION_SEARCH.equals(intent.getAction())) { mInSearchResultMode = true; String query = intent.getStringExtra(SearchManager.QUERY); updateAndFilterScriptList(query); mAdapter.notifyDataSetChanged(); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { menu.add(Menu.NONE, MenuId.RENAME.getId(), Menu.NONE, "Rename"); menu.add(Menu.NONE, MenuId.DELETE.getId(), Menu.NONE, "Delete"); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info; try { info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); } catch (ClassCastException e) { Log.e("Bad menuInfo", e); return false; } File file = (File) mAdapter.getItem(info.position); int itemId = item.getItemId(); if (itemId == MenuId.DELETE.getId()) { delete(file); return true; } else if (itemId == MenuId.RENAME.getId()) { rename(file); return true; } return false; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && mInSearchResultMode) { mInSearchResultMode = false; mAdapter.notifyDataSetInvalidated(); return true; } return super.onKeyDown(keyCode, event); } @Override public void onStop() { super.onStop(); mConfiguration.unregisterObserver(mObserver); } @Override public void onStart() { super.onStart(); mConfiguration.registerObserver(mObserver); } @Override protected void onResume() { super.onResume(); if (!mInSearchResultMode && findViewById(android.R.id.empty) != null) { ((TextView) findViewById(android.R.id.empty)).setText(R.string.no_scripts_message); } updateAndFilterScriptList(mQuery); mAdapter.notifyDataSetChanged(); } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); menu.clear(); buildMenuIdMaps(); buildAddMenu(menu); buildSwitchActivityMenu(menu); menu.add(Menu.NONE, MenuId.SEARCH.getId(), Menu.NONE, "Search").setIcon( R.drawable.ic_menu_search); menu.add(Menu.NONE, MenuId.PREFERENCES.getId(), Menu.NONE, "Preferences").setIcon( android.R.drawable.ic_menu_preferences); menu.add(Menu.NONE, MenuId.REFRESH.getId(), Menu.NONE, "Refresh").setIcon( R.drawable.ic_menu_refresh); return true; } private void buildSwitchActivityMenu(Menu menu) { Menu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, Menu.NONE, "View").setIcon( android.R.drawable.ic_menu_more); subMenu.add(Menu.NONE, MenuId.INTERPRETER_MANAGER.getId(), Menu.NONE, "Interpreters"); subMenu.add(Menu.NONE, MenuId.TRIGGER_MANAGER.getId(), Menu.NONE, "Triggers"); subMenu.add(Menu.NONE, MenuId.LOGCAT_VIEWER.getId(), Menu.NONE, "Logcat"); } private void buildMenuIdMaps() { mAddMenuIds = new LinkedHashMap(); int i = MenuId.values().length + Menu.FIRST; List installed = mConfiguration.getInstalledInterpreters(); Collections.sort(installed, new Comparator() { @Override public int compare(Interpreter interpreterA, Interpreter interpreterB) { return interpreterA.getNiceName().compareTo(interpreterB.getNiceName()); } }); for (Interpreter interpreter : installed) { mAddMenuIds.put(i, interpreter); ++i; } } private void buildAddMenu(Menu menu) { Menu addMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, Menu.NONE, "Add").setIcon( android.R.drawable.ic_menu_add); addMenu.add(Menu.NONE, MenuId.FOLDER_ADD.getId(), Menu.NONE, "Folder"); for (Entry entry : mAddMenuIds.entrySet()) { addMenu.add(Menu.NONE, entry.getKey(), Menu.NONE, entry.getValue().getNiceName()); } addMenu.add(Menu.NONE, MenuId.QRCODE_ADD.getId(), Menu.NONE, "Scan Barcode"); } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == MenuId.INTERPRETER_MANAGER.getId()) { // Show interpreter manger. Intent i = new Intent(this, InterpreterManager.class); startActivity(i); } else if (mAddMenuIds.containsKey(itemId)) { // Add a new script. Intent intent = new Intent(Constants.ACTION_EDIT_SCRIPT); Interpreter interpreter = mAddMenuIds.get(itemId); intent.putExtra(Constants.EXTRA_SCRIPT_PATH, new File(mCurrentDir.getPath(), interpreter.getExtension()).getPath()); intent.putExtra(Constants.EXTRA_SCRIPT_CONTENT, interpreter.getContentTemplate()); intent.putExtra(Constants.EXTRA_IS_NEW_SCRIPT, true); startActivity(intent); synchronized (mQuery) { mQuery = EMPTY; } } else if (itemId == MenuId.QRCODE_ADD.getId()) { try { Intent intent = new Intent("com.google.zxing.client.android.SCAN"); startActivityForResult(intent, RequestCode.QRCODE_ADD.ordinal()); }catch(ActivityNotFoundException e) { Log.e("No handler found to Scan a QR Code!", e); } } else if (itemId == MenuId.FOLDER_ADD.getId()) { addFolder(); } else if (itemId == MenuId.PREFERENCES.getId()) { startActivity(new Intent(this, Preferences.class)); } else if (itemId == MenuId.TRIGGER_MANAGER.getId()) { startActivity(new Intent(this, TriggerManager.class)); } else if (itemId == MenuId.LOGCAT_VIEWER.getId()) { startActivity(new Intent(this, LogcatViewer.class)); } else if (itemId == MenuId.REFRESH.getId()) { updateAndFilterScriptList(mQuery); mAdapter.notifyDataSetChanged(); } else if (itemId == MenuId.SEARCH.getId()) { onSearchRequested(); } return true; } @Override protected void onListItemClick(ListView list, View view, int position, long id) { final File file = (File) list.getItemAtPosition(position); mCurrent = file; if (file.isDirectory()) { mCurrentDir = file; mAdapter.notifyDataSetInvalidated(); return; } doDialogMenu(); return; } // Quickedit chokes on sdk 3 or below, and some Android builds. Provides alternative menu. private void doDialogMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); final CharSequence[] menuList = { "Run Foreground", "Run Background", "Edit", "Delete", "Rename" }; builder.setTitle(mCurrent.getName()); builder.setItems(menuList, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent; switch (which) { case 0: intent = new Intent(ScriptManager.this, ScriptingLayerService.class); intent.setAction(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT); intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mCurrent.getPath()); startService(intent); break; case 1: intent = new Intent(ScriptManager.this, ScriptingLayerService.class); intent.setAction(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT); intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mCurrent.getPath()); startService(intent); break; case 2: editScript(mCurrent); break; case 3: delete(mCurrent); break; case 4: rename(mCurrent); break; } } }); builder.show(); } /** * Opens the script for editing. * * @param script * the name of the script to edit */ private void editScript(File script) { Intent i = new Intent(Constants.ACTION_EDIT_SCRIPT); i.putExtra(Constants.EXTRA_SCRIPT_PATH, script.getAbsolutePath()); startActivity(i); } private void delete(final File file) { AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("Delete"); alert.setMessage("Would you like to delete " + file.getName() + "?"); alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { FileUtils.delete(file); mScripts.remove(file); mAdapter.notifyDataSetChanged(); } }); alert.setNegativeButton("No", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // Ignore. } }); alert.show(); } private void addFolder() { final EditText folderName = new EditText(this); folderName.setHint("Folder Name"); AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("Add Folder"); alert.setView(folderName); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { String name = folderName.getText().toString(); if (name.length() == 0) { Log.e(ScriptManager.this, "Folder name is empty."); return; } else { for (File f : mScripts) { if (f.getName().equals(name)) { Log.e(ScriptManager.this, String.format("Folder \"%s\" already exists.", name)); return; } } } File dir = new File(mCurrentDir, name); if (!FileUtils.makeDirectories(dir, 0755)) { Log.e(ScriptManager.this, String.format("Cannot create folder \"%s\".", name)); } mAdapter.notifyDataSetInvalidated(); } }); alert.show(); } private void rename(final File file) { final EditText newName = new EditText(this); newName.setText(file.getName()); AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("Rename"); alert.setView(newName); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { String name = newName.getText().toString(); if (name.length() == 0) { Log.e(ScriptManager.this, "Name is empty."); return; } else { for (File f : mScripts) { if (f.getName().equals(name)) { Log.e(ScriptManager.this, String.format("\"%s\" already exists.", name)); return; } } } if (!FileUtils.rename(file, name)) { throw new RuntimeException(String.format("Cannot rename \"%s\".", file.getPath())); } mAdapter.notifyDataSetInvalidated(); } }); alert.show(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { RequestCode request = RequestCode.values()[requestCode]; if (resultCode == RESULT_OK) { switch (request) { case QRCODE_ADD: writeScriptFromBarcode(data); break; default: break; } } else { switch (request) { case QRCODE_ADD: break; default: break; } } mAdapter.notifyDataSetInvalidated(); } private void writeScriptFromBarcode(Intent data) { String result = data.getStringExtra("SCAN_RESULT"); if (result == null) { Log.e(this, "Invalid QR code content."); return; } String contents[] = result.split("\n", 2); if (contents.length != 2) { Log.e(this, "Invalid QR code content."); return; } String title = contents[0]; String body = contents[1]; File script = new File(mCurrentDir, title); ScriptStorageAdapter.writeScript(script, body); } @Override public void onDestroy() { super.onDestroy(); mConfiguration.unregisterObserver(mObserver); mManager.setOnCancelListener(null); } private class ScriptListObserver extends DataSetObserver implements ConfigurationObserver { @Override public void onInvalidated() { updateAndFilterScriptList(EMPTY); } @Override public void onConfigurationChanged() { runOnUiThread(new Runnable() { @Override public void run() { updateAndFilterScriptList(mQuery); mAdapter.notifyDataSetChanged(); } }); } } private class ScriptManagerAdapter extends ScriptListAdapter { public ScriptManagerAdapter(Context context) { super(context); } @Override protected List getScriptList() { return mScripts; } } }