/* * Copyright (c) 2013 Menny Even-Danan * * 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.anysoftkeyboard.addons; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.util.AttributeSet; import android.util.Xml; import com.anysoftkeyboard.AnySoftKeyboard; import com.anysoftkeyboard.utils.Log; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; public abstract class AddOnsFactory<E extends AddOn> { private static final class AddOnsComparator implements Comparator<AddOn> { private final String mAskPackageName; private AddOnsComparator(Context askContext) { mAskPackageName = askContext.getPackageName(); } public int compare(AddOn k1, AddOn k2) { String c1 = k1.getPackageName(); String c2 = k2.getPackageName(); if (c1.equals(c2)) return k1.getSortIndex() - k2.getSortIndex(); else if (c1.equals(mAskPackageName))//I want to make sure ASK packages are first return -1; else if (c2.equals(mAskPackageName)) return 1; else return c1.compareToIgnoreCase(c2); } } private final static ArrayList<AddOnsFactory<?>> mActiveInstances = new ArrayList<AddOnsFactory<?>>(); private static final String sTAG = "AddOnsFactory"; public static void onPackageChanged(final Intent eventIntent, final AnySoftKeyboard ask) { boolean cleared = false; boolean recreateView = false; for (AddOnsFactory<?> factory : mActiveInstances) { try { if (factory.isEventRequiresCacheRefresh(eventIntent, ask.getApplicationContext())) { cleared = true; if (factory.isEventRequiresViewReset(eventIntent, ask.getApplicationContext())) recreateView = true; Log.d(sTAG, factory.getClass().getName() + " will handle this package-changed event. Also recreate view? " + recreateView); factory.clearAddOnList(); } } catch (NameNotFoundException e) { e.printStackTrace(); } } if (cleared) ask.resetKeyboardView(recreateView); } public static AddOn locateAddOn(String id, Context askContext) { for (AddOnsFactory<?> factory : mActiveInstances) { AddOn addOn = factory.getAddOnById(id, askContext); if (addOn != null) { Log.d(sTAG, "Located addon with id " + addOn.getId() + " of type " + addOn.getClass().getName()); return addOn; } } return null; } protected final String TAG; /** * This is the interface name that a broadcast receiver implementing an * external addon should say that it supports -- that is, this is the * action it uses for its intent filter. */ private final String RECEIVER_INTERFACE; /** * Name under which an external addon broadcast receiver component * publishes information about itself. */ private final String RECEIVER_META_DATA; private final ArrayList<E> mAddOns = new ArrayList<E>(); private final HashMap<String, E> mAddOnsById = new HashMap<String, E>(); private final boolean mReadExternalPacksToo; private final String ROOT_NODE_TAG; private final String ADDON_NODE_TAG; private final int mBuildInAddOnsResId; private static final String XML_PREF_ID_ATTRIBUTE = "id"; private static final String XML_NAME_RES_ID_ATTRIBUTE = "nameResId"; private static final String XML_DESCRIPTION_ATTRIBUTE = "description"; private static final String XML_SORT_INDEX_ATTRIBUTE = "index"; protected AddOnsFactory(String tag, String receiverInterface, String receiverMetaData, String rootNodeTag, String addonNodeTag, int buildInAddonResId, boolean readExternalPacksToo) { TAG = tag; RECEIVER_INTERFACE = receiverInterface; RECEIVER_META_DATA = receiverMetaData; ROOT_NODE_TAG = rootNodeTag; ADDON_NODE_TAG = addonNodeTag; mBuildInAddOnsResId = buildInAddonResId; mReadExternalPacksToo = readExternalPacksToo; mActiveInstances.add(this); } protected boolean isEventRequiresCacheRefresh(Intent eventIntent, Context context) throws NameNotFoundException { String action = eventIntent.getAction(); String packageNameSchemePart = eventIntent.getData().getSchemeSpecificPart(); if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { //will reset only if the new package has my addons boolean hasAddon = isPackageContainAnAddon(context, packageNameSchemePart); if (hasAddon) { Log.d(TAG, "It seems that an addon exists in a newly installed package " + packageNameSchemePart + ". I need to reload stuff."); return true; } } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action) || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { //If I'm managing OR it contains an addon (could be new feature in the package), I want to reset. boolean isPackagedManaged = isPackageManaged(packageNameSchemePart); if (isPackagedManaged) { Log.d(TAG, "It seems that an addon I use (in package " + packageNameSchemePart + ") has been changed. I need to reload stuff."); return true; } else { boolean hasAddon = isPackageContainAnAddon(context, packageNameSchemePart); if (hasAddon) { Log.d(TAG, "It seems that an addon exists in an updated package " + packageNameSchemePart + ". I need to reload stuff."); return true; } } } else //removed { //so only if I manage this package, I want to reset boolean isPackagedManaged = isPackageManaged(packageNameSchemePart); if (isPackagedManaged) { Log.d(TAG, "It seems that an addon I use (in package " + packageNameSchemePart + ") has been removed. I need to reload stuff."); return true; } } return false; } protected boolean isPackageManaged(String packageNameSchemePart) { for (AddOn addOn : mAddOns) { if (addOn.getPackageName().equals(packageNameSchemePart)) { return true; } } return false; } protected boolean isPackageContainAnAddon(Context context, String packageNameSchemePart) throws NameNotFoundException { PackageInfo newPackage = context.getPackageManager().getPackageInfo(packageNameSchemePart, PackageManager.GET_RECEIVERS + PackageManager.GET_META_DATA); if (newPackage.receivers != null) { ActivityInfo[] receivers = newPackage.receivers; for (ActivityInfo aReceiver : receivers) { //issue 904 if (aReceiver == null || aReceiver.applicationInfo == null || !aReceiver.enabled || !aReceiver.applicationInfo.enabled) continue; final XmlPullParser xml = aReceiver.loadXmlMetaData(context.getPackageManager(), RECEIVER_META_DATA); if (xml != null) { return true; } } } return false; } protected boolean isEventRequiresViewReset(Intent eventIntent, Context context) { return false; } protected synchronized void clearAddOnList() { mAddOns.clear(); mAddOnsById.clear(); } public synchronized E getAddOnById(String id, Context askContext) { if (mAddOnsById.size() == 0) { loadAddOns(askContext); } return mAddOnsById.get(id); } public synchronized final ArrayList<E> getAllAddOns(Context askContext) { if (mAddOns.size() == 0) { loadAddOns(askContext); } return mAddOns; } protected void loadAddOns(final Context askContext) { clearAddOnList(); mAddOns.addAll(getAddOnsFromResId(askContext, askContext, mBuildInAddOnsResId)); mAddOns.addAll(getExternalAddOns(askContext)); buildOtherDataBasedOnNewAddOns(mAddOns); //sorting the keyboards according to the requested //sort order (from minimum to maximum) Collections.sort(mAddOns, new AddOnsComparator(askContext)); } protected void buildOtherDataBasedOnNewAddOns(ArrayList<E> newAddOns) { for (E addOn : newAddOns) mAddOnsById.put(addOn.getId(), addOn); } private ArrayList<E> getExternalAddOns(Context askContext) { final ArrayList<E> externalAddOns = new ArrayList<E>(); if (!mReadExternalPacksToo)//this will disable external packs (API careful stage) return externalAddOns; final List<ResolveInfo> broadcastReceivers = askContext.getPackageManager().queryBroadcastReceivers(new Intent(RECEIVER_INTERFACE), PackageManager.GET_META_DATA); for (final ResolveInfo receiver : broadcastReceivers) { if (receiver.activityInfo == null) { Log.e(TAG, "BroadcastReceiver has null ActivityInfo. Receiver's label is " + receiver.loadLabel(askContext.getPackageManager())); Log.e(TAG, "Is the external keyboard a service instead of BroadcastReceiver?"); // Skip to next receiver continue; } if (!receiver.activityInfo.enabled || !receiver.activityInfo.applicationInfo.enabled) continue; try { final Context externalPackageContext = askContext.createPackageContext(receiver.activityInfo.packageName, PackageManager.GET_META_DATA); final ArrayList<E> packageAddOns = getAddOnsFromActivityInfo(askContext, externalPackageContext, receiver.activityInfo); externalAddOns.addAll(packageAddOns); } catch (final NameNotFoundException e) { Log.e(TAG, "Did not find package: " + receiver.activityInfo.packageName); } } return externalAddOns; } private ArrayList<E> getAddOnsFromResId(Context askContext, Context context, int addOnsResId) { final XmlPullParser xml = context.getResources().getXml(addOnsResId); if (xml == null) return new ArrayList<E>(); return parseAddOnsFromXml(askContext, context, xml); } private ArrayList<E> getAddOnsFromActivityInfo(Context askContext, Context context, ActivityInfo ai) { final XmlPullParser xml = ai.loadXmlMetaData(context.getPackageManager(), RECEIVER_META_DATA); if (xml == null)//issue 718: maybe a bad package? return new ArrayList<E>(); return parseAddOnsFromXml(askContext, context, xml); } private ArrayList<E> parseAddOnsFromXml(Context askContext, Context context, XmlPullParser xml) { final ArrayList<E> addOns = new ArrayList<E>(); try { int event; boolean inRoot = false; while ((event = xml.next()) != XmlPullParser.END_DOCUMENT) { final String tag = xml.getName(); if (event == XmlPullParser.START_TAG) { if (ROOT_NODE_TAG.equals(tag)) { inRoot = true; } else if (inRoot && ADDON_NODE_TAG.equals(tag)) { final AttributeSet attrs = Xml.asAttributeSet(xml); E addOn = createAddOnFromXmlAttributes(askContext, attrs, context); if (addOn != null) { addOns.add(addOn); } } } else if (event == XmlPullParser.END_TAG) { if (ROOT_NODE_TAG.equals(tag)) { inRoot = false; break; } } } } catch (final IOException e) { Log.e(TAG, "IO error:" + e); e.printStackTrace(); } catch (final XmlPullParserException e) { Log.e(TAG, "Parse error:" + e); e.printStackTrace(); } return addOns; } private E createAddOnFromXmlAttributes(Context askContext, AttributeSet attrs, Context context) { final String prefId = attrs.getAttributeValue(null, XML_PREF_ID_ATTRIBUTE); final int nameId = attrs.getAttributeResourceValue(null, XML_NAME_RES_ID_ATTRIBUTE, AddOn.INVALID_RES_ID); final int descriptionInt = attrs.getAttributeResourceValue(null, XML_DESCRIPTION_ATTRIBUTE, AddOn.INVALID_RES_ID); //NOTE, to be compatible we need this. because the most of descriptions are //without @string/adb String description; if (descriptionInt != AddOn.INVALID_RES_ID) { description = context.getResources().getString(descriptionInt); } else { description = attrs.getAttributeValue(null, XML_DESCRIPTION_ATTRIBUTE); } final int sortIndex = attrs.getAttributeUnsignedIntValue(null, XML_SORT_INDEX_ATTRIBUTE, 1); // asserting if ((prefId == null) || (nameId == AddOn.INVALID_RES_ID)) { Log.e(TAG, "External add-on does not include all mandatory details! Will not create add-on."); return null; } else { Log.d(TAG, "External addon details: prefId:" + prefId + " nameId:" + nameId); return createConcreteAddOn(askContext, context, prefId, nameId, description, sortIndex, attrs); } } protected abstract E createConcreteAddOn(Context askContext, Context context, String prefId, int nameId, String description, int sortIndex, AttributeSet attrs); }