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 (at) 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 { 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 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 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 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 110 private void notifyConfigurationObservers() { 111 for (ConfigurationObserver observer : mObserverSet) { 112 observer.onConfigurationChanged(); 113 } 114 } 115 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 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. 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 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 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 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 230 public void startDiscovering() { 231 mListener.discoverAll(); 232 } 233 234 public void startDiscovering(String mime) { 235 mListener.discoverForType(mime); 236 } 237 238 public boolean isDiscoveryComplete() { 239 return mIsDiscoveryComplete; 240 } 241 242 public void registerObserver(ConfigurationObserver observer) { 243 if (observer != null) { 244 mObserverSet.add(observer); 245 } 246 } 247 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 */ 257 public List<? extends Interpreter> getSupportedInterpreters() { 258 return new ArrayList<Interpreter>(mInterpreterSet); 259 } 260 261 /** 262 * Returns the list of all installed interpreters. 263 */ 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 */ 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 */ 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 */ 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