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.interpreter;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.pm.PackageInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ProviderInfo;
27 import android.content.pm.ResolveInfo;
28 import android.content.pm.PackageManager.NameNotFoundException;
29 import android.database.Cursor;
30 import android.net.Uri;
31 
32 import com.googlecode.android_scripting.Log;
33 import com.googlecode.android_scripting.SingleThreadExecutor;
34 import com.googlecode.android_scripting.interpreter.shell.ShellInterpreter;
35 
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.LinkedHashMap;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Set;
43 import java.util.concurrent.CopyOnWriteArraySet;
44 import java.util.concurrent.ExecutorService;
45 
46 /**
47  * Manages and provides access to the set of available interpreters.
48  *
49  * @author Damon Kohler (damonkohler@gmail.com)
50  */
51 public class InterpreterConfiguration {
52 
53   private final InterpreterListener mListener;
54   private final Set<Interpreter> mInterpreterSet;
55   private final Set<ConfigurationObserver> mObserverSet;
56   private final Context mContext;
57   private volatile boolean mIsDiscoveryComplete = false;
58 
59   public interface ConfigurationObserver {
onConfigurationChanged()60     public void onConfigurationChanged();
61   }
62 
63   private class InterpreterListener extends BroadcastReceiver {
64     private final PackageManager mmPackageManager;
65     private final ContentResolver mmResolver;
66     private final ExecutorService mmExecutor;
67     private final Map<String, Interpreter> mmDiscoveredInterpreters;
68 
InterpreterListener(Context context)69     private InterpreterListener(Context context) {
70       mmPackageManager = context.getPackageManager();
71       mmResolver = context.getContentResolver();
72       mmExecutor = new SingleThreadExecutor();
73       mmDiscoveredInterpreters = new HashMap<String, Interpreter>();
74     }
75 
discoverForType(final String mime)76     private void discoverForType(final String mime) {
77       mmExecutor.execute(new Runnable() {
78         @Override
79         public void run() {
80           Intent intent = new Intent(InterpreterConstants.ACTION_DISCOVER_INTERPRETERS);
81           intent.addCategory(Intent.CATEGORY_LAUNCHER);
82           intent.setType(mime);
83           List<ResolveInfo> resolveInfos = mmPackageManager.queryIntentActivities(intent, 0);
84           for (ResolveInfo info : resolveInfos) {
85             addInterpreter(info.activityInfo.packageName);
86           }
87           mIsDiscoveryComplete = true;
88           notifyConfigurationObservers();
89         }
90       });
91     }
92 
discoverAll()93     private void discoverAll() {
94       mmExecutor.execute(new Runnable() {
95         @Override
96         public void run() {
97           Intent intent = new Intent(InterpreterConstants.ACTION_DISCOVER_INTERPRETERS);
98           intent.addCategory(Intent.CATEGORY_LAUNCHER);
99           intent.setType(InterpreterConstants.MIME + "*");
100           List<ResolveInfo> resolveInfos = mmPackageManager.queryIntentActivities(intent, 0);
101           for (ResolveInfo info : resolveInfos) {
102             addInterpreter(info.activityInfo.packageName);
103           }
104           mIsDiscoveryComplete = true;
105           notifyConfigurationObservers();
106         }
107       });
108     }
109 
notifyConfigurationObservers()110     private void notifyConfigurationObservers() {
111       for (ConfigurationObserver observer : mObserverSet) {
112         observer.onConfigurationChanged();
113       }
114     }
115 
addInterpreter(final String packageName)116     private void addInterpreter(final String packageName) {
117       if (mmDiscoveredInterpreters.containsKey(packageName)) {
118         return;
119       }
120       Interpreter discoveredInterpreter = buildInterpreter(packageName);
121       if (discoveredInterpreter == null) {
122         return;
123       }
124       mmDiscoveredInterpreters.put(packageName, discoveredInterpreter);
125       mInterpreterSet.add(discoveredInterpreter);
126       Log.v("Interpreter discovered: " + packageName + "\nBinary: "
127           + discoveredInterpreter.getBinary());
128     }
129 
remove(final String packageName)130     private void remove(final String packageName) {
131       if (!mmDiscoveredInterpreters.containsKey(packageName)) {
132         return;
133       }
134       mmExecutor.execute(new Runnable() {
135         @Override
136         public void run() {
137           Interpreter interpreter = mmDiscoveredInterpreters.get(packageName);
138           if (interpreter == null) {
139             Log.v("Interpreter for " + packageName + " not installed.");
140             return;
141           }
142           mInterpreterSet.remove(interpreter);
143           mmDiscoveredInterpreters.remove(packageName);
144           notifyConfigurationObservers();
145         }
146       });
147     }
148 
149     // We require that there's only one interpreter provider per APK.
buildInterpreter(String packageName)150     private Interpreter buildInterpreter(String packageName) {
151       PackageInfo packInfo;
152       try {
153         packInfo = mmPackageManager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS);
154       } catch (NameNotFoundException e) {
155         throw new RuntimeException("Package '" + packageName + "' not found.");
156       }
157       ProviderInfo provider = packInfo.providers[0];
158 
159       Map<String, String> interpreterMap =
160           getMap(provider, InterpreterConstants.PROVIDER_PROPERTIES);
161       if (interpreterMap == null) {
162         Log.e("Null interpreter map for: " + packageName);
163         return null;
164       }
165       Map<String, String> environmentMap =
166           getMap(provider, InterpreterConstants.PROVIDER_ENVIRONMENT_VARIABLES);
167       if (environmentMap == null) {
168         throw new RuntimeException("Null environment map for: " + packageName);
169       }
170       Map<String, String> argumentsMap = getMap(provider, InterpreterConstants.PROVIDER_ARGUMENTS);
171       if (argumentsMap == null) {
172         throw new RuntimeException("Null arguments map for: " + packageName);
173       }
174       return Interpreter.buildFromMaps(interpreterMap, environmentMap, argumentsMap);
175     }
176 
getMap(ProviderInfo provider, String name)177     private Map<String, String> getMap(ProviderInfo provider, String name) {
178       Uri uri = Uri.parse("content://" + provider.authority + "/" + name);
179       Cursor cursor = mmResolver.query(uri, null, null, null, null);
180       if (cursor == null) {
181         return null;
182       }
183       cursor.moveToFirst();
184       // Use LinkedHashMap so that order is maintained (important for position CLI arguments).
185       Map<String, String> map = new LinkedHashMap<String, String>();
186       for (int i = 0; i < cursor.getColumnCount(); i++) {
187         map.put(cursor.getColumnName(i), cursor.getString(i));
188       }
189       return map;
190     }
191 
192     @Override
onReceive(Context context, Intent intent)193     public void onReceive(Context context, Intent intent) {
194       final String action = intent.getAction();
195       final String packageName = intent.getData().getSchemeSpecificPart();
196       if (action.equals(InterpreterConstants.ACTION_INTERPRETER_ADDED)) {
197         mmExecutor.execute(new Runnable() {
198           @Override
199           public void run() {
200             addInterpreter(packageName);
201             notifyConfigurationObservers();
202           }
203         });
204       } else if (action.equals(InterpreterConstants.ACTION_INTERPRETER_REMOVED)
205           || action.equals(Intent.ACTION_PACKAGE_REMOVED)
206           || action.equals(Intent.ACTION_PACKAGE_REPLACED)
207           || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) {
208         remove(packageName);
209       }
210     }
211 
212   }
213 
InterpreterConfiguration(Context context)214   public InterpreterConfiguration(Context context) {
215     mContext = context;
216     mInterpreterSet = new CopyOnWriteArraySet<Interpreter>();
217     mInterpreterSet.add(new ShellInterpreter());
218     mObserverSet = new CopyOnWriteArraySet<ConfigurationObserver>();
219     IntentFilter filter = new IntentFilter();
220     filter.addAction(InterpreterConstants.ACTION_INTERPRETER_ADDED);
221     filter.addAction(InterpreterConstants.ACTION_INTERPRETER_REMOVED);
222     filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
223     filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
224     filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
225     filter.addDataScheme("package");
226     mListener = new InterpreterListener(mContext);
227     mContext.registerReceiver(mListener, filter);
228   }
229 
startDiscovering()230   public void startDiscovering() {
231     mListener.discoverAll();
232   }
233 
startDiscovering(String mime)234   public void startDiscovering(String mime) {
235     mListener.discoverForType(mime);
236   }
237 
isDiscoveryComplete()238   public boolean isDiscoveryComplete() {
239     return mIsDiscoveryComplete;
240   }
241 
registerObserver(ConfigurationObserver observer)242   public void registerObserver(ConfigurationObserver observer) {
243     if (observer != null) {
244       mObserverSet.add(observer);
245     }
246   }
247 
unregisterObserver(ConfigurationObserver observer)248   public void unregisterObserver(ConfigurationObserver observer) {
249     if (observer != null) {
250       mObserverSet.remove(observer);
251     }
252   }
253 
254   /**
255    * Returns the list of all known interpreters.
256    */
getSupportedInterpreters()257   public List<? extends Interpreter> getSupportedInterpreters() {
258     return new ArrayList<Interpreter>(mInterpreterSet);
259   }
260 
261   /**
262    * Returns the list of all installed interpreters.
263    */
getInstalledInterpreters()264   public List<Interpreter> getInstalledInterpreters() {
265     List<Interpreter> interpreters = new ArrayList<Interpreter>();
266     for (Interpreter i : mInterpreterSet) {
267       if (i.isInstalled()) {
268         interpreters.add(i);
269       }
270     }
271     return interpreters;
272   }
273 
274   /**
275    * Returns the list of interpreters that support interactive mode execution.
276    */
getInteractiveInterpreters()277   public List<Interpreter> getInteractiveInterpreters() {
278     List<Interpreter> interpreters = new ArrayList<Interpreter>();
279     for (Interpreter i : mInterpreterSet) {
280       if (i.isInstalled() && i.hasInteractiveMode()) {
281         interpreters.add(i);
282       }
283     }
284     return interpreters;
285   }
286 
287   /**
288    * Returns the interpreter matching the provided name or null if no interpreter was found.
289    */
getInterpreterByName(String interpreterName)290   public Interpreter getInterpreterByName(String interpreterName) {
291     for (Interpreter i : mInterpreterSet) {
292       if (i.getName().equals(interpreterName)) {
293         return i;
294       }
295     }
296     return null;
297   }
298 
299   /**
300    * Returns the correct interpreter for the provided script name based on the script's extension or
301    * null if no interpreter was found.
302    */
getInterpreterForScript(String scriptName)303   public Interpreter getInterpreterForScript(String scriptName) {
304     int dotIndex = scriptName.lastIndexOf('.');
305     if (dotIndex == -1) {
306       return null;
307     }
308     String ext = scriptName.substring(dotIndex);
309     for (Interpreter i : mInterpreterSet) {
310       if (i.getExtension().equals(ext)) {
311         return i;
312       }
313     }
314     return null;
315   }
316 }
317