/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.onegravity.rteditor.utils.io;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * An Iterator over the lines in a Reader.
 *

 * LineIterator holds a reference to an open Reader.
 * When you have finished with the iterator you should close the reader
 * to free internal resources. This can be done by closing the reader directly,
 * or by calling the {@link #close()} or {@link #closeQuietly(LineIterator)}
 * method on the iterator.
 *

 * The recommended usage pattern is:
 *

 * LineIterator it = FileUtils.lineIterator(file, "UTF-8");
 * try {
 *   while (it.hasNext()) {
 *     String line = it.nextLine();
 *     // do something with line
 *   }
 * } finally {
 *   it.close();
 * }
 *

 *
 * @version $Id: LineIterator.java 1307461 2012-03-30 15:12:29Z ggregory $
 * @since 1.2
 */
public class LineIterator implements Iterator<String> {

    // N.B. This class deliberately does not implement Iterable, see https://issues.apache.org/jira/browse/IO-181

    /** The reader that is being read. */
    private final BufferedReader bufferedReader;
    /** The current line. */
    private String cachedLine;
    /** A flag indicating if the iterator has been fully read. */
    private boolean finished = false;

    /**
     * Constructs an iterator of the lines for a Reader.
     *
     * @param reader the Reader to read from, not null
     * @throws IllegalArgumentException if the reader is null
     */
    public LineIterator(final Reader reader) throws IllegalArgumentException {
        if (reader == null) {
            throw new IllegalArgumentException("Reader must not be null");
        }
        if (reader instanceof BufferedReader) {
            bufferedReader = (BufferedReader) reader;
        } else {
            bufferedReader = new BufferedReader(reader);
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Indicates whether the Reader has more lines.
     * If there is an IOException then {@link #close()} will
     * be called on this instance.
     *
     * @return {@code true} if the Reader has more lines
     * @throws IllegalStateException if an IO exception occurs
     */
    public boolean hasNext() {
        if (cachedLine != null) {
            return true;
        } else if (finished) {
            return false;
        } else {
            try {
                while (true) {
                    String line = bufferedReader.readLine();
                    if (line == null) {
                        finished = true;
                        return false;
                    } else if (isValidLine(line)) {
                        cachedLine = line;
                        return true;
                    }
                }
            } catch(IOException ioe) {
                close();
                throw new IllegalStateException(ioe);
            }
        }
    }

    /**
     * Overridable method to validate each line that is returned.
     * This implementation always returns true.
     * @param line  the line that is to be validated
     * @return true if valid, false to remove from the iterator
     */
    protected boolean isValidLine(String line) {
        return true;
    }

    /**
     * Returns the next line in the wrapped Reader.
     *
     * @return the next line from the input
     * @throws NoSuchElementException if there is no line to return
     */
    public String next() {
        return nextLine();
    }

    /**
     * Returns the next line in the wrapped Reader.
     *
     * @return the next line from the input
     * @throws NoSuchElementException if there is no line to return
     */
    public String nextLine() {
        if (!hasNext()) {
            throw new NoSuchElementException("No more lines");
        }
        String currentLine = cachedLine;
        cachedLine = null;
        return currentLine;
    }

    /**
     * Closes the underlying Reader quietly.
     * This method is useful if you only want to process the first few
     * lines of a larger file. If you do not close the iterator
     * then the Reader remains open.
     * This method can safely be called multiple times.
     */
    public void close() {
        finished = true;
        IOUtils.closeQuietly(bufferedReader);
        cachedLine = null;
    }

    /**
     * Unsupported.
     *
     * @throws UnsupportedOperationException always
     */
    public void remove() {
        throw new UnsupportedOperationException("Remove unsupported on LineIterator");
    }

    //-----------------------------------------------------------------------
    /**
     * Closes the iterator, handling null and ignoring exceptions.
     *
     * @param iterator  the iterator to close
     */
    public static void closeQuietly(LineIterator iterator) {
        if (iterator != null) {
            iterator.close();
        }
    }

}


// This file is part of TagSoup and is Copyright 2002-2008 by John Cowan.
//
// TagSoup is licensed under the Apache License,
// Version 2.0.  You may obtain a copy of this license at
// http://www.apache.org/licenses/LICENSE-2.0 .  You may also have
// additional legal rights not granted by this license.
//
// TagSoup is distributed in the hope that it will be useful, but
// unless required by applicable law or agreed to in writing, TagSoup
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
// OF ANY KIND, either express or implied; not even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
//
// Model of document

package com.onegravity.rteditor.converter.tagsoup;

import android.annotation.SuppressLint;

import java.util.HashMap;

/**
 * Abstract class representing a TSSL schema. Actual TSSL schemas are compiled
 * into concrete subclasses of this class.
 */
public abstract class Schema {

    public static final int M_ANY = 0xFFFFFFFF;
    public static final int M_EMPTY = 0;
    public static final int M_PCDATA = 1 << 30;
    public static final int M_ROOT = 1 << 31;

    public static final int F_RESTART = 1;
    public static final int F_CDATA = 2;
    public static final int F_NOFORCE = 4;

    private HashMap<String, Integer> theEntities = new HashMap<String, Integer>();
    private HashMap<String, ElementType> theElementTypes = new HashMap<String, ElementType>();

    private String theURI = "";
    private String thePrefix = "";
    private ElementType theRoot = null;

    /**
     * Add or replace an element type for this schema.
     *
     * @param name     Name (Qname) of the element
     * @param model    Models of the element's content as a vector of bits
     * @param memberOf Models the element is a member of as a vector of bits
     * @param flags    Flags for the element
     */

    @SuppressLint("DefaultLocale")
    public void elementType(String name, int model, int memberOf, int flags) {
        ElementType e = new ElementType(name, model, memberOf, flags, this);
        theElementTypes.put(name.toLowerCase(), e);
        if (memberOf == M_ROOT)
            theRoot = e;
    }

    /**
     * Get the root element of this schema
     */

    public ElementType rootElementType() {
        return theRoot;
    }

    /**
     * Add or replace a default attribute for an element type in this schema.
     *
     * @param elemName Name (Qname) of the element type
     * @param attrName Name (Qname) of the attribute
     * @param type     Type of the attribute
     * @param value    Default value of the attribute; null if no default
     */

    public void attribute(String elemName, String attrName, String type,
                          String value) {
        ElementType e = getElementType(elemName);
        if (e == null) {
            throw new Error("Attribute " + attrName
                    + " specified for unknown element type " + elemName);
        }
        e.setAttribute(attrName, type, value);
    }

    /**
     * Specify natural parent of an element in this schema.
     *
     * @param name       Name of the child element
     * @param parentName Name of the parent element
     */

    public void parent(String name, String parentName) {
        ElementType child = getElementType(name);
        ElementType parent = getElementType(parentName);
        if (child == null) {
            throw new Error("No child " + name + " for parent " + parentName);
        }
        if (parent == null) {
            throw new Error("No parent " + parentName + " for child " + name);
        }
        child.setParent(parent);
    }

    /**
     * Add to or replace a character entity in this schema.
     *
     * @param name  Name of the entity
     * @param value Value of the entity
     */

    public void entity(String name, int value) {
        theEntities.put(name, value);
    }

    /**
     * Get an ElementType by name.
     *
     * @param name Name (Qname) of the element type
     * @return The corresponding ElementType
     */

    @SuppressLint("DefaultLocale")
    public ElementType getElementType(String name) {
        return (ElementType) (theElementTypes.get(name.toLowerCase()));
    }

    /**
     * Get an entity value by name.
     *
     * @param name Name of the entity
     * @return The corresponding character, or 0 if none
     */

    public int getEntity(String name) {
        // System.err.println("%% Looking up entity " + name);
        Integer ch = (Integer) theEntities.get(name);
        if (ch == null)
            return 0;
        return ch.intValue();
    }

    /**
     * Return the URI (namespace name) of this schema.
     */

    public String getURI() {
        return theURI;
    }

    /**
     * Return the prefix of this schema.
     */

    public String getPrefix() {
        return thePrefix;
    }

    /**
     * Change the URI (namespace name) of this schema.
     */

    public void setURI(String uri) {
        theURI = uri;
    }

    /**
     * Change the prefix of this schema.
     */

    public void setPrefix(String prefix) {
        thePrefix = prefix;
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.media.choose;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.widget.Toast;

import com.onegravity.rteditor.R;
import com.onegravity.rteditor.api.RTMediaFactory;
import com.onegravity.rteditor.api.media.RTAudio;
import com.onegravity.rteditor.api.media.RTImage;
import com.onegravity.rteditor.api.media.RTVideo;
import com.onegravity.rteditor.media.MediaUtils;
import com.onegravity.rteditor.media.MonitoredActivity;
import com.onegravity.rteditor.media.choose.processor.ImageProcessor;
import com.onegravity.rteditor.media.choose.processor.ImageProcessor.ImageProcessorListener;
import com.onegravity.rteditor.utils.Constants.MediaAction;

import java.io.File;

class ImageChooserManager extends MediaChooserManager implements ImageProcessorListener {

    public interface ImageChooserListener extends MediaChooserListener {
        /**
         * Callback method to inform the caller that an image file has been processed
         */
        public void onImageChosen(RTImage image);
    }

    private static final String CAPTURED_IMAGE_TEMPLATE = "CAPTURED_IMAGE.jpeg";

    private ImageChooserListener mListener;

    ImageChooserManager(MonitoredActivity activity, MediaAction mediaAction,
                        RTMediaFactory<RTImage, RTAudio, RTVideo> mediaFactory,
                        ImageChooserListener listener, Bundle savedInstanceState) {

        super(activity, mediaAction, mediaFactory, listener, savedInstanceState);

        mListener = listener;
    }

    @SuppressWarnings("incomplete-switch")
    @Override
    boolean chooseMedia() throws IllegalArgumentException {
        if (mListener == null) {
            throw new IllegalArgumentException("ImageChooserListener cannot be null");
        }
        switch (mMediaAction) {
            case PICK_PICTURE:
                return pickPicture();
            case CAPTURE_PICTURE:
                return takePicture();
        }
        return false;
    }

    private boolean pickPicture() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null)
                .addCategory(Intent.CATEGORY_OPENABLE)
                .setType("image/*");
        String title = mActivity.getString(R.string.rte_pick_image);
        startActivity(Intent.createChooser(intent, title));
        return true;
    }

    // TODO: on Android M we need to ask the WRITE_EXTERNAL_STORAGE permission explicitly
    private boolean takePicture() {
        try {
            // Create an image file name (must be in "public area" or the camera app might not be able to access the file)
            File imagePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
            File imageFile = MediaUtils.createUniqueFile(imagePath, CAPTURED_IMAGE_TEMPLATE, false);
            imagePath.mkdirs();
            if (imagePath.exists() && imageFile.createNewFile()) {
                setOriginalFile(imageFile.getAbsolutePath());
                Uri uriForCamera;

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                    // There are compatibility issues with FileProvider Uris on lower versions
                    uriForCamera = Uri.fromFile(imageFile);
                } else {
                    uriForCamera = FileProvider.getUriForFile(mActivity, "com.onegravity.rteditor.fileprovider", imageFile);
                }
                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                        .putExtra(MediaStore.EXTRA_OUTPUT, uriForCamera);
                startActivity(intent);
            } else {
                Toast.makeText(mActivity, "Can't take picture without an sdcard", Toast.LENGTH_SHORT).show();
                return false;
            }
        } catch (Exception e) {
            Log.e(getClass().getSimpleName(), e.getMessage(), e);
        }
        return true;
    }

    @SuppressWarnings("incomplete-switch")
    @Override
    void processMedia(MediaAction mediaAction, Intent data) {
        switch (mediaAction) {

            case PICK_PICTURE: {
                String originalFile = determineOriginalFile(data);
                if (originalFile != null) {
                    ImageProcessor processor = new ImageProcessor(originalFile, mMediaFactory, this);
                    startBackgroundJob(processor);
                }
                break;
            }

            case CAPTURE_PICTURE: {
                String originalFile = getOriginalFile();
                if (originalFile != null) {
                    ImageProcessor processor = new ImageProcessor(originalFile, mMediaFactory, this);
                    startBackgroundJob(processor);
                }
                break;
            }

        }
    }

    @Override
    /* ImageProcessorListener */
    public void onImageProcessed(RTImage image) {
        if (mListener != null) {
            mListener.onImageChosen(image);
        }
    }

}


/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.onegravity.rteditor.utils.validator;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
URL Validation routines.

 * Behavior of validation is modified by passing in options:
 *

 *
ALLOW_2_SLASHES - [FALSE]  Allows double '/' characters in the path
 * component.

 *
NO_FRAGMENT- [FALSE]  By default fragments are allowed, if this option is
 * included then fragments are flagged as illegal.

 *
ALLOW_ALL_SCHEMES - [FALSE] By default only http, https, and ftp are
 * considered valid schemes.  Enabling this option will let any scheme pass validation.

 *

 *
 *
Originally based in on php script by Debbie Dyer, validation.php v1.2b, Date: 03/07/02,
 * http://javascript.internet.com. However, this validation now bears little resemblance
 * to the php original.

 *

 *   Example of usage:
 *   Construct a UrlValidator with valid schemes of "http", and "https".
 *
 *    String[] schemes = {"http","https"}.
 *    UrlValidator urlValidator = new UrlValidator(schemes);
 *    if (urlValidator.isValid("ftp://foo.bar.com/")) {
 *       System.out.println("url is valid");
 *    } else {
 *       System.out.println("url is invalid");
 *    }
 *
 *    prints "url is invalid"
 *   If instead the default constructor is used.
 *
 *    UrlValidator urlValidator = new UrlValidator();
 *    if (urlValidator.isValid("ftp://foo.bar.com/")) {
 *       System.out.println("url is valid");
 *    } else {
 *       System.out.println("url is invalid");
 *    }
 *
 *   prints out "url is valid"
 *

 *
 * @see
 *
 *  Uniform Resource Identifiers (URI): Generic Syntax
 *
 *
 * @version $Revision: 1649932 $
 * @since Validator 1.4
 */
public class UrlValidator implements Serializable {

    private static final long serialVersionUID = 7557161713937335013L;

    /**
     * Allows all validly formatted schemes to pass validation instead of
     * supplying a set of valid schemes.
     */
    public static final long ALLOW_ALL_SCHEMES = 1 << 0;

    /**
     * Allow two slashes in the path component of the URL.
     */
    public static final long ALLOW_2_SLASHES = 1 << 1;

    /**
     * Enabling this options disallows any URL fragments.
     */
    public static final long NO_FRAGMENTS = 1 << 2;

    /**
     * Allow local URLs, such as http://localhost/ or http://machine/ .
     * This enables a broad-brush check, for complex local machine name
     *  validation requirements you should create your validator with
     *  a {@link RegexValidator} instead ({@link #UrlValidator(RegexValidator, long)})
     */
    public static final long ALLOW_LOCAL_URLS = 1 << 3;

    /**
     * This expression derived/taken from the BNF for URI (RFC2396).
     */
    private static final String URL_REGEX =
            "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?";
    //        12            3  4          5       6   7        8 9
    private static final Pattern URL_PATTERN = Pattern.compile(URL_REGEX);

    /**
     * Schema/Protocol (ie. http:, ftp:, file:, etc).
     */
    private static final int PARSE_URL_SCHEME = 2;

    /**
     * Includes hostname/ip and port number.
     */
    private static final int PARSE_URL_AUTHORITY = 4;

    private static final int PARSE_URL_PATH = 5;

    private static final int PARSE_URL_QUERY = 7;

    private static final int PARSE_URL_FRAGMENT = 9;

    /**
     * Protocol scheme (e.g. http, ftp, https).
     */
    private static final String SCHEME_REGEX = "^\\p{Alpha}[\\p{Alnum}\\+\\-\\.]*";
    private static final Pattern SCHEME_PATTERN = Pattern.compile(SCHEME_REGEX);

    // Drop numeric, and  "+-." for now
    // Validation of character set is done by isValidAuthority
    private static final String AUTHORITY_CHARS_REGEX = "\\p{Alnum}\\-\\.";

    private static final String AUTHORITY_REGEX =
            "^([" + AUTHORITY_CHARS_REGEX + "]*)(:\\d*)?(.*)?";
    //        1                                 2       3
    private static final Pattern AUTHORITY_PATTERN = Pattern.compile(AUTHORITY_REGEX);

    private static final int PARSE_AUTHORITY_HOST_IP = 1;

    private static final int PARSE_AUTHORITY_PORT = 2;

    /**
     * Should always be empty. The code currently allows spaces.
     */
    private static final int PARSE_AUTHORITY_EXTRA = 3;

    private static final String PATH_REGEX = "^(/[-\\w:@&?=+,.!/~*'%$_;\\(\\)]*)?$";
    private static final Pattern PATH_PATTERN = Pattern.compile(PATH_REGEX);

    private static final String QUERY_REGEX = "^(.*)$";
    private static final Pattern QUERY_PATTERN = Pattern.compile(QUERY_REGEX);

    private static final String PORT_REGEX = "^:(\\d{1,5})$";
    private static final Pattern PORT_PATTERN = Pattern.compile(PORT_REGEX);

    /**
     * Holds the set of current validation options.
     */
    private final long options;

    /**
     * The set of schemes that are allowed to be in a URL.
     */
    private final Set allowedSchemes; // Must be lower-case

    /**
     * Regular expressions used to manually validate authorities if IANA
     * domain name validation isn't desired.
     */
    private final RegexValidator authorityValidator;

    /**
     * If no schemes are provided, default to this set.
     */
    private static final String[] DEFAULT_SCHEMES = {"http", "https", "ftp"}; // Must be lower-case

    /**
     * Singleton instance of this class with default schemes and options.
     */
    private static final UrlValidator DEFAULT_URL_VALIDATOR = new UrlValidator();

    /**
     * Returns the singleton instance of this class with default schemes and options.
     * @return singleton instance with default schemes and options
     */
    public static UrlValidator getInstance() {
        return DEFAULT_URL_VALIDATOR;
    }

    /**
     * Create a UrlValidator with default properties.
     */
    public UrlValidator() {
        this(null);
    }

    /**
     * Behavior of validation is modified by passing in several strings options:
     * @param schemes Pass in one or more url schemes to consider valid, passing in
     *        a null will default to "http,https,ftp" being valid.
     *        If a non-null schemes is specified then all valid schemes must
     *        be specified. Setting the ALLOW_ALL_SCHEMES option will
     *        ignore the contents of schemes.
     */
    public UrlValidator(String[] schemes) {
        this(schemes, 0L);
    }

    /**
     * Initialize a UrlValidator with the given validation options.
     * @param options The options should be set using the public constants declared in
     * this class.  To set multiple options you simply add them together.  For example,
     * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options.
     */
    public UrlValidator(long options) {
        this(null, null, options);
    }

    /**
     * Behavior of validation is modified by passing in options:
     * @param schemes The set of valid schemes. Ignored if the ALLOW_ALL_SCHEMES option is set.
     * @param options The options should be set using the public constants declared in
     * this class.  To set multiple options you simply add them together.  For example,
     * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options.
     */
    public UrlValidator(String[] schemes, long options) {
        this(schemes, null, options);
    }

    /**
     * Initialize a UrlValidator with the given validation options.
     * @param authorityValidator Regular expression validator used to validate the authority part
     * This allows the user to override the standard set of domains.
     * @param options Validation options. Set using the public constants of this class.
     * To set multiple options, simply add them together:
     *
ALLOW_2_SLASHES + NO_FRAGMENTS

     * enables both of those options.
     */
    public UrlValidator(RegexValidator authorityValidator, long options) {
        this(null, authorityValidator, options);
    }

    /**
     * Customizable constructor. Validation behavior is modifed by passing in options.
     * @param schemes the set of valid schemes. Ignored if the ALLOW_ALL_SCHEMES option is set.
     * @param authorityValidator Regular expression validator used to validate the authority part
     * @param options Validation options. Set using the public constants of this class.
     * To set multiple options, simply add them together:
     *
ALLOW_2_SLASHES + NO_FRAGMENTS

     * enables both of those options.
     */
    public UrlValidator(String[] schemes, RegexValidator authorityValidator, long options) {
        this.options = options;

        if (isOn(ALLOW_ALL_SCHEMES)) {
            allowedSchemes = Collections.EMPTY_SET;
        } else {
            if (schemes == null) {
                schemes = DEFAULT_SCHEMES;
            }
            allowedSchemes = new HashSet(schemes.length);
            for(int i=0; i < schemes.length; i++) {
                allowedSchemes.add(schemes[i].toLowerCase(Locale.ENGLISH));
            }
        }

        this.authorityValidator = authorityValidator;
    }

    /**
     *
Checks if a field has a valid url address.

     *
     * Note that the method calls #isValidAuthority()
     * which checks that the domain is valid.
     *
     * @param value The value validation is being performed on.  A null
     * value is considered invalid.
     * @return true if the url is valid.
     */
    public boolean isValid(String value) {
        if (value == null) {
            return false;
        }

        // Check the whole url address structure
        Matcher urlMatcher = URL_PATTERN.matcher(value);
        if (!urlMatcher.matches()) {
            return false;
        }

        String scheme = urlMatcher.group(PARSE_URL_SCHEME);
        if (!isValidScheme(scheme)) {
            return false;
        }

        String authority = urlMatcher.group(PARSE_URL_AUTHORITY);
        if ("file".equals(scheme) && "".equals(authority)) {
            // Special case - file: allows an empty authority
        } else {
            // Validate the authority
            if (!isValidAuthority(authority)) {
                return false;
            }
        }

        if (!isValidPath(urlMatcher.group(PARSE_URL_PATH))) {
            return false;
        }

        if (!isValidQuery(urlMatcher.group(PARSE_URL_QUERY))) {
            return false;
        }

        if (!isValidFragment(urlMatcher.group(PARSE_URL_FRAGMENT))) {
            return false;
        }

        return true;
    }

    /**
     * Validate scheme. If schemes[] was initialized to a non null,
     * then only those schemes are allowed.
     * Otherwise the default schemes are "http", "https", "ftp".
     * Matching is case-blind.
     * @param scheme The scheme to validate.  A null value is considered
     * invalid.
     * @return true if valid.
     */
    protected boolean isValidScheme(String scheme) {
        if (scheme == null) {
            return false;
        }

        if (!SCHEME_PATTERN.matcher(scheme).matches()) {
            return false;
        }

        if (isOff(ALLOW_ALL_SCHEMES) && !allowedSchemes.contains(scheme.toLowerCase(Locale.ENGLISH))) {
            return false;
        }

        return true;
    }

    /**
     * Returns true if the authority is properly formatted.  An authority is the combination
     * of hostname and port.  A null authority value is considered invalid.
     * Note: this implementation validates the domain unless a RegexValidator was provided.
     * If a RegexValidator was supplied and it matches, then the authority is regarded
     * as valid with no further checks, otherwise the method checks against the
     * AUTHORITY_PATTERN and the DomainValidator (ALLOW_LOCAL_URLS)
     * @param authority Authority value to validate, alllows IDN
     * @return true if authority (hostname and port) is valid.
     */
    protected boolean isValidAuthority(String authority) {
        if (authority == null) {
            return false;
        }

        // check manual authority validation if specified
        if (authorityValidator != null && authorityValidator.isValid(authority)) {
            return true;
        }
        // convert to ASCII if possible
        final String authorityASCII = DomainValidator.unicodeToASCII(authority);

        Matcher authorityMatcher = AUTHORITY_PATTERN.matcher(authorityASCII);
        if (!authorityMatcher.matches()) {
            return false;
        }

        String hostLocation = authorityMatcher.group(PARSE_AUTHORITY_HOST_IP);
        // check if authority is hostname or IP address:
        // try a hostname first since that's much more likely
        DomainValidator domainValidator = DomainValidator.getInstance(isOn(ALLOW_LOCAL_URLS));
        if (!domainValidator.isValid(hostLocation)) {
            // try an IP address
            InetAddressValidator inetAddressValidator =
                InetAddressValidator.getInstance();
            if (!inetAddressValidator.isValid(hostLocation)) {
                // isn't either one, so the URL is invalid
                return false;
            }
        }

        String port = authorityMatcher.group(PARSE_AUTHORITY_PORT);
        if (port != null && !PORT_PATTERN.matcher(port).matches()) {
            return false;
        }

        String extra = authorityMatcher.group(PARSE_AUTHORITY_EXTRA);
        if (extra != null && extra.trim().length() > 0){
            return false;
        }

        return true;
    }

    /**
     * Returns true if the path is valid.  A null value is considered invalid.
     * @param path Path value to validate.
     * @return true if path is valid.
     */
    protected boolean isValidPath(String path) {
        if (path == null) {
            return false;
        }

        if (!PATH_PATTERN.matcher(path).matches()) {
            return false;
        }

        int slash2Count = countToken("//", path);
        if (isOff(ALLOW_2_SLASHES) && (slash2Count > 0)) {
            return false;
        }

        int slashCount = countToken("/", path);
        int dot2Count = countToken("..", path);
        if (dot2Count > 0 && (slashCount - slash2Count - 1) <= dot2Count) {
            return false;
        }

        return true;
    }

    /**
     * Returns true if the query is null or it's a properly formatted query string.
     * @param query Query value to validate.
     * @return true if query is valid.
     */
    protected boolean isValidQuery(String query) {
        if (query == null) {
            return true;
        }

        return QUERY_PATTERN.matcher(query).matches();
    }

    /**
     * Returns true if the given fragment is null or fragments are allowed.
     * @param fragment Fragment value to validate.
     * @return true if fragment is valid.
     */
    protected boolean isValidFragment(String fragment) {
        if (fragment == null) {
            return true;
        }

        return isOff(NO_FRAGMENTS);
    }

    /**
     * Returns the number of times the token appears in the target.
     * @param token Token value to be counted.
     * @param target Target value to count tokens in.
     * @return the number of tokens.
     */
    protected int countToken(String token, String target) {
        int tokenIndex = 0;
        int count = 0;
        while (tokenIndex != -1) {
            tokenIndex = target.indexOf(token, tokenIndex);
            if (tokenIndex > -1) {
                tokenIndex++;
                count++;
            }
        }
        return count;
    }

    /**
     * Tests whether the given flag is on.  If the flag is not a power of 2
     * (ie. 3) this tests whether the combination of flags is on.
     *
     * @param flag Flag value to check.
     *
     * @return whether the specified flag value is on.
     */
    private boolean isOn(long flag) {
        return (options & flag) > 0;
    }

    /**
     * Tests whether the given flag is off.  If the flag is not a power of 2
     * (ie. 3) this tests whether the combination of flags is off.
     *
     * @param flag Flag value to check.
     *
     * @return whether the specified flag value is off.
     */
    private boolean isOff(long flag) {
        return (options & flag) == 0;
    }
}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.effects;

import com.onegravity.rteditor.RTEditText;
import com.onegravity.rteditor.fonts.RTTypeface;
import com.onegravity.rteditor.spans.AbsoluteSizeSpan;
import com.onegravity.rteditor.spans.BackgroundColorSpan;
import com.onegravity.rteditor.spans.BoldSpan;
import com.onegravity.rteditor.spans.ForegroundColorSpan;
import com.onegravity.rteditor.spans.ItalicSpan;
import com.onegravity.rteditor.spans.LinkSpan;
import com.onegravity.rteditor.spans.StrikethroughSpan;
import com.onegravity.rteditor.spans.SubscriptSpan;
import com.onegravity.rteditor.spans.SuperscriptSpan;
import com.onegravity.rteditor.spans.TypefaceSpan;
import com.onegravity.rteditor.spans.UnderlineSpan;

import java.util.ArrayList;

public class Effects {
    // character effects
    public static final Effect<Boolean, BoldSpan> BOLD = new BoldEffect();                             // boolean effect
    public static final Effect<Boolean, ItalicSpan> ITALIC = new ItalicEffect();                       // boolean effect
    public static final Effect<Boolean, UnderlineSpan> UNDERLINE = new UnderlineEffect();              // boolean effect
    public static final Effect<Boolean, StrikethroughSpan> STRIKETHROUGH = new StrikethroughEffect();  // boolean effect
    public static final Effect<Boolean, SuperscriptSpan> SUPERSCRIPT = new SuperscriptEffect();        // boolean effect
    public static final Effect<Boolean, SubscriptSpan> SUBSCRIPT = new SubscriptEffect();              // boolean effect
    public static final Effect<Integer, AbsoluteSizeSpan> FONTSIZE = new AbsoluteSizeEffect();         // non-boolean effect
    public static final Effect<Integer, ForegroundColorSpan> FONTCOLOR = new ForegroundColorEffect();  // non-boolean effect
    public static final Effect<Integer, BackgroundColorSpan> BGCOLOR = new BackgroundColorEffect();    // non-boolean effect
    public static final Effect<RTTypeface, TypefaceSpan> TYPEFACE = new TypefaceEffect();              // non-boolean effect
    public static final Effect<String, LinkSpan> LINK = new LinkEffect();                              // non-boolean effect

    // paragraph effects
    public static final BulletEffect BULLET = new BulletEffect();                                      // boolean effect
    public static final NumberEffect NUMBER = new NumberEffect();                                      // boolean effect
    public static final IndentationEffect INDENTATION = new IndentationEffect();                       // non-boolean effect
    public static final AlignmentEffect ALIGNMENT = new AlignmentEffect();                             // non-boolean effect

    /*
     * ALL_EFFECTS is a list of all defined effects, for simpler iteration over all effects.
     */
    public static final ArrayList<Effect> ALL_EFFECTS = new ArrayList<Effect>();

    static {
        // character effects
        ALL_EFFECTS.add(BOLD);
        ALL_EFFECTS.add(ITALIC);
        ALL_EFFECTS.add(UNDERLINE);
        ALL_EFFECTS.add(STRIKETHROUGH);
        ALL_EFFECTS.add(SUPERSCRIPT);
        ALL_EFFECTS.add(SUBSCRIPT);
        ALL_EFFECTS.add(FONTSIZE);
        ALL_EFFECTS.add(FONTCOLOR);
        ALL_EFFECTS.add(BGCOLOR);
        ALL_EFFECTS.add(TYPEFACE);
        ALL_EFFECTS.add(LINK);

        // paragraph effects
        ALL_EFFECTS.add(BULLET);
        ALL_EFFECTS.add(NUMBER);
        ALL_EFFECTS.add(INDENTATION);
        ALL_EFFECTS.add(ALIGNMENT);
    }

    /*
     * FORMATTING_EFFECTS is a list of all effects which will be removed when the formatting is removed from the text.
     */
    public static final ArrayList<Effect> FORMATTING_EFFECTS = new ArrayList<Effect>();

    static {
        // character effects
        FORMATTING_EFFECTS.add(BOLD);
        FORMATTING_EFFECTS.add(ITALIC);
        FORMATTING_EFFECTS.add(UNDERLINE);
        FORMATTING_EFFECTS.add(STRIKETHROUGH);
        FORMATTING_EFFECTS.add(SUPERSCRIPT);
        FORMATTING_EFFECTS.add(SUBSCRIPT);
        FORMATTING_EFFECTS.add(FONTSIZE);
        FORMATTING_EFFECTS.add(FONTCOLOR);
        FORMATTING_EFFECTS.add(BGCOLOR);
        FORMATTING_EFFECTS.add(TYPEFACE);
        FORMATTING_EFFECTS.add(LINK);

        // paragraph effects
        FORMATTING_EFFECTS.add(BULLET);
        FORMATTING_EFFECTS.add(NUMBER);
        FORMATTING_EFFECTS.add(INDENTATION);
        FORMATTING_EFFECTS.add(ALIGNMENT);
    }

    /**
     * This important method makes sure that all paragraph effects are applied to whole paragraphs.
     * While it's optimized for performance it's still an expensive operation so it shouldn't be
     * called too often.
     *
     * @param exclude if an Effect has just been applied, there's no need to cleanup that Effect.
     */
    public static void cleanupParagraphs(RTEditText editor, Effect...exclude) {
        cleanupParagraphs(editor, Effects.ALIGNMENT, exclude);
        cleanupParagraphs(editor, Effects.INDENTATION, exclude);
        cleanupParagraphs(editor, Effects.BULLET, exclude);
        cleanupParagraphs(editor, Effects.NUMBER, exclude);
    }

    private static void cleanupParagraphs(RTEditText editor, ParagraphEffect effect, Effect...exclude) {
        for (Effect e : exclude) {
            if (effect == e) {
                return;
            }
        }

        effect.applyToSelection(editor, null, null);
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.api;

import android.content.Context;
import android.util.Log;

import com.onegravity.rteditor.api.media.RTAudio;
import com.onegravity.rteditor.api.media.RTAudioImpl;
import com.onegravity.rteditor.api.media.RTImage;
import com.onegravity.rteditor.api.media.RTImageImpl;
import com.onegravity.rteditor.api.media.RTMediaSource;
import com.onegravity.rteditor.api.media.RTMediaType;
import com.onegravity.rteditor.api.media.RTVideo;
import com.onegravity.rteditor.api.media.RTVideoImpl;
import com.onegravity.rteditor.media.MediaUtils;
import com.onegravity.rteditor.utils.Helper;

import com.onegravity.rteditor.utils.io.IOUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * This is a basic implementation of the RTMediaFactory using either the
 * internal (as in Context.context.getFilesDir() or the primary external
 * file system (as in Context.getExternalFilesDir(String).
 */
public class RTMediaFactoryImpl implements RTMediaFactory<RTImage, RTAudio, RTVideo> {
    private static final long serialVersionUID = 6970361368051595063L;

    private File mStoragePath;

    public RTMediaFactoryImpl(Context context) {
        this(context, true);    // use external storage as default
    }

    public RTMediaFactoryImpl(Context context, boolean externalStorage) {
        mStoragePath = externalStorage ?
                context.getExternalFilesDir(null) :
                context.getFilesDir();
    }

    /**
     * Returns the absolute file path for a certain RTMediaType.
     * <p>
     * The media type specific path as provided by RTMediaType is appended to
     * the storage path (e.g. <storage area>/images for image files).
     */
    protected String getAbsolutePath(RTMediaType mediaType) {
        File mediaPath = new File(mStoragePath.getAbsolutePath(), mediaType.mediaPath());
        if (!mediaPath.exists()) {
            mediaPath.mkdirs();
        }
        return mediaPath.getAbsolutePath();
    }

    /*
     * Use case 1: Inserting media objects into the rich text editor.
     *
     * This default implementation copies all files into the dedicated media
     * storage area.
     */

    @Override
    /* @inheritDoc */
    public RTImage createImage(RTMediaSource mediaSource) {
        File targetFile = loadMedia(mediaSource);
        return targetFile == null ? null :
                new RTImageImpl(targetFile.getAbsolutePath());
    }

    @Override
    /* @inheritDoc */
    public RTAudio createAudio(RTMediaSource mediaSource) {
        File targetFile = loadMedia(mediaSource);
        return targetFile == null ? null :
                new RTAudioImpl(targetFile.getAbsolutePath());
    }

    @Override
    /* @inheritDoc */
    public RTVideo createVideo(RTMediaSource mediaSource) {
        File targetFile = loadMedia(mediaSource);
        return targetFile == null ? null :
                new RTVideoImpl(targetFile.getAbsolutePath());
    }

    private File loadMedia(RTMediaSource mediaSource) {
        File targetPath = new File(getAbsolutePath(mediaSource.getMediaType()));
        File targetFile = MediaUtils.createUniqueFile(targetPath,
                mediaSource.getName(),
                mediaSource.getMimeType(),
                false);

        copyFile(mediaSource.getInputStream(), targetFile);

        return targetFile;
    }

    private void copyFile(InputStream in, File targetFile) {
        OutputStream out = null;
        try {
            out = new FileOutputStream(targetFile);
            IOUtils.copy(in, out);
        } catch (IOException ioe) {
            Log.e(getClass().getSimpleName(), ioe.getMessage(), ioe);
        } finally {
            Helper.closeQuietly(out);
            Helper.closeQuietly(in);
        }
    }

    /*
     * Use case 2: Load a rich text with referenced media objects into the rich
     * text editor.
     *
     * This default implementation doesn't apply any transformations to the path
     * because the files are stored in the file system where they can be
     * accessed directly by the rich text editor (via ImageSpan).
     */

    @Override
    /* @inheritDoc */
    public RTImage createImage(String path) {
        return new RTImageImpl(path);
    }

    @Override
    /* @inheritDoc */
    public RTAudio createAudio(String path) {
        return new RTAudioImpl(path);
    }

    @Override
    /* @inheritDoc */
    public RTVideo createVideo(String path) {
        return new RTVideoImpl(path);
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.effects;

import android.text.Spannable;
import android.text.Spanned;

import com.onegravity.rteditor.RTEditText;
import com.onegravity.rteditor.spans.RTSpan;
import com.onegravity.rteditor.utils.Selection;

/**
 * CharacterEffects are always applied to the current selection, like bold text or typeface.
 */
abstract class CharacterEffect<V, C extends RTSpan<V>> extends Effect<V, C> {

    @Override
    final protected SpanCollector<V> newSpanCollector(Class<? extends RTSpan<V>> spanClazz) {
        return new CharacterSpanCollector<V>(spanClazz);
    }

    @Override
    final protected Selection getSelection(RTEditText editor) {
        return new Selection(editor);
    }

    /**
     * Apply this effect to the selection.
     * If value is Null then the effect will be removed from the current selection.
     *
     * @param editor The editor to apply the effect to (current selection)
     * @param value  The value to apply (depends on the Effect)
     */
    public void applyToSelection(RTEditText editor, V value) {
        Selection selection = getSelection(editor);
        // SPAN_INCLUSIVE_INCLUSIVE is default for empty spans
        int flags = selection.isEmpty() ? Spanned.SPAN_INCLUSIVE_INCLUSIVE : Spanned.SPAN_EXCLUSIVE_INCLUSIVE;

        Spannable str = editor.getText();

        for (RTSpan<V> span : getSpans(str, selection, SpanCollectMode.SPAN_FLAGS)) {
            boolean sameSpan = span.getValue().equals(value);
            int spanStart = str.getSpanStart(span);
            if (spanStart < selection.start()) {
                // process preceding spans
                if (sameSpan) {
                    // we have a preceding span --> use SPAN_EXCLUSIVE_INCLUSIVE instead of SPAN_INCLUSIVE_INCLUSIVE
                    flags = Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
                    selection.offset(selection.start() - spanStart, 0);
                } else {
                    str.setSpan(newSpan(span.getValue()), spanStart, selection.start(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
            int spanEnd = str.getSpanEnd(span);
            if (spanEnd > selection.end()) {
                // process succeeding spans
                if (sameSpan) {
                    selection.offset(0, spanEnd - selection.end());
                } else {
                    str.setSpan(newSpan(span.getValue()), selection.end(), spanEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
                }
            }
            str.removeSpan(span);
        }

        if (value != null) {
            RTSpan<V> newSpan = newSpan(value);
            if (newSpan != null) {
                str.setSpan(newSpan, selection.start(), selection.end(), flags);
            }
        }
    }

    /**
     * Create an RTSpan for this effect.
     * This method is used in applyToSelection(RTEditText, V).
     * If the RTSpan can't be created only with V as value parameter then applyToSelection needs
     * to be implemented by the sub class.
     *
     * @return the class of the span this effect supports. Can be Null but then the subclass has to
     * override applyToSelection(RTEditText, V)
     */
    abstract protected RTSpan<V> newSpan(V value);

}


package com.onegravity.rteditor.utils.validator;

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

import java.io.Serializable;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
Perform email validations.

 *

 * Based on a script by Sandeep V. Tamhankar
 * http://javascript.internet.com
 *

 *

 * This implementation is not guaranteed to catch all possible errors in an email address.
 *
.
 *
 * @version $Revision: 1649927 $
 * @since Validator 1.4
 */
public class EmailValidator implements Serializable {

    private static final long serialVersionUID = 1705927040799295880L;

    private static final String SPECIAL_CHARS = "\\p{Cntrl}\\(\\)<>@,;:'\\\\\\\"\\.\\[\\]";
    private static final String VALID_CHARS = "[^\\s" + SPECIAL_CHARS + "]";
    private static final String QUOTED_USER = "(\"[^\"]*\")";
    private static final String WORD = "((" + VALID_CHARS + "|')+|" + QUOTED_USER + ")";

    private static final String EMAIL_REGEX = "^\\s*?(.+)@(.+?)\\s*$";
    private static final String IP_DOMAIN_REGEX = "^\\[(.*)\\]$";
    private static final String USER_REGEX = "^\\s*" + WORD + "(\\." + WORD + ")*$";

    private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
    private static final Pattern IP_DOMAIN_PATTERN = Pattern.compile(IP_DOMAIN_REGEX);
    private static final Pattern USER_PATTERN = Pattern.compile(USER_REGEX);

    private final boolean allowLocal;

    /**
     * Singleton instance of this class, which
     * doesn't consider local addresses as valid.
     */
    private static final EmailValidator EMAIL_VALIDATOR = new EmailValidator(false);

    /**
     * Singleton instance of this class, which does
     * consider local addresses valid.
     */
    private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL = new EmailValidator(true);

    /**
     * Returns the Singleton instance of this validator.
     *
     * @return singleton instance of this validator.
     */
    public static EmailValidator getInstance() {
        return EMAIL_VALIDATOR;
    }

    /**
     * Returns the Singleton instance of this validator,
     * with local validation as required.
     *
     * @param allowLocal Should local addresses be considered valid?
     * @return singleton instance of this validator
     */
    public static EmailValidator getInstance(boolean allowLocal) {
        if (allowLocal) {
            return EMAIL_VALIDATOR_WITH_LOCAL;
        }
        return EMAIL_VALIDATOR;
    }

    /**
     * Protected constructor for subclasses to use.
     *
     * @param allowLocal Should local addresses be considered valid?
     */
    protected EmailValidator(boolean allowLocal) {
        super();
        this.allowLocal = allowLocal;
    }

    /**
     *
Checks if a field has a valid e-mail address.

     *
     * @param email The value validation is being performed on.  A null
     *              value is considered invalid.
     * @return true if the email address is valid.
     */
    public boolean isValid(String email) {
        if (email == null) {
            return false;
        }

        if (email.endsWith(".")) { // check this first - it's cheap!
            return false;
        }

        // Check the whole email address structure
        Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
        if (!emailMatcher.matches()) {
            return false;
        }

        if (!isValidUser(emailMatcher.group(1))) {
            return false;
        }

        if (!isValidDomain(emailMatcher.group(2))) {
            return false;
        }

        return true;
    }

    /**
     * Returns true if the domain component of an email address is valid.
     *
     * @param domain being validated, may be in IDN format
     * @return true if the email address's domain is valid.
     */
    protected boolean isValidDomain(String domain) {
        // see if domain is an IP address in brackets
        Matcher ipDomainMatcher = IP_DOMAIN_PATTERN.matcher(domain);

        if (ipDomainMatcher.matches()) {
            InetAddressValidator inetAddressValidator =
                    InetAddressValidator.getInstance();
            return inetAddressValidator.isValid(ipDomainMatcher.group(1));
        }
        // Domain is symbolic name
        DomainValidator domainValidator =
                DomainValidator.getInstance(allowLocal);
        return domainValidator.isValid(domain) ||
                domainValidator.isValidTld(domain);
    }

    /**
     * Returns true if the user component of an email address is valid.
     *
     * @param user being validated
     * @return true if the user name is valid.
     */
    protected boolean isValidUser(String user) {
        return USER_PATTERN.matcher(user).matches();
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor;

import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method.ArrowKeyMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ClickableSpan;
import android.text.style.LeadingMarginSpan;
import android.view.MotionEvent;
import android.widget.TextView;

/**
 * ArrowKeyMovementMethod does support selection of text but not the clicking of links.
 * LinkMovementMethod does support clicking of links but not the selection of text.
 * This class adds the link clicking to the ArrowKeyMovementMethod.
 * We basically take the LinkMovementMethod onTouchEvent code and remove the line
 * Selection.removeSelection(buffer);
 * which de-selects all text when no link was found.
 */
public class RTEditorMovementMethod extends ArrowKeyMovementMethod {

    private static RTEditorMovementMethod sInstance;

    private static Rect sLineBounds = new Rect();

    public static synchronized  MovementMethod getInstance() {
        if (sInstance == null) {
            sInstance = new RTEditorMovementMethod();
        }
        return sInstance;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {

            int index = getCharIndexAt(widget, event);
            if (index != -1) {
                ClickableSpan[] link = buffer.getSpans(index, index, ClickableSpan.class);
                if (link.length != 0) {
                    if (action == MotionEvent.ACTION_UP) {
                        link[0].onClick(widget);
                    } else if (action == MotionEvent.ACTION_DOWN) {
                        Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));
                    }
                    return true;
                }
            }
            /*else {
                Selection.removeSelection(buffer);
            }*/

        }

        return super.onTouchEvent(widget, buffer, event);
    }

    // TODO finding links doesn't work with right alignment and potentially other formatting options
    private int getCharIndexAt(TextView textView, MotionEvent event) {
        // get coordinates
        int x = (int) event.getX();
        int y = (int) event.getY();
        x -= textView.getTotalPaddingLeft();
        y -= textView.getTotalPaddingTop();
        x += textView.getScrollX();
        y += textView.getScrollY();

        /*
         * Fail-fast check of the line bound.
         * If we're not within the line bound no character was touched
         */
        Layout layout = textView.getLayout();
        int line = layout.getLineForVertical(y);
        synchronized (sLineBounds) {
            layout.getLineBounds(line, sLineBounds);
            if (!sLineBounds.contains(x, y)) {
                return -1;
            }
        }

        // retrieve line text
        Spanned text = (Spanned) textView.getText();
        int lineStart = layout.getLineStart(line);
        int lineEnd = layout.getLineEnd(line);
        int lineLength = lineEnd - lineStart;
        if (lineLength == 0) {
            return -1;
        }
        Spanned lineText = (Spanned) text.subSequence(lineStart, lineEnd);

        // compute leading margin and subtract it from the x coordinate
        int margin = 0;
        LeadingMarginSpan[] marginSpans = lineText.getSpans(0, lineLength, LeadingMarginSpan.class);
        if (marginSpans != null) {
            for (LeadingMarginSpan span : marginSpans) {
                margin += span.getLeadingMargin(true);
            }
        }
        x -= margin;

        // retrieve text widths
        float[] widths = new float[lineLength];
        TextPaint paint = textView.getPaint();
        paint.getTextWidths(lineText, 0, lineLength, widths);

        // scale text widths by relative font size (absolute size / default size)
        final float defaultSize = textView.getTextSize();
        float scaleFactor = 1f;
        AbsoluteSizeSpan[] absSpans = lineText.getSpans(0, lineLength, AbsoluteSizeSpan.class);
        if (absSpans != null) {
            for (AbsoluteSizeSpan span : absSpans) {
                int spanStart = lineText.getSpanStart(span);
                int spanEnd = lineText.getSpanEnd(span);
                scaleFactor = span.getSize() / defaultSize;
                int start = Math.max(lineStart, spanStart);
                int end = Math.min(lineEnd, spanEnd);
                for (int i = start; i < end; i++) {
                    widths[i] *= scaleFactor;
                }
            }
        }

        // find index of touched character
        float startChar = 0;
        float endChar = 0;
        for (int i = 0; i < lineLength; i++) {
            startChar = endChar;
            endChar += widths[i];
            if (endChar >= x) {
                // which "end" is closer to x, the start or the end of the character?
                int index = lineStart + (x - startChar < endChar - x ? i : i + 1);
                //Logger.e(Logger.LOG_TAG, "Found character: " + (text.length()>index ? text.charAt(index) : ""));
                return index;
            }
        }

        return -1;
    }
}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor;

import android.annotation.SuppressLint;
import android.text.Spannable;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

/**
 * This class manages Operations for multiple rich text editors.
 * It's used by the RTManager to undo/redo operations.
 */
@SuppressLint("UseSparseArrays")
class RTOperationManager {

    /*
     * Maximum number of operations to put in the undo/redo stack
     */
    private static final int MAX_NR_OF_OPERATIONS = 50;

    /*
     * two operations performed in this time frame (in ms) are considered one
     * operation
     */
    private static final int TIME_BETWEEN_OPERATIONS = 300;

    /*
     * The undo/redo stacks by editor Id
     */
    private Map<Integer, Stack<Operation>> mUndoStacks = new HashMap<>();
    private Map<Integer, Stack<Operation>> mRedoStacks = new HashMap<>();
    ;

    // ****************************************** Operation Classes *******************************************

    /**
     * An atomic operation in the rich text editor.
     * If two operations are performed within a certain time frame
     * they will be considered as one operation and un-done/re-done together.
     */
    private abstract static class Operation {
        private long mTimestamp;

        private int mSelStartBefore;
        private int mSelEndBefore;
        private Spannable mBefore;

        private int mSelStartAfter;
        private int mSelEndAfter;
        private Spannable mAfter;

        Operation(Spannable before, Spannable after, int selStartBefore, int selEndBefore, int selStartAfter, int selEndAfter) {
            mSelStartBefore = selStartBefore;
            mSelEndBefore = selEndBefore;
            mSelStartAfter = selStartAfter;
            mSelEndAfter = selEndAfter;
            mBefore = before;
            mAfter = after;
            mTimestamp = System.currentTimeMillis();
        }

        boolean canMerge(Operation other) {
            return Math.abs(mTimestamp - other.mTimestamp) < TIME_BETWEEN_OPERATIONS;
        }

        Operation merge(Operation previousOp) {
            mBefore = previousOp.mBefore;
            mSelStartBefore = previousOp.mSelStartBefore;
            mSelEndBefore = previousOp.mSelEndBefore;
            return this;
        }

        void undo(RTEditText editor) {
            editor.ignoreTextChanges();
            editor.setText(mBefore);
            editor.setSelection(mSelStartBefore, mSelEndBefore);
            editor.registerTextChanges();
        }

        void redo(RTEditText editor) {
            editor.ignoreTextChanges();
            editor.setText(mAfter);
            editor.setSelection(mSelStartAfter, mSelEndAfter);
            editor.registerTextChanges();
        }
    }

    static class TextChangeOperation extends Operation {
        TextChangeOperation(Spannable before, Spannable after, int selStartBefore, int selEndBefore, int selStartAfter, int selEndAfter) {
            super(before, after, selStartBefore, selEndBefore, selStartAfter, selEndAfter);
        }
    }

    // ****************************************** execute/undo/redo/flush *******************************************

    /**
     * Call this when an operation is performed to add it to the undo stack.
     *
     * @param editor The rich text editor the operation was performed on
     * @param op     The Operation that was performed
     */
    synchronized void executed(RTEditText editor, Operation op) {
        Stack<Operation> undoStack = getUndoStack(editor);
        Stack<Operation> redoStack = getRedoStack(editor);

        // if operations are executed in a quick succession we "merge" them to have but one
        // -> saves memory and makes more sense from a user perspective (each key stroke an undo? -> no way)
        while (!undoStack.empty() && op.canMerge(undoStack.peek())) {
            Operation previousOp = undoStack.pop();
            op.merge(previousOp);
        }

        push(op, undoStack);
        redoStack.clear();
    }

    /**
     * Undo the last operation for a specific rich text editor
     *
     * @param editor Undo the last operation for this rich text editor
     */
    synchronized void undo(RTEditText editor) {
        Stack<Operation> undoStack = getUndoStack(editor);
        if (!undoStack.empty()) {
            Stack<Operation> redoStack = getRedoStack(editor);
            Operation op = undoStack.pop();
            push(op, redoStack);
            op.undo(editor);
            while (!undoStack.empty() && op.canMerge(undoStack.peek())) {
                op = undoStack.pop();
                push(op, redoStack);
                op.undo(editor);
            }
        }
    }

    /**
     * Re-do the last undone operation for a specific rich text editor
     *
     * @param editor Re-do an operation for this rich text editor
     */
    synchronized void redo(RTEditText editor) {
        Stack<Operation> redoStack = getRedoStack(editor);
        if (!redoStack.empty()) {
            Stack<Operation> undoStack = getUndoStack(editor);
            Operation op = redoStack.pop();
            push(op, undoStack);
            op.redo(editor);
            while (!redoStack.empty() && op.canMerge(redoStack.peek())) {
                op = redoStack.pop();
                push(op, undoStack);
                op.redo(editor);
            }
        }
    }

    /**
     * Flush all operations for a specific rich text editor (method unused at the moment)
     *
     * @param editor This rich text editor's operations will be flushed
     */
    synchronized void flushOperations(RTEditText editor) {
        Stack<Operation> undoStack = getUndoStack(editor);
        Stack<Operation> redoStack = getRedoStack(editor);
        undoStack.clear();
        redoStack.clear();
    }

    // ****************************************** Private Methods *******************************************

    private void push(Operation op, Stack<Operation> stack) {
        if (stack.size() >= MAX_NR_OF_OPERATIONS) {
            stack.remove(0);
        }
        stack.push(op);
    }

    private Stack<Operation> getUndoStack(RTEditText editor) {
        return getStack(mUndoStacks, editor);
    }

    private Stack<Operation> getRedoStack(RTEditText editor) {
        return getStack(mRedoStacks, editor);
    }

    private Stack<Operation> getStack(Map<Integer, Stack<Operation>> stacks, RTEditText editor) {
        Stack<Operation> stack = stacks.get(editor.getId());
        if (stack == null) {
            stack = new Stack<>();
            stacks.put(editor.getId(), stack);
        }
        return stack;
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.converter;

import android.text.TextUtils;
import android.util.Patterns;

import com.onegravity.rteditor.api.format.RTFormat;
import com.onegravity.rteditor.api.format.RTHtml;
import com.onegravity.rteditor.api.format.RTPlainText;
import com.onegravity.rteditor.api.media.RTAudio;
import com.onegravity.rteditor.api.media.RTImage;
import com.onegravity.rteditor.api.media.RTVideo;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Converts plain text to html
 */
public class ConverterTextToHtml {

    // Number of extra bytes to allocate in a string buffer for htmlification.
    private static final int TEXT_TO_HTML_EXTRA_BUFFER_LENGTH = 512;

    private static final String BITCOIN_URI_PATTERN = "bitcoin:[1-9a-km-zA-HJ-NP-Z]{27,34}(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?";

    private static final boolean USE_REPLACE_ALL = false;

    public static RTHtml<RTImage, RTAudio, RTVideo> convert(RTPlainText input) {
        String text = input.getText();
        String result = convert(text);
        return new RTHtml<>(RTFormat.HTML, result);
    }

    public static String convert(String text) {
        // Escape the entities and add newlines.
        String htmlified = text == null ? "" : TextUtils.htmlEncode(text);

        // Linkify the message.
        StringBuffer linkified = new StringBuffer(htmlified.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
        linkifyText(htmlified, linkified);

        // For some reason, TextUtils.htmlEncode escapes ' into ', which is technically part of the XHTML 1.0
        // standard, but Gmail doesn't recognize it as an HTML entity. We unescape that here.
        String result = linkified.toString().replace("\n", "
\n").replace("'", "'");
        return result;
    }

    private static void linkifyText(final String text, final StringBuffer outputBuffer) {
        String prepared = replaceAll(text, BITCOIN_URI_PATTERN, "$0");
        Matcher m = Patterns.WEB_URL.matcher(prepared);
        while (m.find()) {
            int start = m.start();
            if (start == 0 || (start != 0 && text.charAt(start - 1) != '@')) {
                if (m.group().indexOf(':') > 0) { // With no URI-schema we may get "http:/" links with the second / missing
                    m.appendReplacement(outputBuffer, "$0");
                } else {
                    m.appendReplacement(outputBuffer, "$0");
                }
            } else {
                m.appendReplacement(outputBuffer, "$0");
            }
        }
        m.appendTail(outputBuffer);
    }

    /**
     * A memory optimized algorithm for String.replaceAll
     */
    private static String replaceAll(String source, String search, String replace) {
        if (USE_REPLACE_ALL) {
            return source.replaceAll(search, replace);
        } else {
            Pattern p = Pattern.compile(search);
            Matcher m = p.matcher(source);
            StringBuffer sb = new StringBuffer();
            boolean atLeastOneFound = false;
            while (m.find()) {
                m.appendReplacement(sb, replace);
                atLeastOneFound = true;
            }
            if (atLeastOneFound) {
                m.appendTail(sb);
                return sb.toString();
            } else {
                return source;
            }
        }
    }

}


/*
 * Copyright (C) 2007 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.onegravity.rteditor.media.crop;

import java.io.FileDescriptor;
import java.util.Iterator;
import java.util.WeakHashMap;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

/**
 * This class provides several utilities to cancel bitmap decoding.
 *

 * The function decodeFileDescriptor() is used to decode a bitmap. During
 * decoding if another thread wants to cancel it, it calls the function
 * cancelThreadDecoding() specifying the Thread which is in decoding.
 *

 * cancelThreadDecoding() is sticky until allowThreadDecoding() is called.
 *

 * You can also cancel decoding for a set of threads using ThreadSet as the
 * parameter for cancelThreadDecoding. To put a thread into a ThreadSet, use the
 * add() method. A ThreadSet holds (weak) references to the threads, so you
 * don't need to remove Thread from it if some thread dies.
 */
public class BitmapManager {

    private static enum State {
        CANCEL, ALLOW
    }

    private static class ThreadStatus {

        public State mState = State.ALLOW;
        public BitmapFactory.Options mOptions;

        @Override
        public String toString() {

            String s;
            if (mState == State.CANCEL) {
                s = "Cancel";
            } else if (mState == State.ALLOW) {
                s = "Allow";
            } else {
                s = "?";
            }
            s = "thread state = " + s + ", options = " + mOptions;
            return s;
        }
    }

    public static class ThreadSet implements Iterable<Thread> {
        private final WeakHashMap<Thread, Object> mWeakCollection = new WeakHashMap<Thread, Object>();

        public void add(Thread t) {

            mWeakCollection.put(t, null);
        }

        public void remove(Thread t) {

            mWeakCollection.remove(t);
        }

        public Iterator<Thread> iterator() {

            return mWeakCollection.keySet().iterator();
        }
    }

    private final WeakHashMap<Thread, ThreadStatus> mThreadStatus = new WeakHashMap<Thread, ThreadStatus>();

    private static BitmapManager sManager = null;

    private BitmapManager() {

    }

    /**
     * Get thread status and create one if specified.
     */
    private synchronized ThreadStatus getOrCreateThreadStatus(Thread t) {
        ThreadStatus status = mThreadStatus.get(t);
        if (status == null) {
            status = new ThreadStatus();
            mThreadStatus.put(t, status);
        }
        return status;
    }

    /**
     * The following three methods are used to keep track of
     * BitmapFaction.Options used for decoding and cancelling.
     */
    private synchronized void setDecodingOptions(Thread t, BitmapFactory.Options options) {
        getOrCreateThreadStatus(t).mOptions = options;
    }

    synchronized BitmapFactory.Options getDecodingOptions(Thread t) {
        ThreadStatus status = mThreadStatus.get(t);
        return status != null ? status.mOptions : null;
    }

    synchronized void removeDecodingOptions(Thread t) {
        ThreadStatus status = mThreadStatus.get(t);
        status.mOptions = null;
    }

    /**
     * The following two methods are used to allow/cancel a set of threads for
     * bitmap decoding.
     */
    public synchronized void allowThreadDecoding(ThreadSet threads) {
        for (Thread t : threads) {
            allowThreadDecoding(t);
        }
    }

    public synchronized void cancelThreadDecoding(ThreadSet threads) {
        for (Thread t : threads) {
            cancelThreadDecoding(t);
        }
    }

    /**
     * The following three methods are used to keep track of which thread is
     * being disabled for bitmap decoding.
     */
    public synchronized boolean canThreadDecoding(Thread t) {
        ThreadStatus status = mThreadStatus.get(t);
        if (status == null) {
            // allow decoding by default
            return true;
        }

        return (status.mState != State.CANCEL);
    }

    public synchronized void allowThreadDecoding(Thread t) {
        getOrCreateThreadStatus(t).mState = State.ALLOW;
    }

    public synchronized void cancelThreadDecoding(Thread t) {
        ThreadStatus status = getOrCreateThreadStatus(t);
        status.mState = State.CANCEL;
        if (status.mOptions != null) {
            status.mOptions.requestCancelDecode();
        }

        // Wake up threads in waiting list
        notifyAll();
    }

    public static synchronized BitmapManager instance() {
        if (sManager == null) {
            sManager = new BitmapManager();
        }
        return sManager;
    }

    /**
     * The real place to delegate bitmap decoding to BitmapFactory.
     */
    public Bitmap decodeFileDescriptor(FileDescriptor fd, BitmapFactory.Options options) {
        if (options.mCancel) {
            return null;
        }

        Thread thread = Thread.currentThread();
        if (!canThreadDecoding(thread)) {
            return null;
        }

        setDecodingOptions(thread, options);
        Bitmap b = BitmapFactory.decodeFileDescriptor(fd, null, options);

        removeDecodingOptions(thread);
        return b;
    }
}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.fonts;

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Typeface;

import com.onegravity.rteditor.utils.io.FilenameUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;

/**
 * This class retrieves fonts from the assets and the Android system folders.
 */
public class FontManager {

    private static final String[] FONT_DIRS = {"/system/fonts", "/system/font", "/data/fonts"};

    private static final Map<String, String> ASSET_FONTS_BY_NAME = new TreeMap<String, String>();
    private static final Map<String, String> SYSTEM_FONTS_BY_PATH = new TreeMap<String, String>();
    private static final Map<String, String> SYSTEM_FONTS_BY_NAME = new TreeMap<String, String>();

    /*
     * Don't load the same font more than once -> cache them here.
     */
    private static final RTTypefaceSet ALL_FONTS = new RTTypefaceSet() {
        /**
         * @return The RTTypeface with the specified name or false if no such RTTypeface exists.
         */
        RTTypeface get(String fontName) {
            for (RTTypeface typeface : this) {
                if (typeface.getName().equals(fontName)) {
                    return typeface;
                }
            }
            return null;
        }

        /**
         * @return True if the collections contains an RTTypeface with the specified name, false otherwise.
         */
        boolean contains(String fontName) {
            return get(fontName) != null;
        }
    };

    /**
     * Use this method to preload fonts asynchronously e.g. when the app starts up.
     */
    public static void preLoadFonts(final Context context) {
        new Thread(() -> {
            synchronized (ASSET_FONTS_BY_NAME) {
                getAssetFonts(context);
            }
            synchronized (SYSTEM_FONTS_BY_NAME) {
                getSystemFonts();
            }
        }).start();
    }

    /**
     * Retrieve the fonts from the asset and the system folder.
     *
     * @return A Map mapping the name of the font to the Typeface.
     * If the name can't be retrieved the file name will be used (e.g. arial.ttf).
     */
    public static SortedSet<RTTypeface> getFonts(Context context) {
        /*
         * Fonts from the assets folder
         */
        Map<String, String> assetFonts = getAssetFonts(context);
        AssetManager assets = context.getResources().getAssets();
        for (String fontName : assetFonts.keySet()) {
            String filePath = assetFonts.get(fontName);
            if (!ALL_FONTS.contains(fontName)) {
                try {
                    Typeface typeface = Typeface.createFromAsset(assets, filePath);
                    ALL_FONTS.add(new RTTypeface(fontName, typeface));
                }
                catch (Exception e) {
                    // this can happen if we don't have access to the font or it's not a font or...
                }
            }
        }

        /*
         * Fonts from the system
         */
        Map<String, String> systemFonts = getSystemFonts();
        for (String fontName : systemFonts.keySet()) {
            String filePath = systemFonts.get(fontName);
            if (!ALL_FONTS.contains(fontName)) {
                try {
                    Typeface typeface = Typeface.createFromFile(filePath);
                    ALL_FONTS.add(new RTTypeface(fontName, typeface));
                }
                catch (Exception e) {
                    // this can happen if we don't have access to the font or it's not a font or...
                }
            }
        }

        return ALL_FONTS;
    }

    /**
     * Returns the RTTypeface for a specific font identified by name.
     * This assumes that the fonts have already been loaded.
     *
     * @return The RTTypeface for a specific font name or Null of no such font exists.
     */
    public static RTTypeface getTypeface(String fontName) {
        return ALL_FONTS.get(fontName);
    }

    /**
     * Retrieve the fonts from the asset folder.
     *
     * @return A Map mapping name of the font to the file path.
     * If the name can't be retrieved the file name will be used (e.g. arial.ttf).
     */
    private static Map<String, String> getAssetFonts(Context context) {
        synchronized (ASSET_FONTS_BY_NAME) {
            /*
             * Let's do this only once because it's expensive and the result won't change in any case.
             */
            if (ASSET_FONTS_BY_NAME.isEmpty()) {
                AssetManager assets = context.getResources().getAssets();

                Collection<String> fontFiles = AssetIndex.getAssetIndex(context);
                if (fontFiles == null || fontFiles.isEmpty()) {
                    fontFiles = listFontFiles(context.getResources());
                }

                for (String filePath : fontFiles) {
                    if (filePath.toLowerCase(Locale.getDefault()).endsWith("ttf")) {
                        String fontName = TTFAnalyzer.getFontName(assets, filePath);
                        if (fontName == null) {
                            fontName = getFileName(filePath);
                        }
                        ASSET_FONTS_BY_NAME.put(fontName, filePath);
                    }
                }
            }

            return ASSET_FONTS_BY_NAME;
        }
    }

    private static Collection<String> listFontFiles(Resources res) {
        Collection<String> fonts = new ArrayList<String>();
        listFontFiles(res.getAssets(), fonts, "", 0);
        return fonts;
    }

    private static void listFontFiles(AssetManager assets, Collection<String> fonts, String path,
                                      int level) {
        if (level >= 8) {
            // on certain devices we run into stack overflows because of the recursion
            // --> limit the depth of the recursion to 8 calls
            return;
        }
        try {
            String[] list = assets.list(path);
            if (list != null && list.length > 0) {
                // it's a folder
                for (String file : list) {
                    String prefix = "".equals(path) ? "" : path + File.separator;
                    listFontFiles(assets, fonts, prefix + file, ++level);
                }
            } else if (path.endsWith("ttf")) {
                // it's a font file
                fonts.add(path);
            }
        } catch (IOException ignore) {
        }
    }

    /**
     * Retrieve the fonts from the system folders.
     *
     * @return A Map mapping name of the font to the file path.
     * If the name can't be retrieved the file name will be used (e.g. arial.ttf).
     */
    private static Map<String, String> getSystemFonts() {
        synchronized (SYSTEM_FONTS_BY_NAME) {
            for (String fontDir : FONT_DIRS) {
                File dir = new File(fontDir);

                if (!dir.exists()) continue;

                File[] files = dir.listFiles();

                if (files == null) continue;

                for (File file : files) {
                    String filePath = file.getAbsolutePath();
                    if (!SYSTEM_FONTS_BY_PATH.containsKey(filePath)) {
                        String fontName = TTFAnalyzer.getFontName(file.getAbsolutePath());
                        if (fontName == null) {
                            fontName = getFileName(filePath);
                        }
                        SYSTEM_FONTS_BY_PATH.put(filePath, fontName);
                        SYSTEM_FONTS_BY_NAME.put(fontName, filePath);
                    }
                }
            }

            return SYSTEM_FONTS_BY_NAME;
        }
    }

    private static String getFileName(String path) {
        return FilenameUtils.getBaseName(path).replace(File.pathSeparator, "");
    }

}