blob: 9cc7c978afe72ae8a7df99982f35945731190609 [file] [log] [blame]
/*
* Copyright (C) 2016 Google Inc.
*
* 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.net.Uri;
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.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
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.facade.FacadeConfiguration;
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 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.
*
* @author Damon Kohler (damonkohler@gmail.com)
*/
public class ScriptManager extends ListActivity {
private final static String EMPTY = "";
private List<File> mScripts;
private ScriptManagerAdapter mAdapter;
private SharedPreferences mPreferences;
private HashMap<Integer, Interpreter> 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<File> 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<File>() {
@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<Integer, Interpreter>();
int i = MenuId.values().length + Menu.FIRST;
List<Interpreter> installed = mConfiguration.getInstalledInterpreters();
Collections.sort(installed, new Comparator<Interpreter>() {
@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<Integer, Interpreter> 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<File> getScriptList() {
return mScripts;
}
}
}