/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googlecode.android_scripting.interpreter; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.net.Uri; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.SingleThreadExecutor; import com.googlecode.android_scripting.interpreter.shell.ShellInterpreter; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; /** * Manages and provides access to the set of available interpreters. * */ public class InterpreterConfiguration { private final InterpreterListener mListener; private final Set<Interpreter> mInterpreterSet; private final Set<ConfigurationObserver> mObserverSet; private final Context mContext; private volatile boolean mIsDiscoveryComplete = false; public interface ConfigurationObserver { public void onConfigurationChanged(); } private class InterpreterListener extends BroadcastReceiver { private final PackageManager mmPackageManager; private final ContentResolver mmResolver; private final ExecutorService mmExecutor; private final Map<String, Interpreter> mmDiscoveredInterpreters; private InterpreterListener(Context context) { mmPackageManager = context.getPackageManager(); mmResolver = context.getContentResolver(); mmExecutor = new SingleThreadExecutor(); mmDiscoveredInterpreters = new HashMap<String, Interpreter>(); } private void discoverForType(final String mime) { mmExecutor.execute(new Runnable() { @Override public void run() { Intent intent = new Intent(InterpreterConstants.ACTION_DISCOVER_INTERPRETERS); intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.setType(mime); List<ResolveInfo> resolveInfos = mmPackageManager.queryIntentActivities(intent, 0); for (ResolveInfo info : resolveInfos) { addInterpreter(info.activityInfo.packageName); } mIsDiscoveryComplete = true; notifyConfigurationObservers(); } }); } private void discoverAll() { mmExecutor.execute(new Runnable() { @Override public void run() { Intent intent = new Intent(InterpreterConstants.ACTION_DISCOVER_INTERPRETERS); intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.setType(InterpreterConstants.MIME + "*"); List<ResolveInfo> resolveInfos = mmPackageManager.queryIntentActivities(intent, 0); for (ResolveInfo info : resolveInfos) { addInterpreter(info.activityInfo.packageName); } mIsDiscoveryComplete = true; notifyConfigurationObservers(); } }); } private void notifyConfigurationObservers() { for (ConfigurationObserver observer : mObserverSet) { observer.onConfigurationChanged(); } } private void addInterpreter(final String packageName) { if (mmDiscoveredInterpreters.containsKey(packageName)) { return; } Interpreter discoveredInterpreter = buildInterpreter(packageName); if (discoveredInterpreter == null) { return; } mmDiscoveredInterpreters.put(packageName, discoveredInterpreter); mInterpreterSet.add(discoveredInterpreter); Log.v("Interpreter discovered: " + packageName + "\nBinary: " + discoveredInterpreter.getBinary()); } private void remove(final String packageName) { if (!mmDiscoveredInterpreters.containsKey(packageName)) { return; } mmExecutor.execute(new Runnable() { @Override public void run() { Interpreter interpreter = mmDiscoveredInterpreters.get(packageName); if (interpreter == null) { Log.v("Interpreter for " + packageName + " not installed."); return; } mInterpreterSet.remove(interpreter); mmDiscoveredInterpreters.remove(packageName); notifyConfigurationObservers(); } }); } // We require that there's only one interpreter provider per APK. private Interpreter buildInterpreter(String packageName) { PackageInfo packInfo; try { packInfo = mmPackageManager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS); } catch (NameNotFoundException e) { throw new RuntimeException("Package '" + packageName + "' not found."); } ProviderInfo provider = packInfo.providers[0]; Map<String, String> interpreterMap = getMap(provider, InterpreterConstants.PROVIDER_PROPERTIES); if (interpreterMap == null) { Log.e("Null interpreter map for: " + packageName); return null; } Map<String, String> environmentMap = getMap(provider, InterpreterConstants.PROVIDER_ENVIRONMENT_VARIABLES); if (environmentMap == null) { throw new RuntimeException("Null environment map for: " + packageName); } Map<String, String> argumentsMap = getMap(provider, InterpreterConstants.PROVIDER_ARGUMENTS); if (argumentsMap == null) { throw new RuntimeException("Null arguments map for: " + packageName); } return Interpreter.buildFromMaps(interpreterMap, environmentMap, argumentsMap); } private Map<String, String> getMap(ProviderInfo provider, String name) { Uri uri = Uri.parse("content://" + provider.authority + "/" + name); Cursor cursor = mmResolver.query(uri, null, null, null, null); if (cursor == null) { return null; } cursor.moveToFirst(); // Use LinkedHashMap so that order is maintained (important for position CLI arguments). Map<String, String> map = new LinkedHashMap<String, String>(); for (int i = 0; i < cursor.getColumnCount(); i++) { map.put(cursor.getColumnName(i), cursor.getString(i)); } return map; } @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final String packageName = intent.getData().getSchemeSpecificPart(); if (action.equals(InterpreterConstants.ACTION_INTERPRETER_ADDED)) { mmExecutor.execute(new Runnable() { @Override public void run() { addInterpreter(packageName); notifyConfigurationObservers(); } }); } else if (action.equals(InterpreterConstants.ACTION_INTERPRETER_REMOVED) || action.equals(Intent.ACTION_PACKAGE_REMOVED) || action.equals(Intent.ACTION_PACKAGE_REPLACED) || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) { remove(packageName); } } } public InterpreterConfiguration(Context context) { mContext = context; mInterpreterSet = new CopyOnWriteArraySet<Interpreter>(); mInterpreterSet.add(new ShellInterpreter()); mObserverSet = new CopyOnWriteArraySet<ConfigurationObserver>(); IntentFilter filter = new IntentFilter(); filter.addAction(InterpreterConstants.ACTION_INTERPRETER_ADDED); filter.addAction(InterpreterConstants.ACTION_INTERPRETER_REMOVED); filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addAction(Intent.ACTION_PACKAGE_REPLACED); filter.addDataScheme("package"); mListener = new InterpreterListener(mContext); mContext.registerReceiver(mListener, filter); } public void startDiscovering() { mListener.discoverAll(); } public void startDiscovering(String mime) { mListener.discoverForType(mime); } public boolean isDiscoveryComplete() { return mIsDiscoveryComplete; } public void registerObserver(ConfigurationObserver observer) { if (observer != null) { mObserverSet.add(observer); } } public void unregisterObserver(ConfigurationObserver observer) { if (observer != null) { mObserverSet.remove(observer); } } /** * Returns the list of all known interpreters. */ public List<? extends Interpreter> getSupportedInterpreters() { return new ArrayList<Interpreter>(mInterpreterSet); } /** * Returns the list of all installed interpreters. */ public List<Interpreter> getInstalledInterpreters() { List<Interpreter> interpreters = new ArrayList<Interpreter>(); for (Interpreter i : mInterpreterSet) { if (i.isInstalled()) { interpreters.add(i); } } return interpreters; } /** * Returns the list of interpreters that support interactive mode execution. */ public List<Interpreter> getInteractiveInterpreters() { List<Interpreter> interpreters = new ArrayList<Interpreter>(); for (Interpreter i : mInterpreterSet) { if (i.isInstalled() && i.hasInteractiveMode()) { interpreters.add(i); } } return interpreters; } /** * Returns the interpreter matching the provided name or null if no interpreter was found. */ public Interpreter getInterpreterByName(String interpreterName) { for (Interpreter i : mInterpreterSet) { if (i.getName().equals(interpreterName)) { return i; } } return null; } /** * Returns the correct interpreter for the provided script name based on the script's extension or * null if no interpreter was found. */ public Interpreter getInterpreterForScript(String scriptName) { int dotIndex = scriptName.lastIndexOf('.'); if (dotIndex == -1) { return null; } String ext = scriptName.substring(dotIndex); for (Interpreter i : mInterpreterSet) { if (i.getExtension().equals(ext)) { return i; } } return null; } }