• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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