im.neon.util.VectorUtils.java Source code

Java tutorial

Introduction

Here is the source code for im.neon.util.VectorUtils.java

Source

/*
 * Copyright 2016 OpenMarket Ltd
 *
 * 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 im.neon.util;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.v4.util.LruCache;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewParent;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.Toast;

import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.call.MXCallsManager;
import org.matrix.androidsdk.data.Room;
import org.matrix.androidsdk.data.RoomState;
import org.matrix.androidsdk.db.MXMediasCache;
import org.matrix.androidsdk.rest.callback.ApiCallback;
import org.matrix.androidsdk.rest.callback.SimpleApiCallback;
import org.matrix.androidsdk.rest.model.MatrixError;
import org.matrix.androidsdk.rest.model.PublicRoom;
import org.matrix.androidsdk.rest.model.RoomMember;
import org.matrix.androidsdk.rest.model.User;
import org.matrix.androidsdk.util.ImageUtils;
import org.matrix.androidsdk.util.Log;

import java.io.InputStream;
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.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import im.neon.R;
import im.neon.VectorApp;
import im.neon.adapters.ParticipantAdapterItem;

public class VectorUtils {

    private static final String LOG_TAG = "VectorUtils";

    //public static final int REQUEST_FILES = 0;
    public static final int TAKE_IMAGE = 1;

    //==============================================================================================================
    // permalink methods
    //==============================================================================================================

    /**
     * Provides a permalink for a room id and an eventId.
     * The eventId is optional.
     *
     * @param roomIdOrAlias the room id or alias.
     * @param eventId       the event id (optional)
     * @return the permalink
     */
    public static String getPermalink(String roomIdOrAlias, String eventId) {
        if (TextUtils.isEmpty(roomIdOrAlias)) {
            return null;
        }

        String link = "https://matrix.to/#/" + roomIdOrAlias;

        if (!TextUtils.isEmpty(eventId)) {
            link += "/" + eventId;
        }

        // the $ character is not as a part of an url so escape it.
        return link.replace("$", "%24");
    }

    //==============================================================================================================
    // Clipboard helper
    //==============================================================================================================

    /**
     * Copy a text to the clipboard.
     *
     * @param context the context
     * @param text    the text to copy
     */
    public static void copyToClipboard(Context context, CharSequence text) {
        ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
        clipboard.setPrimaryClip(ClipData.newPlainText("", text));
        Toast.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show();
    }

    //==============================================================================================================
    // Rooms methods
    //==============================================================================================================

    /**
     * Returns the public room display name..
     *
     * @param publicRoom the public room.
     * @return the room display name.
     */
    public static String getPublicRoomDisplayName(PublicRoom publicRoom) {
        String displayName = publicRoom.name;

        if (TextUtils.isEmpty(displayName)) {
            if (publicRoom.getAliases().size() > 0) {
                displayName = publicRoom.getAliases().get(0);
            } else {
                displayName = publicRoom.roomId;
            }
        } else if (!displayName.startsWith("#") && (0 < publicRoom.getAliases().size())) {
            displayName = displayName + " (" + publicRoom.getAliases().get(0) + ")";
        }

        return displayName;
    }

    /**
     * Provide a display name for a calling room
     *
     * @param context the application context.
     * @param session the room session.
     * @param room    the room.
     * @return the calling room display name.
     */
    public static String getCallingRoomDisplayName(Context context, MXSession session, Room room) {
        if ((null == context) || (null == session) || (null == room)) {
            return null;
        }

        Collection<RoomMember> roomMembers = room.getJoinedMembers();

        if (2 == roomMembers.size()) {
            ArrayList<RoomMember> roomMembersList = new ArrayList<>(roomMembers);

            if (TextUtils.equals(roomMembersList.get(0).getUserId(), session.getMyUserId())) {
                return room.getLiveState().getMemberName(roomMembersList.get(1).getUserId());
            } else {
                return room.getLiveState().getMemberName(roomMembersList.get(0).getUserId());
            }
        } else {
            return getRoomDisplayName(context, session, room);
        }
    }

    /**
     * Vector client formats the room display with a different manner than the SDK one.
     *
     * @param context the application context.
     * @param session the room session.
     * @param room    the room.
     * @return the room display name.
     */
    public static String getRoomDisplayName(Context context, MXSession session, Room room) {
        // sanity checks
        if (null == room) {
            return null;
        }

        // this algorithm is the one defined in
        // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
        // calculateRoomName(room, userId)

        RoomState roomState = room.getLiveState();

        if (!TextUtils.isEmpty(roomState.name)) {
            return roomState.name;
        }

        String alias = roomState.alias;

        if (TextUtils.isEmpty(alias) && (roomState.getAliases().size() > 0)) {
            alias = roomState.getAliases().get(0);
        }

        if (!TextUtils.isEmpty(alias)) {
            return alias;
        }

        String myUserId = session.getMyUserId();

        Collection<RoomMember> members = roomState.getDisplayableMembers();
        ArrayList<RoomMember> othersActiveMembers = new ArrayList<>();
        ArrayList<RoomMember> activeMembers = new ArrayList<>();

        for (RoomMember member : members) {
            if (!TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_LEAVE)) {
                if (!TextUtils.equals(member.getUserId(), myUserId)) {
                    othersActiveMembers.add(member);
                }
                activeMembers.add(member);
            }
        }

        Collections.sort(othersActiveMembers, new Comparator<RoomMember>() {
            @Override
            public int compare(RoomMember m1, RoomMember m2) {
                long diff = m1.getOriginServerTs() - m2.getOriginServerTs();

                return (diff == 0) ? 0 : ((diff < 0) ? -1 : +1);
            }
        });

        String displayName = "";

        if (othersActiveMembers.size() == 0) {
            if (activeMembers.size() == 1) {
                RoomMember member = activeMembers.get(0);

                if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_INVITE)) {

                    if (!TextUtils.isEmpty(member.getInviterId())) {
                        // extract who invited us to the room
                        displayName = context.getString(R.string.room_displayname_invite_from,
                                roomState.getMemberName(member.getInviterId()));
                    } else {
                        displayName = context.getString(R.string.room_displayname_room_invite);
                    }
                } else {
                    displayName = context.getString(R.string.room_displayname_no_title);
                }
            }
        } else if (othersActiveMembers.size() == 1) {
            RoomMember member = othersActiveMembers.get(0);
            displayName = roomState.getMemberName(member.getUserId());
        } else if (othersActiveMembers.size() == 2) {
            RoomMember member1 = othersActiveMembers.get(0);
            RoomMember member2 = othersActiveMembers.get(1);

            displayName = context.getString(R.string.room_displayname_two_members,
                    roomState.getMemberName(member1.getUserId()), roomState.getMemberName(member2.getUserId()));
        } else {
            RoomMember member = othersActiveMembers.get(0);
            displayName = context.getString(R.string.room_displayname_more_than_two_members,
                    roomState.getMemberName(member.getUserId()), othersActiveMembers.size() - 1);
        }

        return displayName;
    }

    //==============================================================================================================
    // Avatars generation
    //==============================================================================================================

    // avatars cache
    static final private LruCache<String, Bitmap> mAvatarImageByKeyDict = new LruCache<>(20 * 1024 * 1024);
    // the avatars background color
    static final private ArrayList<Integer> mColorList = new ArrayList<>(
            Arrays.asList(0xff76cfa6, 0xff50e2c2, 0xfff4c371));

    /**
     * Provides the avatar background color from a text.
     *
     * @param text the text.
     * @return the color.
     */
    public static int getAvatarColor(String text) {
        long colorIndex = 0;

        if (!TextUtils.isEmpty(text)) {
            long sum = 0;

            for (int i = 0; i < text.length(); i++) {
                sum += text.charAt(i);
            }

            colorIndex = sum % mColorList.size();
        }

        return mColorList.get((int) colorIndex);
    }

    /**
     * Create a thumbnail avatar.
     *
     * @param context         the context
     * @param backgroundColor the background color
     * @param text            the text to display.
     * @return the generated bitmap
     */
    private static Bitmap createAvatarThumbnail(Context context, int backgroundColor, String text) {
        float densityScale = context.getResources().getDisplayMetrics().density;
        // the avatar size is 42dp, convert it in pixels.
        return createAvatar(backgroundColor, text, (int) (42 * densityScale));
    }

    /**
     * Create an avatar bitmap from a text.
     *
     * @param backgroundColor the background color.
     * @param text            the text to display.
     * @param pixelsSide      the avatar side in pixels
     * @return the generated bitmap
     */
    private static Bitmap createAvatar(int backgroundColor, String text, int pixelsSide) {
        android.graphics.Bitmap.Config bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;

        Bitmap bitmap = Bitmap.createBitmap(pixelsSide, pixelsSide, bitmapConfig);
        Canvas canvas = new Canvas(bitmap);

        canvas.drawColor(backgroundColor);

        // prepare the text drawing
        Paint textPaint = new Paint();
        textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
        textPaint.setColor(Color.WHITE);
        // the text size is proportional to the avatar size.
        // by default, the avatar size is 42dp, the text size is 28 dp (not sp because it has to be fixed).
        textPaint.setTextSize(pixelsSide * 2 / 3);

        // get its size
        Rect textBounds = new Rect();
        textPaint.getTextBounds(text, 0, text.length(), textBounds);

        // draw the text in center
        canvas.drawText(text, (canvas.getWidth() - textBounds.width() - textBounds.left) / 2,
                (canvas.getHeight() + textBounds.height() - textBounds.bottom) / 2, textPaint);

        // Return the avatar
        return bitmap;
    }

    /**
     * Return the char to display for a name
     *
     * @param name the name
     * @return teh first char
     */
    private static String getInitialLetter(String name) {
        String firstChar = " ";

        if (!TextUtils.isEmpty(name)) {
            int idx = 0;
            char initial = name.charAt(idx);

            if ((initial == '@' || initial == '#') && (name.length() > 1)) {
                idx++;
            }

            // string.codePointAt(0) would do this, but that isn't supported by
            // some browsers (notably PhantomJS).
            int chars = 1;
            char first = name.charAt(idx);

            // LEFT-TO-RIGHT MARK
            if ((name.length() >= 2) && (0x200e == first)) {
                idx++;
                first = name.charAt(idx);
            }

            // check if its the start of a surrogate pair
            if (first >= 0xD800 && first <= 0xDBFF && (name.length() > (idx + 1))) {
                char second = name.charAt(idx + 1);
                if (second >= 0xDC00 && second <= 0xDFFF) {
                    chars++;
                }
            }

            firstChar = name.substring(idx, idx + chars);
        }

        return firstChar.toUpperCase();
    }

    /**
     * Returns an avatar from a text.
     *
     * @param context the context.
     * @param aText   the text.
     * @param create  create the avatar if it does not exist
     * @return the avatar.
     */
    public static Bitmap getAvatar(Context context, int backgroundColor, String aText, boolean create) {
        String firstChar = getInitialLetter(aText);
        String key = firstChar + "_" + backgroundColor;

        // check if the avatar is already defined
        Bitmap thumbnail = mAvatarImageByKeyDict.get(key);

        if ((null == thumbnail) && create) {
            thumbnail = VectorUtils.createAvatarThumbnail(context, backgroundColor, firstChar);
            mAvatarImageByKeyDict.put(key, thumbnail);
        }

        return thumbnail;
    }

    /**
     * Set the default vector avatar for a member.
     *
     * @param imageView   the imageView to set.
     * @param userId      the member userId.
     * @param displayName the member display name.
     */
    private static void setDefaultMemberAvatar(final ImageView imageView, final String userId,
            final String displayName) {
        // sanity checks
        if (null != imageView && !TextUtils.isEmpty(userId)) {
            final Bitmap bitmap = VectorUtils.getAvatar(imageView.getContext(), VectorUtils.getAvatarColor(userId),
                    TextUtils.isEmpty(displayName) ? userId : displayName, true);

            if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
                imageView.setImageBitmap(bitmap);
            } else {
                final String tag = userId + " - " + displayName;
                imageView.setTag(tag);

                mUIHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (TextUtils.equals(tag, (String) imageView.getTag())) {
                            imageView.setImageBitmap(bitmap);
                        }
                    }
                });
            }
        }
    }

    /**
     * Set the default vector room avatar.
     *
     * @param imageView   the image view.
     * @param roomId      the room id.
     * @param displayName the room display name.
     */
    public static void setDefaultRoomVectorAvatar(ImageView imageView, String roomId, String displayName) {
        VectorUtils.setDefaultMemberAvatar(imageView, roomId, displayName);
    }

    /**
     * Set the room avatar in an imageView.
     *
     * @param context   the context
     * @param session   the session
     * @param imageView the image view
     * @param room      the room
     */
    public static void loadRoomAvatar(Context context, MXSession session, ImageView imageView, Room room) {
        if (null != room) {
            VectorUtils.loadUserAvatar(context, session, imageView, room.getAvatarUrl(), room.getRoomId(),
                    VectorUtils.getRoomDisplayName(context, session, room));
        }
    }

    /**
     * Set the call avatar in an imageView.
     *
     * @param context   the context
     * @param session   the session
     * @param imageView the image view
     * @param room      the room
     */
    public static void loadCallAvatar(Context context, MXSession session, ImageView imageView, Room room) {
        // sanity check
        if ((null != room) && (null != session) && (null != imageView) && session.isAlive()) {
            // reset the imageView tag
            imageView.setTag(null);

            String callAvatarUrl = room.getCallAvatarUrl();
            String roomId = room.getRoomId();
            String displayName = VectorUtils.getRoomDisplayName(context, session, room);
            int pixelsSide = imageView.getLayoutParams().width;

            // when size < 0, it means that the render graph must compute it
            // so, we search the valid parent view with valid size
            if (pixelsSide < 0) {
                ViewParent parent = imageView.getParent();

                while ((pixelsSide < 0) && (null != parent)) {
                    if (parent instanceof View) {
                        View parentAsView = (View) parent;
                        pixelsSide = parentAsView.getLayoutParams().width;
                    }
                    parent = parent.getParent();
                }
            }

            // if the avatar is already cached, use it
            if (session.getMediasCache().isAvatarThumbnailCached(callAvatarUrl,
                    context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size))) {
                session.getMediasCache().loadAvatarThumbnail(session.getHomeserverConfig(), imageView,
                        callAvatarUrl, context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size));
            } else {
                Bitmap bitmap = null;

                if (pixelsSide > 0) {
                    // get the avatar bitmap.
                    bitmap = VectorUtils.createAvatar(VectorUtils.getAvatarColor(roomId),
                            getInitialLetter(displayName), pixelsSide);
                }

                // until the dedicated avatar is loaded.
                session.getMediasCache().loadAvatarThumbnail(session.getHomeserverConfig(), imageView,
                        callAvatarUrl, context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size),
                        bitmap);
            }
        }
    }

    /**
     * Set the room member avatar in an imageView.
     *
     * @param context    the context
     * @param session    the session
     * @param imageView  the image view
     * @param roomMember the room member
     */
    public static void loadRoomMemberAvatar(Context context, MXSession session, ImageView imageView,
            RoomMember roomMember) {
        if (null != roomMember) {
            VectorUtils.loadUserAvatar(context, session, imageView, roomMember.avatarUrl, roomMember.getUserId(),
                    roomMember.displayname);
        }
    }

    /**
     * Set the user avatar in an imageView.
     *
     * @param context   the context
     * @param session   the session
     * @param imageView the image view
     * @param user      the user
     */
    public static void loadUserAvatar(Context context, MXSession session, ImageView imageView, User user) {
        if (null != user) {
            VectorUtils.loadUserAvatar(context, session, imageView, user.getAvatarUrl(), user.user_id,
                    user.displayname);
        }
    }

    // the background thread
    private static HandlerThread mImagesThread = null;
    private static android.os.Handler mImagesThreadHandler = null;
    private static Handler mUIHandler = null;

    /**
     * Set the user avatar in an imageView.
     *
     * @param context     the context
     * @param session     the session
     * @param imageView   the image view
     * @param avatarUrl   the avatar url
     * @param userId      the user id
     * @param displayName the user display name
     */
    public static void loadUserAvatar(final Context context, final MXSession session, final ImageView imageView,
            final String avatarUrl, final String userId, final String displayName) {
        // sanity check
        if ((null == session) || (null == imageView) || !session.isAlive()) {
            return;
        }

        // reset the imageView tag
        imageView.setTag(null);

        if (session.getMediasCache().isAvatarThumbnailCached(avatarUrl,
                context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size))) {
            session.getMediasCache().loadAvatarThumbnail(session.getHomeserverConfig(), imageView, avatarUrl,
                    context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size));
        } else {
            if (null == mImagesThread) {
                mImagesThread = new HandlerThread("ImagesThread", Thread.MIN_PRIORITY);
                mImagesThread.start();
                mImagesThreadHandler = new android.os.Handler(mImagesThread.getLooper());
                mUIHandler = new Handler(Looper.getMainLooper());
            }

            final Bitmap bitmap = VectorUtils.getAvatar(imageView.getContext(), VectorUtils.getAvatarColor(userId),
                    TextUtils.isEmpty(displayName) ? userId : displayName, false);

            // test if the default avatar has been computed
            if (null != bitmap) {
                imageView.setImageBitmap(bitmap);

                final String tag = avatarUrl + userId + displayName;
                imageView.setTag(tag);

                if (!MXMediasCache.isMediaUrlUnreachable(avatarUrl)) {
                    mImagesThreadHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (TextUtils.equals(tag, (String) imageView.getTag())) {
                                session.getMediasCache().loadAvatarThumbnail(session.getHomeserverConfig(),
                                        imageView, avatarUrl,
                                        context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size),
                                        bitmap);
                            }
                        }
                    });
                }
            } else {
                final String tmpTag0 = "00" + avatarUrl + "-" + userId + "--" + displayName;
                imageView.setTag(tmpTag0);

                // create the default avatar in the background thread
                mImagesThreadHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (TextUtils.equals(tmpTag0, (String) imageView.getTag())) {
                            imageView.setTag(null);
                            setDefaultMemberAvatar(imageView, userId, displayName);

                            if (!MXMediasCache.isMediaUrlUnreachable(avatarUrl)) {
                                final String tmpTag1 = "11" + avatarUrl + "-" + userId + "--" + displayName;
                                imageView.setTag(tmpTag1);

                                // wait that it is rendered to load the right one
                                mUIHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        // test if the imageView tag has not been updated
                                        if (TextUtils.equals(tmpTag1, (String) imageView.getTag())) {
                                            final String tmptag2 = "22" + avatarUrl + userId + displayName;
                                            imageView.setTag(tmptag2);

                                            mImagesThreadHandler.post(new Runnable() {
                                                @Override
                                                public void run() {
                                                    // test if the imageView tag has not been updated
                                                    if (TextUtils.equals(tmptag2, (String) imageView.getTag())) {
                                                        final Bitmap bitmap = VectorUtils.getAvatar(
                                                                imageView.getContext(),
                                                                VectorUtils.getAvatarColor(userId),
                                                                TextUtils.isEmpty(displayName) ? userId
                                                                        : displayName,
                                                                false);
                                                        session.getMediasCache().loadAvatarThumbnail(
                                                                session.getHomeserverConfig(), imageView, avatarUrl,
                                                                context.getResources().getDimensionPixelSize(
                                                                        R.dimen.profile_avatar_size),
                                                                bitmap);
                                                    }
                                                }
                                            });
                                        }
                                    }
                                });
                            }
                        }
                    }
                });
            }
        }
    }

    //==============================================================================================================
    // About / terms and conditions
    //==============================================================================================================

    private static AlertDialog mMainAboutDialog = null;

    /**
     * Provide the application version
     *
     * @param context the application context
     * @return the version. an empty string is not found.
     */
    public static String getApplicationVersion(final Context context) {
        return im.neon.Matrix.getInstance(context).getVersion(false);
    }

    /**
     * Display the licenses text.
     */
    public static void displayThirdPartyLicenses() {
        final Activity activity = VectorApp.getCurrentActivity();

        if (null != activity) {
            if (null != mMainAboutDialog) {
                if (mMainAboutDialog.isShowing() && (null != mMainAboutDialog.getOwnerActivity())) {
                    try {
                        mMainAboutDialog.dismiss();
                    } catch (Exception e) {
                        Log.e(LOG_TAG, "## displayThirdPartyLicenses() : " + e.getMessage());
                    }
                }
                mMainAboutDialog = null;
            }

            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    WebView view = (WebView) LayoutInflater.from(activity).inflate(R.layout.dialog_licenses, null);
                    view.loadUrl("file:///android_asset/open_source_licenses.html");

                    View titleView = LayoutInflater.from(activity).inflate(R.layout.dialog_licenses_header, null);

                    view.setScrollbarFadingEnabled(false);
                    mMainAboutDialog = new AlertDialog.Builder(activity).setCustomTitle(titleView).setView(view)
                            .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    mMainAboutDialog = null;
                                }
                            }).setOnCancelListener(new DialogInterface.OnCancelListener() {
                                @Override
                                public void onCancel(DialogInterface dialog) {
                                    mMainAboutDialog = null;
                                }
                            }).create();

                    mMainAboutDialog.show();
                }
            });
        }
    }

    /**
     * Open a webview above the current activity.
     *
     * @param context the application context
     * @param url     the url to open
     */
    private static void displayInWebview(final Context context, String url) {
        AlertDialog.Builder alert = new AlertDialog.Builder(context);

        WebView wv = new WebView(context);
        wv.loadUrl(url);
        wv.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);

                return true;
            }
        });

        alert.setView(wv);
        alert.setPositiveButton(android.R.string.ok, null);
        alert.show();
    }

    /**
     * Display the term and conditions.
     */
    public static void displayAppTac() {
        if (null != VectorApp.getCurrentActivity()) {
            displayInWebview(VectorApp.getCurrentActivity(), "https://riot.im/tac");
        }
    }

    /**
     * Display the copyright.
     */
    public static void displayAppCopyright() {
        if (null != VectorApp.getCurrentActivity()) {
            displayInWebview(VectorApp.getCurrentActivity(), "https://riot.im/copyright");
        }
    }

    /**
     * Display the privacy policy.
     */
    public static void displayAppPrivacyPolicy() {
        if (null != VectorApp.getCurrentActivity()) {
            displayInWebview(VectorApp.getCurrentActivity(), "https://riot.im/privacy");
        }
    }

    //==============================================================================================================
    // List uris from intent
    //==============================================================================================================

    /**
     * Return a selected bitmap from an intent.
     *
     * @param intent the intent
     * @return the bitmap uri
     */
    @SuppressLint("NewApi")
    public static Uri getThumbnailUriFromIntent(Context context, final Intent intent, MXMediasCache mediasCache) {
        // sanity check
        if ((null != intent) && (null != context) && (null != mediasCache)) {
            Uri thumbnailUri = null;
            ClipData clipData = null;

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                clipData = intent.getClipData();
            }

            // multiple data
            if (null != clipData) {
                if (clipData.getItemCount() > 0) {
                    thumbnailUri = clipData.getItemAt(0).getUri();
                }
            } else if (null != intent.getData()) {
                thumbnailUri = intent.getData();
            }

            if (null != thumbnailUri) {
                try {
                    ResourceUtils.Resource resource = ResourceUtils.openResource(context, thumbnailUri, null);

                    // sanity check
                    if ((null != resource) && resource.isJpegResource()) {
                        InputStream stream = resource.mContentStream;
                        int rotationAngle = ImageUtils.getRotationAngleForBitmap(context, thumbnailUri);

                        String mediaUrl = ImageUtils.scaleAndRotateImage(context, stream, resource.mMimeType, 1024,
                                rotationAngle, mediasCache);
                        thumbnailUri = Uri.parse(mediaUrl);
                    }

                    return thumbnailUri;

                } catch (Exception e) {
                    Log.e(LOG_TAG, "## etThumbnailUriFromIntent failed " + e.getMessage());
                }
            }
        }

        return null;
    }

    //==============================================================================================================
    // User presence
    //==============================================================================================================

    /**
     * Format a time interval in seconds to a string
     *
     * @param context         the context.
     * @param secondsInterval the time interval.
     * @return the formatted string
     */
    private static String formatSecondsIntervalFloored(Context context, long secondsInterval) {
        String formattedString;

        if (secondsInterval < 0) {
            formattedString = "0" + context.getResources().getString(R.string.format_time_s);
        } else {
            if (secondsInterval < 60) {
                formattedString = secondsInterval + context.getResources().getString(R.string.format_time_s);
            } else if (secondsInterval < 3600) {
                formattedString = (secondsInterval / 60) + context.getResources().getString(R.string.format_time_m);
            } else if (secondsInterval < 86400) {
                formattedString = (secondsInterval / 3600)
                        + context.getResources().getString(R.string.format_time_h);
            } else {
                formattedString = (secondsInterval / 86400)
                        + context.getResources().getString(R.string.format_time_d);
            }
        }

        return formattedString;
    }

    /**
     * Provide the user online status from his user Id.
     * if refreshCallback is set, try to refresh the user presence if it is not known
     *
     * @param context         the context.
     * @param session         the session.
     * @param userId          the userId.
     * @param refreshCallback the presence callback.
     * @return the online status description.
     */
    public static String getUserOnlineStatus(final Context context, final MXSession session, final String userId,
            final SimpleApiCallback<Void> refreshCallback) {
        // sanity checks
        if ((null == session) || (null == userId)) {
            return null;
        }

        final User user = session.getDataHandler().getStore().getUser(userId);

        // refresh the presence with this conditions
        boolean triggerRefresh = (null == user) || user.isPresenceObsolete();

        if ((null != refreshCallback) && triggerRefresh) {
            Log.d(LOG_TAG, "Get the user presence : " + userId);

            final String fPresence = (null != user) ? user.presence : null;

            session.refreshUserPresence(userId, new ApiCallback<Void>() {
                @Override
                public void onSuccess(Void info) {
                    boolean isUpdated = false;
                    User updatedUser = session.getDataHandler().getStore().getUser(userId);

                    // don't find any info for the user
                    if ((null == user) && (null == updatedUser)) {
                        Log.d(LOG_TAG, "Don't find any presence info of " + userId);
                    } else if ((null == user) && (null != updatedUser)) {
                        Log.d(LOG_TAG, "Got the user presence : " + userId);
                        isUpdated = true;
                    } else if (!TextUtils.equals(fPresence, updatedUser.presence)) {
                        isUpdated = true;
                        Log.d(LOG_TAG, "Got some new user presence info : " + userId);
                        Log.d(LOG_TAG, "currently_active : " + updatedUser.currently_active);
                        Log.d(LOG_TAG, "lastActiveAgo : " + updatedUser.lastActiveAgo);
                    }

                    if (isUpdated && (null != refreshCallback)) {
                        try {
                            refreshCallback.onSuccess(null);
                        } catch (Exception e) {
                            Log.e(LOG_TAG, "getUserOnlineStatus refreshCallback failed");
                        }
                    }
                }

                @Override
                public void onNetworkError(Exception e) {
                    Log.e(LOG_TAG, "getUserOnlineStatus onNetworkError " + e.getLocalizedMessage());
                }

                @Override
                public void onMatrixError(MatrixError e) {
                    Log.e(LOG_TAG, "getUserOnlineStatus onMatrixError " + e.getLocalizedMessage());
                }

                @Override
                public void onUnexpectedError(Exception e) {
                    Log.e(LOG_TAG, "getUserOnlineStatus onUnexpectedError " + e.getLocalizedMessage());
                }
            });
        }

        // unknown user
        if (null == user) {
            return null;
        }

        String presenceText = null;
        if (TextUtils.equals(user.presence, User.PRESENCE_ONLINE)) {
            presenceText = context.getResources().getString(R.string.room_participants_online);
        } else if (TextUtils.equals(user.presence, User.PRESENCE_UNAVAILABLE)) {
            presenceText = context.getResources().getString(R.string.room_participants_idle);
        } else if (TextUtils.equals(user.presence, User.PRESENCE_OFFLINE) || (null == user.presence)) {
            presenceText = context.getResources().getString(R.string.room_participants_offline);
        }

        if (presenceText != null) {
            if ((null != user.currently_active) && user.currently_active) {
                presenceText += " " + context.getResources().getString(R.string.room_participants_now);
            } else if ((null != user.lastActiveAgo) && (user.lastActiveAgo > 0)) {
                presenceText += " " + formatSecondsIntervalFloored(context, user.getAbsoluteLastActiveAgo() / 1000L)
                        + " " + context.getResources().getString(R.string.room_participants_ago);
            }
        }

        return presenceText;
    }

    //==============================================================================================================
    // Users list
    //==============================================================================================================

    /**
     * List the active users i.e the active rooms users (invited or joined) and the contacts with matrix id emails.
     * This function could require a long time to process so it should be called in background.
     *
     * @param session the session.
     * @return a map indexed by the matrix id.
     */
    public static Map<String, ParticipantAdapterItem> listKnownParticipants(MXSession session) {
        // a hash map is a lot faster than a list search
        Map<String, ParticipantAdapterItem> map = new HashMap<>();

        // check known users
        Collection<User> users = session.getDataHandler().getStore().getUsers();

        // we don't need to populate the room members or each room
        // because an user is created for each joined / invited room member event
        for (User user : users) {
            if (!MXCallsManager.isConferenceUserId(user.user_id)) {
                map.put(user.user_id, new ParticipantAdapterItem(user));
            }
        }

        return map;
    }

    //==============================================================================================================
    // URL parser
    //==============================================================================================================

    private static final Pattern mUrlPattern = Pattern.compile(
            "(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" + "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*"
                    + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
            Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);

    /**
     * List the URLs in a text.
     *
     * @param text the text to parse
     * @return the list of URLss
     */
    public static List<String> listURLs(String text) {
        ArrayList<String> URLs = new ArrayList<>();

        // sanity checks
        if (!TextUtils.isEmpty(text)) {
            Matcher matcher = mUrlPattern.matcher(text);

            while (matcher.find()) {
                int matchStart = matcher.start(1);
                int matchEnd = matcher.end();

                String charBef = "";
                String charAfter = "";

                if (matchStart > 2) {
                    charBef = text.substring(matchStart - 2, matchStart);
                }

                if ((matchEnd - 1) < text.length()) {
                    charAfter = text.substring(matchEnd - 1, matchEnd);
                }

                // keep the link between parenthesis, it might be a link [title](link)
                if (!TextUtils.equals(charAfter, ")") || !TextUtils.equals(charBef, "](")) {
                    String url = text.substring(matchStart, matchEnd);

                    if (URLs.indexOf(url) < 0) {
                        URLs.add(url);
                    }
                }
            }
        }

        return URLs;
    }

    //==============================================================================================================
    // ExpandableListView tools
    //==============================================================================================================

    /**
     * Provides the visible child views.
     * The map key is the group position.
     * The map values are the visible child views.
     *
     * @param expandableListView the listview
     * @param adapter            the linked adapter
     * @return visible views map
     */
    public static HashMap<Integer, List<Integer>> getVisibleChildViews(ExpandableListView expandableListView,
            BaseExpandableListAdapter adapter) {
        HashMap<Integer, List<Integer>> map = new HashMap<>();

        long firstPackedPosition = expandableListView
                .getExpandableListPosition(expandableListView.getFirstVisiblePosition());

        int firstGroupPosition = ExpandableListView.getPackedPositionGroup(firstPackedPosition);
        int firstChildPosition = ExpandableListView.getPackedPositionChild(firstPackedPosition);

        long lastPackedPosition = expandableListView
                .getExpandableListPosition(expandableListView.getLastVisiblePosition());

        int lastGroupPosition = ExpandableListView.getPackedPositionGroup(lastPackedPosition);
        int lastChildPosition = ExpandableListView.getPackedPositionChild(lastPackedPosition);

        for (int groupPos = firstGroupPosition; groupPos <= lastGroupPosition; groupPos++) {
            ArrayList<Integer> list = new ArrayList<>();

            int startChildPos = (groupPos == firstGroupPosition) ? firstChildPosition : 0;
            int endChildPos = (groupPos == lastGroupPosition) ? lastChildPosition
                    : adapter.getChildrenCount(groupPos) - 1;

            for (int index = startChildPos; index <= endChildPos; index++) {
                list.add(index);
            }

            map.put(groupPos, list);
        }

        return map;
    }
}