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