com.anysoftkeyboard.addons.AddOnsFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.anysoftkeyboard.addons.AddOnsFactory.java

Source

/*
 * 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.SharedPreferences;
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.preference.PreferenceManager;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.XmlRes;
import android.support.v4.content.SharedPreferencesCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Xml;

import com.anysoftkeyboard.AnySoftKeyboard;
import com.anysoftkeyboard.utils.Logger;
import com.menny.android.anysoftkeyboard.BuildConfig;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static java.util.Collections.unmodifiableList;

public abstract class AddOnsFactory<E extends AddOn> {

    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";
    private static final String XML_DEV_ADD_ON_ATTRIBUTE = "devOnly";
    private static final String XML_HIDDEN_ADD_ON_ATTRIBUTE = "hidden";

    @NonNull
    protected final Context mContext;
    protected final String mTag;
    /**
     * 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 mReceiverInterface;
    /**
     * Name under which an external addon broadcast receiver component
     * publishes information about itself.
     */
    private final String mReceiverMetaData;
    protected final ArrayList<E> mAddOns = new ArrayList<>();
    protected final HashMap<CharSequence, E> mAddOnsById = new HashMap<>();
    private final boolean mReadExternalPacksToo;
    private final String mRootNodeTag;
    private final String mAddonNodeTag;
    @XmlRes
    private final int mBuildInAddOnsResId;
    protected final CharSequence mDefaultAddOnId;
    private final boolean mDevAddOnsIncluded;

    //NOTE: this should only be used when interacting with shared-prefs!
    protected final String mPrefIdPrefix;
    protected final SharedPreferences mSharedPreferences;

    protected AddOnsFactory(@NonNull Context context, String tag, String receiverInterface, String receiverMetaData,
            String rootNodeTag, String addonNodeTag, String prefIdPrefix, @XmlRes int buildInAddonResId,
            @StringRes int defaultAddOnStringId, boolean readExternalPacksToo) {
        this(context, tag, receiverInterface, receiverMetaData, rootNodeTag, addonNodeTag, prefIdPrefix,
                buildInAddonResId, defaultAddOnStringId, readExternalPacksToo, BuildConfig.TESTING_BUILD);
    }

    @VisibleForTesting
    AddOnsFactory(@NonNull Context context, String tag, String receiverInterface, String receiverMetaData,
            String rootNodeTag, String addonNodeTag, @NonNull String prefIdPrefix, @XmlRes int buildInAddonResId,
            @StringRes int defaultAddOnStringId, boolean readExternalPacksToo, boolean isDebugBuild) {
        mContext = context;
        mTag = tag;
        mReceiverInterface = receiverInterface;
        mReceiverMetaData = receiverMetaData;
        mRootNodeTag = rootNodeTag;
        mAddonNodeTag = addonNodeTag;
        if (TextUtils.isEmpty(prefIdPrefix))
            throw new IllegalArgumentException("prefIdPrefix can not be empty!");
        mPrefIdPrefix = prefIdPrefix;
        mBuildInAddOnsResId = buildInAddonResId;
        mReadExternalPacksToo = readExternalPacksToo;
        mDevAddOnsIncluded = isDebugBuild;
        mDefaultAddOnId = defaultAddOnStringId == 0 ? null : context.getText(defaultAddOnStringId);
        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
    }

    @Nullable
    protected static CharSequence getTextFromResourceOrText(Context context, AttributeSet attrs,
            String attributeName) {
        final int stringResId = attrs.getAttributeResourceValue(null, attributeName, AddOn.INVALID_RES_ID);
        if (stringResId != AddOn.INVALID_RES_ID) {
            return context.getResources().getText(stringResId);
        } else {
            return attrs.getAttributeValue(null, attributeName);
        }
    }

    public static void onExternalPackChanged(Intent eventIntent, AnySoftKeyboard ime, AddOnsFactory... factories) {
        boolean cleared = false;
        boolean recreateView = false;
        for (AddOnsFactory<?> factory : factories) {
            try {
                if (factory.isEventRequiresCacheRefresh(eventIntent)) {
                    cleared = true;
                    if (factory.isEventRequiresViewReset(eventIntent))
                        recreateView = true;
                    Logger.d("AddOnsFactory", factory.getClass().getName()
                            + " will handle this package-changed event. Also recreate view? " + recreateView);
                    factory.clearAddOnList();
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }
        if (cleared)
            ime.resetKeyboardView(recreateView);
    }

    public final List<E> getEnabledAddOns() {
        List<CharSequence> enabledIds = getEnabledIds();
        List<E> addOns = new ArrayList<>(enabledIds.size());
        for (CharSequence enabledId : enabledIds) {
            E addOn = getAddOnById(enabledId);
            if (addOn != null)
                addOns.add(addOn);
        }

        return Collections.unmodifiableList(addOns);
    }

    public boolean isAddOnEnabled(CharSequence addOnId) {
        return mSharedPreferences.getBoolean(mPrefIdPrefix + addOnId, isAddOnEnabledByDefault(addOnId));
    }

    protected final void setAddOnEnableValueInPrefs(SharedPreferences.Editor editor, CharSequence addOnId,
            boolean enabled) {
        editor.putBoolean(mPrefIdPrefix + addOnId, enabled);
    }

    public abstract void setAddOnEnabled(CharSequence addOnId, boolean enabled);

    protected boolean isAddOnEnabledByDefault(@NonNull CharSequence addOnId) {
        return false;
    }

    public final E getEnabledAddOn() {
        return getEnabledAddOns().get(0);
    }

    public final synchronized List<CharSequence> getEnabledIds() {
        ArrayList<CharSequence> enabledIds = new ArrayList<>();
        for (E addOn : getAllAddOns()) {
            final CharSequence addOnId = addOn.getId();
            if (isAddOnEnabled(addOnId))
                enabledIds.add(addOnId);
        }

        //ensuring at least one add-on is there
        if (enabledIds.size() == 0 && !TextUtils.isEmpty(mDefaultAddOnId))
            enabledIds.add(mDefaultAddOnId);

        return Collections.unmodifiableList(enabledIds);
    }

    private boolean isEventRequiresCacheRefresh(Intent eventIntent) 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(packageNameSchemePart);
            if (hasAddon) {
                Logger.d(mTag, "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) {
                Logger.d(mTag, "It seems that an addon I use (in package " + packageNameSchemePart
                        + ") has been changed. I need to reload stuff.");
                return true;
            } else {
                boolean hasAddon = isPackageContainAnAddon(packageNameSchemePart);
                if (hasAddon) {
                    Logger.d(mTag, "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) {
                Logger.d(mTag, "It seems that an addon I use (in package " + packageNameSchemePart
                        + ") has been removed. I need to reload stuff.");
                return true;
            }
        }
        return false;
    }

    private boolean isPackageManaged(String packageNameSchemePart) {
        for (AddOn addOn : mAddOnsById.values()) {
            if (addOn.getPackageName().equals(packageNameSchemePart)) {
                return true;
            }
        }

        return false;
    }

    private boolean isPackageContainAnAddon(String packageNameSchemePart) throws NameNotFoundException {
        PackageInfo newPackage = mContext.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(mContext.getPackageManager(),
                        mReceiverMetaData);
                if (xml != null) {
                    return true;
                }
            }
        }

        return false;
    }

    protected boolean isEventRequiresViewReset(Intent eventIntent) {
        return false;
    }

    @CallSuper
    protected synchronized void clearAddOnList() {
        mAddOns.clear();
        mAddOnsById.clear();
    }

    public synchronized E getAddOnById(CharSequence id) {
        if (mAddOnsById.size() == 0) {
            loadAddOns();
        }
        return mAddOnsById.get(id);
    }

    public final synchronized List<E> getAllAddOns() {
        Logger.d(mTag, "getAllAddOns has %d add on for %s", mAddOns.size(), getClass().getName());
        if (mAddOns.size() == 0) {
            loadAddOns();
        }
        Logger.d(mTag, "getAllAddOns will return %d add on for %s", mAddOns.size(), getClass().getName());
        return unmodifiableList(mAddOns);
    }

    @CallSuper
    protected void loadAddOns() {
        clearAddOnList();

        List<E> local = getAddOnsFromResId(mContext, mBuildInAddOnsResId);
        for (E addon : local) {
            Logger.d(mTag, "Local add-on %s loaded", addon.getId());
        }
        mAddOns.addAll(local);
        List<E> external = getExternalAddOns();
        for (E addon : external) {
            Logger.d(mTag, "External add-on %s loaded", addon.getId());
        }
        mAddOns.addAll(external);
        Logger.d(mTag, "Have %d add on for %s", mAddOns.size(), getClass().getName());

        for (E addOn : mAddOns)
            mAddOnsById.put(addOn.getId(), addOn);
        //removing hidden addons from global list, so hidden addons exist only in the mapping
        for (E addOn : mAddOnsById.values()) {
            if (addOn instanceof AddOnImpl && ((AddOnImpl) addOn).isHiddenAddon()) {
                mAddOns.remove(addOn);
            }
        }

        //sorting the keyboards according to the requested
        //sort order (from minimum to maximum)
        Collections.sort(mAddOns, new AddOnsComparator());
        Logger.d(mTag, "Have %d add on for %s (after sort)", mAddOns.size(), getClass().getName());
    }

    private List<E> getExternalAddOns() {
        if (!mReadExternalPacksToo)//this will disable external packs (API careful stage)
            return Collections.emptyList();

        final PackageManager packageManager = mContext.getPackageManager();
        final List<ResolveInfo> broadcastReceivers = packageManager
                .queryBroadcastReceivers(new Intent(mReceiverInterface), PackageManager.GET_META_DATA);

        final List<E> externalAddOns = new ArrayList<>();

        for (final ResolveInfo receiver : broadcastReceivers) {
            if (receiver.activityInfo == null) {
                Logger.e(mTag, "BroadcastReceiver has null ActivityInfo. Receiver's label is "
                        + receiver.loadLabel(packageManager));
                Logger.e(mTag, "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 = mContext
                        .createPackageContext(receiver.activityInfo.packageName, Context.CONTEXT_IGNORE_SECURITY);
                final List<E> packageAddOns = getAddOnsFromActivityInfo(externalPackageContext,
                        receiver.activityInfo);

                externalAddOns.addAll(packageAddOns);
            } catch (final NameNotFoundException e) {
                Logger.e(mTag, "Did not find package: " + receiver.activityInfo.packageName);
            }

        }

        return externalAddOns;
    }

    private List<E> getAddOnsFromResId(Context packContext, int addOnsResId) {
        final XmlPullParser xml = packContext.getResources().getXml(addOnsResId);
        if (xml == null)
            return Collections.emptyList();
        return parseAddOnsFromXml(packContext, xml);
    }

    private List<E> getAddOnsFromActivityInfo(Context packContext, ActivityInfo ai) {
        final XmlPullParser xml = ai.loadXmlMetaData(mContext.getPackageManager(), mReceiverMetaData);
        if (xml == null)//issue 718: maybe a bad package?
            return new ArrayList<>();
        return parseAddOnsFromXml(packContext, xml);
    }

    private ArrayList<E> parseAddOnsFromXml(Context packContext, XmlPullParser xml) {
        final ArrayList<E> addOns = new ArrayList<>();
        try {
            int event;
            boolean inRoot = false;
            while ((event = xml.next()) != XmlPullParser.END_DOCUMENT) {
                final String tag = xml.getName();
                if (event == XmlPullParser.START_TAG) {
                    if (mRootNodeTag.equals(tag)) {
                        inRoot = true;
                    } else if (inRoot && mAddonNodeTag.equals(tag)) {
                        final AttributeSet attrs = Xml.asAttributeSet(xml);
                        E addOn = createAddOnFromXmlAttributes(attrs, packContext);
                        if (addOn != null) {
                            addOns.add(addOn);
                        }
                    }
                } else if (event == XmlPullParser.END_TAG) {
                    if (mRootNodeTag.equals(tag)) {
                        inRoot = false;
                        break;
                    }
                }
            }
        } catch (final IOException e) {
            Logger.e(mTag, "IO error:" + e);
            e.printStackTrace();
        } catch (final XmlPullParserException e) {
            Logger.e(mTag, "Parse error:" + e);
            e.printStackTrace();
        }

        return addOns;
    }

    @Nullable
    private E createAddOnFromXmlAttributes(AttributeSet attrs, Context packContext) {
        final CharSequence prefId = getTextFromResourceOrText(packContext, attrs, XML_PREF_ID_ATTRIBUTE);
        final CharSequence name = getTextFromResourceOrText(packContext, attrs, XML_NAME_RES_ID_ATTRIBUTE);

        if ((!mDevAddOnsIncluded) && attrs.getAttributeBooleanValue(null, XML_DEV_ADD_ON_ATTRIBUTE, false)) {
            Logger.w(mTag,
                    "Discarding add-on %s (name %s) since it is marked as DEV addon, and we're not a TESTING_BUILD build.",
                    prefId, name);
            return null;
        }

        final boolean isHidden = attrs.getAttributeBooleanValue(null, XML_HIDDEN_ADD_ON_ATTRIBUTE, false);
        final CharSequence description = getTextFromResourceOrText(packContext, attrs, XML_DESCRIPTION_ATTRIBUTE);

        final int sortIndex = attrs.getAttributeUnsignedIntValue(null, XML_SORT_INDEX_ATTRIBUTE, 1);

        // asserting
        if (TextUtils.isEmpty(prefId) || TextUtils.isEmpty(name)) {
            Logger.e(mTag, "External add-on does not include all mandatory details! Will not create add-on.");
            return null;
        } else {

            Logger.d(mTag, "External addon details: prefId:" + prefId + " name:" + name);
            return createConcreteAddOn(mContext, packContext, prefId, name, description, isHidden, sortIndex,
                    attrs);
        }
    }

    protected abstract E createConcreteAddOn(Context askContext, Context context, CharSequence prefId,
            CharSequence name, CharSequence description, boolean isHidden, int sortIndex, AttributeSet attrs);

    private static final class AddOnsComparator implements Comparator<AddOn> {
        private final String mAskPackageName;

        private AddOnsComparator() {
            mAskPackageName = BuildConfig.APPLICATION_ID;
        }

        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);
        }
    }

    public abstract static class SingleAddOnsFactory<E extends AddOn> extends AddOnsFactory<E> {

        protected SingleAddOnsFactory(@NonNull Context context, String tag, String receiverInterface,
                String receiverMetaData, String rootNodeTag, String addonNodeTag, String prefIdPrefix,
                @XmlRes int buildInAddonResId, @StringRes int defaultAddOnStringId, boolean readExternalPacksToo) {
            super(context, tag, receiverInterface, receiverMetaData, rootNodeTag, addonNodeTag, prefIdPrefix,
                    buildInAddonResId, defaultAddOnStringId, readExternalPacksToo);
        }

        @Override
        public void setAddOnEnabled(CharSequence addOnId, boolean enabled) {
            SharedPreferences.Editor editor = mSharedPreferences.edit();
            if (enabled) {
                //ensuring addons are loaded.
                getAllAddOns();
                //disable any other addon
                for (CharSequence otherAddOnId : mAddOnsById.keySet()) {
                    setAddOnEnableValueInPrefs(editor, otherAddOnId, otherAddOnId.equals(addOnId));
                }
            } else {
                //enabled the default, disable the requested
                //NOTE: can not directly disable a default addon!
                //you should enable something else, which will cause the current (default?)
                //add-on to be automatically disabled.
                setAddOnEnableValueInPrefs(editor, addOnId, false);
                setAddOnEnableValueInPrefs(editor, mDefaultAddOnId, true);
            }
            SharedPreferencesCompat.EditorCompat.getInstance().apply(editor);
        }
    }

    public abstract static class MultipleAddOnsFactory<E extends AddOn> extends AddOnsFactory<E> {
        private final String mSortedIdsPrefId;

        protected MultipleAddOnsFactory(@NonNull Context context, String tag, String receiverInterface,
                String receiverMetaData, String rootNodeTag, String addonNodeTag, String prefIdPrefix,
                @XmlRes int buildInAddonResId, @StringRes int defaultAddOnStringId, boolean readExternalPacksToo) {
            super(context, tag, receiverInterface, receiverMetaData, rootNodeTag, addonNodeTag, prefIdPrefix,
                    buildInAddonResId, defaultAddOnStringId, readExternalPacksToo);

            mSortedIdsPrefId = prefIdPrefix + "AddOnsFactory_order_key";
        }

        public final void setAddOnsOrder(Collection<E> addOnsOr) {
            List<CharSequence> ids = new ArrayList<>(addOnsOr.size());
            for (E addOn : addOnsOr) {
                ids.add(addOn.getId());
            }

            setAddOnIdsOrder(ids);
        }

        public final void setAddOnIdsOrder(Collection<CharSequence> enabledAddOnIds) {
            Set<CharSequence> storedKeys = new HashSet<>();
            StringBuilder orderValue = new StringBuilder();
            int currentOrderIndex = 0;
            for (CharSequence id : enabledAddOnIds) {
                //adding each once.
                if (!storedKeys.contains(id)) {
                    storedKeys.add(id);
                    if (mAddOnsById.containsKey(id)) {
                        final E addOnToReorder = mAddOnsById.get(id);
                        mAddOns.remove(addOnToReorder);
                        mAddOns.add(currentOrderIndex, addOnToReorder);
                        if (currentOrderIndex > 0) {
                            orderValue.append(",");
                        }
                        orderValue.append(id);
                        currentOrderIndex++;
                    }
                }
            }

            SharedPreferences.Editor editor = mSharedPreferences.edit();
            editor.putString(mSortedIdsPrefId, orderValue.toString());
            SharedPreferencesCompat.EditorCompat.getInstance().apply(editor);
        }

        @Override
        protected void loadAddOns() {
            super.loadAddOns();

            //now forcing order
            List<String> order = Arrays.asList(mSharedPreferences.getString(mSortedIdsPrefId, "").split(","));
            int currentOrderIndex = 0;
            Set<String> seenIds = new HashSet<>();
            for (String id : order) {
                if (mAddOnsById.containsKey(id) && !seenIds.contains(id)) {
                    seenIds.add(id);
                    E addOnToReorder = mAddOnsById.get(id);
                    mAddOns.remove(addOnToReorder);
                    mAddOns.add(currentOrderIndex, addOnToReorder);
                    currentOrderIndex++;
                }
            }
        }

        @Override
        public void setAddOnEnabled(CharSequence addOnId, boolean enabled) {
            SharedPreferences.Editor editor = mSharedPreferences.edit();
            setAddOnEnableValueInPrefs(editor, addOnId, enabled);
            SharedPreferencesCompat.EditorCompat.getInstance().apply(editor);
        }

        @Override
        protected boolean isAddOnEnabledByDefault(@NonNull CharSequence addOnId) {
            return super.isAddOnEnabledByDefault(addOnId) || mDefaultAddOnId.equals(addOnId);
        }
    }
}