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