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
17package com.googlecode.android_scripting.activity;
18
19import android.app.ListActivity;
20import android.app.SearchManager;
21import android.content.Intent;
22import android.database.MatrixCursor;
23import android.os.Bundle;
24import android.util.TypedValue;
25import android.view.ContextMenu;
26import android.view.KeyEvent;
27import android.view.Menu;
28import android.view.MenuItem;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.ContextMenu.ContextMenuInfo;
32import android.widget.AdapterView;
33import android.widget.AlphabetIndexer;
34import android.widget.BaseAdapter;
35import android.widget.ListView;
36import android.widget.SectionIndexer;
37import android.widget.TextView;
38
39import com.google.common.base.Predicate;
40import com.google.common.collect.Collections2;
41import com.google.common.collect.Lists;
42import com.googlecode.android_scripting.BaseApplication;
43import com.googlecode.android_scripting.Constants;
44import com.googlecode.android_scripting.Log;
45import com.googlecode.android_scripting.R;
46import com.googlecode.android_scripting.facade.FacadeConfiguration;
47import com.googlecode.android_scripting.interpreter.Interpreter;
48import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
49import com.googlecode.android_scripting.language.SupportedLanguages;
50import com.googlecode.android_scripting.rpc.MethodDescriptor;
51import com.googlecode.android_scripting.rpc.ParameterDescriptor;
52import com.googlecode.android_scripting.rpc.RpcDeprecated;
53import com.googlecode.android_scripting.rpc.RpcMinSdk;
54
55import java.lang.reflect.Method;
56import java.util.HashSet;
57import java.util.List;
58import java.util.Set;
59
60public class ApiBrowser extends ListActivity {
61
62  private boolean searchResultMode = false;
63
64  private static enum RequestCode {
65    RPC_PROMPT
66  }
67
68  private static enum MenuId {
69    EXPAND_ALL, COLLAPSE_ALL, SEARCH;
70    public int getId() {
71      return ordinal() + Menu.FIRST;
72    }
73  }
74
75  private static enum ContextMenuId {
76    INSERT_TEXT, PROMPT_PARAMETERS, HELP;
77    public int getId() {
78      return ordinal() + Menu.FIRST;
79    }
80  }
81
82  private List<MethodDescriptor> mMethodDescriptors;
83  private Set<Integer> mExpandedPositions;
84  private ApiBrowserAdapter mAdapter;
85  private boolean mIsLanguageSupported;
86
87  @Override
88  public void onCreate(Bundle savedInstanceState) {
89    super.onCreate(savedInstanceState);
90    CustomizeWindow.requestCustomTitle(this, "API Browser", R.layout.api_browser);
91    getListView().setFastScrollEnabled(true);
92    mExpandedPositions = new HashSet<Integer>();
93    updateAndFilterMethodDescriptors(null);
94    String scriptName = getIntent().getStringExtra(Constants.EXTRA_SCRIPT_PATH);
95    mIsLanguageSupported = SupportedLanguages.checkLanguageSupported(scriptName);
96    mAdapter = new ApiBrowserAdapter();
97    setListAdapter(mAdapter);
98    registerForContextMenu(getListView());
99    setResult(RESULT_CANCELED);
100  }
101
102  private void updateAndFilterMethodDescriptors(final String query) {
103    mMethodDescriptors =
104        Lists.newArrayList(Collections2.filter(FacadeConfiguration.collectMethodDescriptors(),
105            new Predicate<MethodDescriptor>() {
106              @Override
107              public boolean apply(MethodDescriptor descriptor) {
108                Method method = descriptor.getMethod();
109                if (method.isAnnotationPresent(RpcDeprecated.class)) {
110                  return false;
111                } else if (method.isAnnotationPresent(RpcMinSdk.class)) {
112                  int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value();
113                  if (FacadeConfiguration.getSdkLevel() < requiredSdkLevel) {
114                    return false;
115                  }
116                }
117                if (query == null) {
118                  return true;
119                }
120                return descriptor.getName().toLowerCase().contains(query.toLowerCase());
121              }
122            }));
123  }
124
125  @Override
126  protected void onNewIntent(Intent intent) {
127    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
128      searchResultMode = true;
129      final String query = intent.getStringExtra(SearchManager.QUERY);
130      ((TextView) findViewById(R.id.left_text)).setText(query);
131      updateAndFilterMethodDescriptors(query);
132      if (mMethodDescriptors.size() == 1) {
133        mExpandedPositions.add(0);
134      } else {
135        mExpandedPositions.clear();
136      }
137      mAdapter.notifyDataSetChanged();
138    }
139  }
140
141  @Override
142  public boolean onKeyDown(int keyCode, KeyEvent event) {
143    if (keyCode == KeyEvent.KEYCODE_BACK && searchResultMode) {
144      searchResultMode = false;
145      mExpandedPositions.clear();
146      ((TextView) findViewById(R.id.left_text)).setText("API Browser");
147      updateAndFilterMethodDescriptors("");
148      mAdapter.notifyDataSetChanged();
149      return true;
150    }
151    return super.onKeyDown(keyCode, event);
152  }
153
154  @Override
155  public boolean onPrepareOptionsMenu(Menu menu) {
156    super.onPrepareOptionsMenu(menu);
157    menu.clear();
158    menu.add(Menu.NONE, MenuId.EXPAND_ALL.getId(), Menu.NONE, "Expand All").setIcon(
159        android.R.drawable.ic_menu_add);
160    menu.add(Menu.NONE, MenuId.COLLAPSE_ALL.getId(), Menu.NONE, "Collapse All").setIcon(
161        android.R.drawable.ic_menu_close_clear_cancel);
162    menu.add(Menu.NONE, MenuId.SEARCH.getId(), Menu.NONE, "Search").setIcon(
163        R.drawable.ic_menu_search);
164    return true;
165  }
166
167  @Override
168  public boolean onOptionsItemSelected(MenuItem item) {
169    super.onOptionsItemSelected(item);
170    int itemId = item.getItemId();
171    if (itemId == MenuId.EXPAND_ALL.getId()) {
172      for (int i = 0; i < mMethodDescriptors.size(); i++) {
173        mExpandedPositions.add(i);
174      }
175    } else if (itemId == MenuId.COLLAPSE_ALL.getId()) {
176      mExpandedPositions.clear();
177    } else if (itemId == MenuId.SEARCH.getId()) {
178      onSearchRequested();
179    }
180
181    mAdapter.notifyDataSetInvalidated();
182    return true;
183  }
184
185  @Override
186  protected void onListItemClick(ListView l, View v, int position, long id) {
187    if (mExpandedPositions.contains(position)) {
188      mExpandedPositions.remove(position);
189    } else {
190      mExpandedPositions.add(position);
191    }
192    mAdapter.notifyDataSetInvalidated();
193  }
194
195  @Override
196  public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
197    if (!mIsLanguageSupported) {
198      return;
199    }
200    menu.add(Menu.NONE, ContextMenuId.INSERT_TEXT.getId(), Menu.NONE, "Insert");
201    menu.add(Menu.NONE, ContextMenuId.PROMPT_PARAMETERS.getId(), Menu.NONE, "Prompt");
202  }
203
204  @Override
205  public boolean onContextItemSelected(MenuItem item) {
206    AdapterView.AdapterContextMenuInfo info;
207    try {
208      info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
209    } catch (ClassCastException e) {
210      Log.e("Bad menuInfo", e);
211      return false;
212    }
213
214    MethodDescriptor rpc = (MethodDescriptor) getListAdapter().getItem(info.position);
215    if (rpc == null) {
216      Log.v("No RPC selected.");
217      return false;
218    }
219
220    if (item.getItemId() == ContextMenuId.INSERT_TEXT.getId()) {
221      // There's no activity to track calls to insert (like there is for prompt) so we track it
222      // here instead.
223      insertText(rpc, new String[0]);
224    } else if (item.getItemId() == ContextMenuId.PROMPT_PARAMETERS.getId()) {
225      Intent intent = new Intent(this, ApiPrompt.class);
226      intent.putExtra(Constants.EXTRA_API_PROMPT_RPC_NAME, rpc.getName());
227      ParameterDescriptor[] parameters = rpc.getParameterValues(new String[0]);
228      String[] values = new String[parameters.length];
229      int index = 0;
230      for (ParameterDescriptor parameter : parameters) {
231        values[index++] = parameter.getValue();
232      }
233      intent.putExtra(Constants.EXTRA_API_PROMPT_VALUES, values);
234      startActivityForResult(intent, RequestCode.RPC_PROMPT.ordinal());
235    } else if (item.getItemId() == ContextMenuId.HELP.getId()) {
236      String help = rpc.getDeclaringClass().getSimpleName() + ".html#" + rpc.getName();
237
238    }
239    return true;
240  }
241
242  @Override
243  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
244    super.onActivityResult(requestCode, resultCode, data);
245    RequestCode request = RequestCode.values()[requestCode];
246    if (resultCode == RESULT_OK) {
247      switch (request) {
248      case RPC_PROMPT:
249        MethodDescriptor rpc =
250            FacadeConfiguration.getMethodDescriptor(data
251                .getStringExtra(Constants.EXTRA_API_PROMPT_RPC_NAME));
252        String[] values = data.getStringArrayExtra(Constants.EXTRA_API_PROMPT_VALUES);
253        insertText(rpc, values);
254        break;
255      default:
256        break;
257      }
258    } else {
259      switch (request) {
260      case RPC_PROMPT:
261        break;
262      default:
263        break;
264      }
265    }
266  }
267
268  private void insertText(MethodDescriptor rpc, String[] values) {
269    String scriptText = getIntent().getStringExtra(Constants.EXTRA_SCRIPT_TEXT);
270    InterpreterConfiguration config =
271        ((BaseApplication) getApplication()).getInterpreterConfiguration();
272
273    Interpreter interpreter =
274        config.getInterpreterByName(getIntent().getStringExtra(Constants.EXTRA_INTERPRETER_NAME));
275    String rpcHelpText = interpreter.getRpcText(scriptText, rpc, values);
276
277    Intent intent = new Intent();
278    intent.putExtra(Constants.EXTRA_RPC_HELP_TEXT, rpcHelpText);
279    setResult(RESULT_OK, intent);
280    finish();
281  }
282
283  private class ApiBrowserAdapter extends BaseAdapter implements SectionIndexer {
284
285    private final AlphabetIndexer mIndexer;
286    private final MatrixCursor mCursor;
287
288    public ApiBrowserAdapter() {
289      mCursor = new MatrixCursor(new String[] { "NAME" });
290      for (MethodDescriptor info : mMethodDescriptors) {
291        mCursor.addRow(new String[] { info.getName() });
292      }
293      mIndexer = new AlphabetIndexer(mCursor, 0, " ABCDEFGHIJKLMNOPQRSTUVWXYZ");
294    }
295
296    @Override
297    public int getCount() {
298      return mMethodDescriptors.size();
299    }
300
301    @Override
302    public Object getItem(int position) {
303      return mMethodDescriptors.get(position);
304    }
305
306    @Override
307    public long getItemId(int position) {
308      return position;
309    }
310
311    @Override
312    public View getView(final int position, View convertView, ViewGroup parent) {
313      TextView view;
314      if (convertView == null) {
315        view = new TextView(ApiBrowser.this);
316      } else {
317        view = (TextView) convertView;
318      }
319      view.setPadding(4, 4, 4, 4);
320      view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
321      if (mExpandedPositions.contains(position)) {
322        view.setText(mMethodDescriptors.get(position).getHelp());
323      } else {
324        view.setText(mMethodDescriptors.get(position).getName());
325      }
326      return view;
327    }
328
329    @Override
330    public int getPositionForSection(int section) {
331      return mIndexer.getPositionForSection(section);
332    }
333
334    @Override
335    public int getSectionForPosition(int position) {
336      return mIndexer.getSectionForPosition(position);
337    }
338
339    @Override
340    public Object[] getSections() {
341      return mIndexer.getSections();
342    }
343  }
344}
345