1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.googlecode.android_scripting.activity; 18 19 import android.app.AlertDialog; 20 import android.app.ListActivity; 21 import android.app.SearchManager; 22 import android.content.ActivityNotFoundException; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.database.DataSetObserver; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.preference.PreferenceManager; 31 import android.view.ContextMenu; 32 import android.view.ContextMenu.ContextMenuInfo; 33 import android.view.KeyEvent; 34 import android.view.Menu; 35 import android.view.MenuItem; 36 import android.view.View; 37 import android.widget.AdapterView; 38 import android.widget.EditText; 39 import android.widget.ListView; 40 import android.widget.TextView; 41 42 import com.google.common.base.Predicate; 43 import com.google.common.collect.Collections2; 44 import com.google.common.collect.Lists; 45 import com.googlecode.android_scripting.ActivityFlinger; 46 import com.googlecode.android_scripting.BaseApplication; 47 import com.googlecode.android_scripting.Constants; 48 import com.googlecode.android_scripting.FileUtils; 49 import com.googlecode.android_scripting.IntentBuilders; 50 import com.googlecode.android_scripting.Log; 51 import com.googlecode.android_scripting.R; 52 import com.googlecode.android_scripting.ScriptListAdapter; 53 import com.googlecode.android_scripting.ScriptStorageAdapter; 54 import com.googlecode.android_scripting.interpreter.Interpreter; 55 import com.googlecode.android_scripting.interpreter.InterpreterConfiguration; 56 import com.googlecode.android_scripting.interpreter.InterpreterConfiguration.ConfigurationObserver; 57 import com.googlecode.android_scripting.interpreter.InterpreterConstants; 58 import com.googlecode.android_scripting.service.ScriptingLayerService; 59 60 import java.io.File; 61 import java.util.Collections; 62 import java.util.Comparator; 63 import java.util.HashMap; 64 import java.util.LinkedHashMap; 65 import java.util.List; 66 import java.util.Map.Entry; 67 68 /** 69 * Manages creation, deletion, and execution of stored scripts. 70 * 71 */ 72 public class ScriptManager extends ListActivity { 73 74 private final static String EMPTY = ""; 75 76 private List<File> mScripts; 77 private ScriptManagerAdapter mAdapter; 78 private SharedPreferences mPreferences; 79 private HashMap<Integer, Interpreter> mAddMenuIds; 80 private ScriptListObserver mObserver; 81 private InterpreterConfiguration mConfiguration; 82 private SearchManager mManager; 83 private boolean mInSearchResultMode = false; 84 private String mQuery = EMPTY; 85 private File mCurrentDir; 86 private final File mBaseDir = new File(InterpreterConstants.SCRIPTS_ROOT); 87 private final Handler mHandler = new Handler(); 88 private File mCurrent; 89 90 private static enum RequestCode { 91 INSTALL_INTERPETER, QRCODE_ADD 92 } 93 94 private static enum MenuId { 95 DELETE, HELP, FOLDER_ADD, QRCODE_ADD, INTERPRETER_MANAGER, PREFERENCES, LOGCAT_VIEWER, 96 TRIGGER_MANAGER, REFRESH, SEARCH, RENAME, EXTERNAL; getId()97 public int getId() { 98 return ordinal() + Menu.FIRST; 99 } 100 } 101 102 @Override onCreate(Bundle savedInstanceState)103 public void onCreate(Bundle savedInstanceState) { 104 super.onCreate(savedInstanceState); 105 CustomizeWindow.requestCustomTitle(this, "Scripts", R.layout.script_manager); 106 if (FileUtils.externalStorageMounted()) { 107 File sl4a = mBaseDir.getParentFile(); 108 if (!sl4a.exists()) { 109 sl4a.mkdir(); 110 try { 111 FileUtils.chmod(sl4a, 0755); // Handle the sl4a parent folder first. 112 } catch (Exception e) { 113 // Not much we can do here if it doesn't work. 114 } 115 } 116 if (!FileUtils.makeDirectories(mBaseDir, 0755)) { 117 new AlertDialog.Builder(this) 118 .setTitle("Error") 119 .setMessage( 120 "Failed to create scripts directory.\n" + mBaseDir + "\n" 121 + "Please check the permissions of your external storage media.") 122 .setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton("Ok", null).show(); 123 } 124 } else { 125 new AlertDialog.Builder(this).setTitle("External Storage Unavilable") 126 .setMessage("Scripts will be unavailable as long as external storage is unavailable.") 127 .setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton("Ok", null).show(); 128 } 129 130 mCurrentDir = mBaseDir; 131 mPreferences = PreferenceManager.getDefaultSharedPreferences(this); 132 mAdapter = new ScriptManagerAdapter(this); 133 mObserver = new ScriptListObserver(); 134 mAdapter.registerDataSetObserver(mObserver); 135 mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration(); 136 mManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); 137 138 registerForContextMenu(getListView()); 139 updateAndFilterScriptList(mQuery); 140 setListAdapter(mAdapter); 141 ActivityFlinger.attachView(getListView(), this); 142 ActivityFlinger.attachView(getWindow().getDecorView(), this); 143 startService(IntentBuilders.buildTriggerServiceIntent()); 144 handleIntent(getIntent()); 145 } 146 147 @Override onNewIntent(Intent intent)148 protected void onNewIntent(Intent intent) { 149 handleIntent(intent); 150 } 151 152 @SuppressWarnings("serial") updateAndFilterScriptList(final String query)153 private void updateAndFilterScriptList(final String query) { 154 List<File> scripts; 155 if (mPreferences.getBoolean("show_all_files", false)) { 156 scripts = ScriptStorageAdapter.listAllScripts(mCurrentDir); 157 } else { 158 scripts = ScriptStorageAdapter.listExecutableScripts(mCurrentDir, mConfiguration); 159 } 160 mScripts = Lists.newArrayList(Collections2.filter(scripts, new Predicate<File>() { 161 @Override 162 public boolean apply(File file) { 163 return file.getName().toLowerCase().contains(query.toLowerCase()); 164 } 165 })); 166 167 // TODO(tturney): Add a text view that shows the queried text. 168 synchronized (mQuery) { 169 if (!mQuery.equals(query)) { 170 if (query != null || !query.equals(EMPTY)) { 171 mQuery = query; 172 } 173 } 174 } 175 176 if ((mScripts.size() == 0) && findViewById(android.R.id.empty) != null) { 177 ((TextView) findViewById(android.R.id.empty)).setText("No matches found."); 178 } 179 180 // TODO(damonkohler): Extending the File class here seems odd. 181 if (!mCurrentDir.equals(mBaseDir)) { 182 mScripts.add(0, new File(mCurrentDir.getParent()) { 183 @Override 184 public boolean isDirectory() { 185 return true; 186 } 187 188 @Override 189 public String getName() { 190 return ".."; 191 } 192 }); 193 } 194 } 195 handleIntent(Intent intent)196 private void handleIntent(Intent intent) { 197 if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 198 mInSearchResultMode = true; 199 String query = intent.getStringExtra(SearchManager.QUERY); 200 updateAndFilterScriptList(query); 201 mAdapter.notifyDataSetChanged(); 202 } 203 } 204 205 @Override onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)206 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 207 menu.add(Menu.NONE, MenuId.RENAME.getId(), Menu.NONE, "Rename"); 208 menu.add(Menu.NONE, MenuId.DELETE.getId(), Menu.NONE, "Delete"); 209 } 210 211 @Override onContextItemSelected(MenuItem item)212 public boolean onContextItemSelected(MenuItem item) { 213 AdapterView.AdapterContextMenuInfo info; 214 try { 215 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 216 } catch (ClassCastException e) { 217 Log.e("Bad menuInfo", e); 218 return false; 219 } 220 File file = (File) mAdapter.getItem(info.position); 221 int itemId = item.getItemId(); 222 if (itemId == MenuId.DELETE.getId()) { 223 delete(file); 224 return true; 225 } else if (itemId == MenuId.RENAME.getId()) { 226 rename(file); 227 return true; 228 } 229 return false; 230 } 231 232 @Override onKeyDown(int keyCode, KeyEvent event)233 public boolean onKeyDown(int keyCode, KeyEvent event) { 234 if (keyCode == KeyEvent.KEYCODE_BACK && mInSearchResultMode) { 235 mInSearchResultMode = false; 236 mAdapter.notifyDataSetInvalidated(); 237 return true; 238 } 239 return super.onKeyDown(keyCode, event); 240 } 241 242 @Override onStop()243 public void onStop() { 244 super.onStop(); 245 mConfiguration.unregisterObserver(mObserver); 246 } 247 248 @Override onStart()249 public void onStart() { 250 super.onStart(); 251 mConfiguration.registerObserver(mObserver); 252 } 253 254 @Override onResume()255 protected void onResume() { 256 super.onResume(); 257 if (!mInSearchResultMode && findViewById(android.R.id.empty) != null) { 258 ((TextView) findViewById(android.R.id.empty)).setText(R.string.no_scripts_message); 259 } 260 updateAndFilterScriptList(mQuery); 261 mAdapter.notifyDataSetChanged(); 262 } 263 264 @Override onPrepareOptionsMenu(Menu menu)265 public boolean onPrepareOptionsMenu(Menu menu) { 266 super.onPrepareOptionsMenu(menu); 267 menu.clear(); 268 buildMenuIdMaps(); 269 buildAddMenu(menu); 270 buildSwitchActivityMenu(menu); 271 menu.add(Menu.NONE, MenuId.SEARCH.getId(), Menu.NONE, "Search").setIcon( 272 R.drawable.ic_menu_search); 273 menu.add(Menu.NONE, MenuId.PREFERENCES.getId(), Menu.NONE, "Preferences").setIcon( 274 android.R.drawable.ic_menu_preferences); 275 menu.add(Menu.NONE, MenuId.REFRESH.getId(), Menu.NONE, "Refresh").setIcon( 276 R.drawable.ic_menu_refresh); 277 return true; 278 } 279 buildSwitchActivityMenu(Menu menu)280 private void buildSwitchActivityMenu(Menu menu) { 281 Menu subMenu = 282 menu.addSubMenu(Menu.NONE, Menu.NONE, Menu.NONE, "View").setIcon( 283 android.R.drawable.ic_menu_more); 284 subMenu.add(Menu.NONE, MenuId.INTERPRETER_MANAGER.getId(), Menu.NONE, "Interpreters"); 285 subMenu.add(Menu.NONE, MenuId.TRIGGER_MANAGER.getId(), Menu.NONE, "Triggers"); 286 subMenu.add(Menu.NONE, MenuId.LOGCAT_VIEWER.getId(), Menu.NONE, "Logcat"); 287 } 288 buildMenuIdMaps()289 private void buildMenuIdMaps() { 290 mAddMenuIds = new LinkedHashMap<Integer, Interpreter>(); 291 int i = MenuId.values().length + Menu.FIRST; 292 List<Interpreter> installed = mConfiguration.getInstalledInterpreters(); 293 Collections.sort(installed, new Comparator<Interpreter>() { 294 @Override 295 public int compare(Interpreter interpreterA, Interpreter interpreterB) { 296 return interpreterA.getNiceName().compareTo(interpreterB.getNiceName()); 297 } 298 }); 299 for (Interpreter interpreter : installed) { 300 mAddMenuIds.put(i, interpreter); 301 ++i; 302 } 303 } 304 buildAddMenu(Menu menu)305 private void buildAddMenu(Menu menu) { 306 Menu addMenu = 307 menu.addSubMenu(Menu.NONE, Menu.NONE, Menu.NONE, "Add").setIcon( 308 android.R.drawable.ic_menu_add); 309 addMenu.add(Menu.NONE, MenuId.FOLDER_ADD.getId(), Menu.NONE, "Folder"); 310 for (Entry<Integer, Interpreter> entry : mAddMenuIds.entrySet()) { 311 addMenu.add(Menu.NONE, entry.getKey(), Menu.NONE, entry.getValue().getNiceName()); 312 } 313 addMenu.add(Menu.NONE, MenuId.QRCODE_ADD.getId(), Menu.NONE, "Scan Barcode"); 314 } 315 316 @Override onOptionsItemSelected(MenuItem item)317 public boolean onOptionsItemSelected(MenuItem item) { 318 int itemId = item.getItemId(); 319 if (itemId == MenuId.INTERPRETER_MANAGER.getId()) { 320 // Show interpreter manger. 321 Intent i = new Intent(this, InterpreterManager.class); 322 startActivity(i); 323 } else if (mAddMenuIds.containsKey(itemId)) { 324 // Add a new script. 325 Intent intent = new Intent(Constants.ACTION_EDIT_SCRIPT); 326 Interpreter interpreter = mAddMenuIds.get(itemId); 327 intent.putExtra(Constants.EXTRA_SCRIPT_PATH, 328 new File(mCurrentDir.getPath(), interpreter.getExtension()).getPath()); 329 intent.putExtra(Constants.EXTRA_SCRIPT_CONTENT, interpreter.getContentTemplate()); 330 intent.putExtra(Constants.EXTRA_IS_NEW_SCRIPT, true); 331 startActivity(intent); 332 synchronized (mQuery) { 333 mQuery = EMPTY; 334 } 335 } else if (itemId == MenuId.QRCODE_ADD.getId()) { 336 try { 337 Intent intent = new Intent("com.google.zxing.client.android.SCAN"); 338 startActivityForResult(intent, RequestCode.QRCODE_ADD.ordinal()); 339 }catch(ActivityNotFoundException e) { 340 Log.e("No handler found to Scan a QR Code!", e); 341 } 342 } else if (itemId == MenuId.FOLDER_ADD.getId()) { 343 addFolder(); 344 } else if (itemId == MenuId.PREFERENCES.getId()) { 345 startActivity(new Intent(this, Preferences.class)); 346 } else if (itemId == MenuId.TRIGGER_MANAGER.getId()) { 347 startActivity(new Intent(this, TriggerManager.class)); 348 } else if (itemId == MenuId.LOGCAT_VIEWER.getId()) { 349 startActivity(new Intent(this, LogcatViewer.class)); 350 } else if (itemId == MenuId.REFRESH.getId()) { 351 updateAndFilterScriptList(mQuery); 352 mAdapter.notifyDataSetChanged(); 353 } else if (itemId == MenuId.SEARCH.getId()) { 354 onSearchRequested(); 355 } 356 return true; 357 } 358 359 @Override onListItemClick(ListView list, View view, int position, long id)360 protected void onListItemClick(ListView list, View view, int position, long id) { 361 final File file = (File) list.getItemAtPosition(position); 362 mCurrent = file; 363 if (file.isDirectory()) { 364 mCurrentDir = file; 365 mAdapter.notifyDataSetInvalidated(); 366 return; 367 } 368 doDialogMenu(); 369 return; 370 } 371 372 // Quickedit chokes on sdk 3 or below, and some Android builds. Provides alternative menu. doDialogMenu()373 private void doDialogMenu() { 374 AlertDialog.Builder builder = new AlertDialog.Builder(this); 375 final CharSequence[] menuList = 376 { "Run Foreground", "Run Background", "Edit", "Delete", "Rename" }; 377 builder.setTitle(mCurrent.getName()); 378 builder.setItems(menuList, new DialogInterface.OnClickListener() { 379 380 @Override 381 public void onClick(DialogInterface dialog, int which) { 382 Intent intent; 383 switch (which) { 384 case 0: 385 intent = new Intent(ScriptManager.this, ScriptingLayerService.class); 386 intent.setAction(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT); 387 intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mCurrent.getPath()); 388 startService(intent); 389 break; 390 case 1: 391 intent = new Intent(ScriptManager.this, ScriptingLayerService.class); 392 intent.setAction(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT); 393 intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mCurrent.getPath()); 394 startService(intent); 395 break; 396 case 2: 397 editScript(mCurrent); 398 break; 399 case 3: 400 delete(mCurrent); 401 break; 402 case 4: 403 rename(mCurrent); 404 break; 405 } 406 } 407 }); 408 builder.show(); 409 } 410 411 /** 412 * Opens the script for editing. 413 * 414 * @param script 415 * the name of the script to edit 416 */ editScript(File script)417 private void editScript(File script) { 418 Intent i = new Intent(Constants.ACTION_EDIT_SCRIPT); 419 i.putExtra(Constants.EXTRA_SCRIPT_PATH, script.getAbsolutePath()); 420 startActivity(i); 421 } 422 delete(final File file)423 private void delete(final File file) { 424 AlertDialog.Builder alert = new AlertDialog.Builder(this); 425 alert.setTitle("Delete"); 426 alert.setMessage("Would you like to delete " + file.getName() + "?"); 427 alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() { 428 public void onClick(DialogInterface dialog, int whichButton) { 429 FileUtils.delete(file); 430 mScripts.remove(file); 431 mAdapter.notifyDataSetChanged(); 432 } 433 }); 434 alert.setNegativeButton("No", new DialogInterface.OnClickListener() { 435 public void onClick(DialogInterface dialog, int whichButton) { 436 // Ignore. 437 } 438 }); 439 alert.show(); 440 } 441 addFolder()442 private void addFolder() { 443 final EditText folderName = new EditText(this); 444 folderName.setHint("Folder Name"); 445 AlertDialog.Builder alert = new AlertDialog.Builder(this); 446 alert.setTitle("Add Folder"); 447 alert.setView(folderName); 448 alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { 449 public void onClick(DialogInterface dialog, int whichButton) { 450 String name = folderName.getText().toString(); 451 if (name.length() == 0) { 452 Log.e(ScriptManager.this, "Folder name is empty."); 453 return; 454 } else { 455 for (File f : mScripts) { 456 if (f.getName().equals(name)) { 457 Log.e(ScriptManager.this, String.format("Folder \"%s\" already exists.", name)); 458 return; 459 } 460 } 461 } 462 File dir = new File(mCurrentDir, name); 463 if (!FileUtils.makeDirectories(dir, 0755)) { 464 Log.e(ScriptManager.this, String.format("Cannot create folder \"%s\".", name)); 465 } 466 mAdapter.notifyDataSetInvalidated(); 467 } 468 }); 469 alert.show(); 470 } 471 rename(final File file)472 private void rename(final File file) { 473 final EditText newName = new EditText(this); 474 newName.setText(file.getName()); 475 AlertDialog.Builder alert = new AlertDialog.Builder(this); 476 alert.setTitle("Rename"); 477 alert.setView(newName); 478 alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { 479 public void onClick(DialogInterface dialog, int whichButton) { 480 String name = newName.getText().toString(); 481 if (name.length() == 0) { 482 Log.e(ScriptManager.this, "Name is empty."); 483 return; 484 } else { 485 for (File f : mScripts) { 486 if (f.getName().equals(name)) { 487 Log.e(ScriptManager.this, String.format("\"%s\" already exists.", name)); 488 return; 489 } 490 } 491 } 492 if (!FileUtils.rename(file, name)) { 493 throw new RuntimeException(String.format("Cannot rename \"%s\".", file.getPath())); 494 } 495 mAdapter.notifyDataSetInvalidated(); 496 } 497 }); 498 alert.show(); 499 } 500 501 @Override onActivityResult(int requestCode, int resultCode, Intent data)502 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 503 RequestCode request = RequestCode.values()[requestCode]; 504 if (resultCode == RESULT_OK) { 505 switch (request) { 506 case QRCODE_ADD: 507 writeScriptFromBarcode(data); 508 break; 509 default: 510 break; 511 } 512 } else { 513 switch (request) { 514 case QRCODE_ADD: 515 break; 516 default: 517 break; 518 } 519 } 520 mAdapter.notifyDataSetInvalidated(); 521 } 522 writeScriptFromBarcode(Intent data)523 private void writeScriptFromBarcode(Intent data) { 524 String result = data.getStringExtra("SCAN_RESULT"); 525 if (result == null) { 526 Log.e(this, "Invalid QR code content."); 527 return; 528 } 529 String contents[] = result.split("\n", 2); 530 if (contents.length != 2) { 531 Log.e(this, "Invalid QR code content."); 532 return; 533 } 534 String title = contents[0]; 535 String body = contents[1]; 536 File script = new File(mCurrentDir, title); 537 ScriptStorageAdapter.writeScript(script, body); 538 } 539 540 @Override onDestroy()541 public void onDestroy() { 542 super.onDestroy(); 543 mConfiguration.unregisterObserver(mObserver); 544 mManager.setOnCancelListener(null); 545 } 546 547 private class ScriptListObserver extends DataSetObserver implements ConfigurationObserver { 548 @Override onInvalidated()549 public void onInvalidated() { 550 updateAndFilterScriptList(EMPTY); 551 } 552 553 @Override onConfigurationChanged()554 public void onConfigurationChanged() { 555 runOnUiThread(new Runnable() { 556 @Override 557 public void run() { 558 updateAndFilterScriptList(mQuery); 559 mAdapter.notifyDataSetChanged(); 560 } 561 }); 562 } 563 } 564 565 private class ScriptManagerAdapter extends ScriptListAdapter { ScriptManagerAdapter(Context context)566 public ScriptManagerAdapter(Context context) { 567 super(context); 568 } 569 570 @Override getScriptList()571 protected List<File> getScriptList() { 572 return mScripts; 573 } 574 } 575 } 576