Home | History | Annotate | Download | only in activity
      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;
     97     public int getId() {
     98       return ordinal() + Menu.FIRST;
     99     }
    100   }
    101 
    102   @Override
    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
    148   protected void onNewIntent(Intent intent) {
    149     handleIntent(intent);
    150   }
    151 
    152   @SuppressWarnings("serial")
    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 
    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
    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
    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
    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
    243   public void onStop() {
    244     super.onStop();
    245     mConfiguration.unregisterObserver(mObserver);
    246   }
    247 
    248   @Override
    249   public void onStart() {
    250     super.onStart();
    251     mConfiguration.registerObserver(mObserver);
    252   }
    253 
    254   @Override
    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
    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 
    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 
    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 
    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
    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
    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.
    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    */
    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 
    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 
    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 
    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
    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 
    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
    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
    549     public void onInvalidated() {
    550       updateAndFilterScriptList(EMPTY);
    551     }
    552 
    553     @Override
    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 {
    566     public ScriptManagerAdapter(Context context) {
    567       super(context);
    568     }
    569 
    570     @Override
    571     protected List<File> getScriptList() {
    572       return mScripts;
    573     }
    574   }
    575 }
    576