im.vector.VectorApp.java Source code

Java tutorial

Introduction

Here is the source code for im.vector.VectorApp.java

Source

/*
 * Copyright 2014 OpenMarket Ltd
 * Copyright 2017 Vector Creations 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.vector;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.multidex.MultiDex;
import android.support.multidex.MultiDexApplication;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Pair;

import com.facebook.stetho.Stetho;

import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.util.Log;
import org.piwik.sdk.Piwik;
import org.piwik.sdk.QueryParams;
import org.piwik.sdk.TrackMe;
import org.piwik.sdk.Tracker;
import org.piwik.sdk.TrackerConfig;
import org.piwik.sdk.extra.CustomVariables;
import org.piwik.sdk.extra.TrackHelper;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import im.vector.activity.CommonActivityUtils;
import im.vector.activity.JitsiCallActivity;
import im.vector.activity.VectorCallViewActivity;
import im.vector.activity.VectorMediasPickerActivity;
import im.vector.activity.WidgetActivity;
import im.vector.contacts.ContactsManager;
import im.vector.contacts.PIDsRetriever;
import im.vector.gcm.GcmRegistrationManager;
import im.vector.services.EventStreamService;
import im.vector.util.CallsManager;
import im.vector.util.PhoneNumberUtils;
import im.vector.util.PreferencesManager;
import im.vector.util.RageShake;
import im.vector.util.ThemeUtils;
import im.vector.util.VectorMarkdownParser;

/**
 * The main application injection point
 */
public class VectorApp extends MultiDexApplication {
    private static final String LOG_TAG = VectorApp.class.getSimpleName();

    // key to save the crash status
    private static final String PREFS_CRASH_KEY = "PREFS_CRASH_KEY";

    /**
     * The current instance.
     */
    private static VectorApp instance = null;

    /**
     * Rage shake detection to send a bug report.
     */
    private RageShake mRageShake;

    /**
     * Delay to detect if the application is in background.
     * If there is no active activity during the elapsed time, it means that the application is in background.
     */
    private static final long MAX_ACTIVITY_TRANSITION_TIME_MS = 4000;

    /**
     * The current active activity
     */
    private static Activity mCurrentActivity = null;

    /**
     * Background application detection
     */
    private Timer mActivityTransitionTimer;
    private TimerTask mActivityTransitionTimerTask;
    private boolean mIsInBackground = true;

    /**
     * Google analytics information.
     */
    public static int VERSION_BUILD = -1;
    private static String VECTOR_VERSION_STRING = "";
    private static String SDK_VERSION_STRING = "";
    private static String SHORT_VERSION = "";

    /**
     * Tells if there a pending call whereas the application is backgrounded.
     */
    private boolean mIsCallingInBackground = false;

    /**
     * Monitor the created activities to detect memory leaks.
     */
    private final ArrayList<String> mCreatedActivities = new ArrayList<>();

    /**
     * Markdown parser
     */
    private VectorMarkdownParser mMarkdownParser;

    /**
     * Calls manager
     */
    private CallsManager mCallsManager;

    /**
     * @return the current instance
     */
    public static VectorApp getInstance() {
        return instance;
    }

    /**
     * The directory in which the logs are stored
     */
    public static File mLogsDirectoryFile = null;

    /**
     * The last time that removeMediasBefore has been called.
     */
    private long mLastMediasCheck = 0;

    private final BroadcastReceiver mLanguageReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (!TextUtils.equals(Locale.getDefault().toString(), getApplicationLocale().toString())) {
                Log.d(LOG_TAG, "## onReceive() : the locale has been updated to " + Locale.getDefault().toString()
                        + ", restore the expected value " + getApplicationLocale().toString());
                updateApplicationSettings(getApplicationLocale(), getFontScale(),
                        ThemeUtils.getApplicationTheme(context));

                if (null != getCurrentActivity()) {
                    restartActivity(getCurrentActivity());
                }
            }
        }
    };

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

    @Override
    public void onCreate() {
        Log.d(LOG_TAG, "onCreate");
        super.onCreate();

        if (BuildConfig.DEBUG) {
            Stetho.initializeWithDefaults(this);
        }

        instance = this;
        mCallsManager = new CallsManager(this);
        mActivityTransitionTimer = null;
        mActivityTransitionTimerTask = null;

        try {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            VERSION_BUILD = packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(LOG_TAG, "fails to retrieve the package info " + e.getMessage());
        }

        VECTOR_VERSION_STRING = Matrix.getInstance(this).getVersion(true, true);

        // not the first launch
        if (null != Matrix.getInstance(this).getDefaultSession()) {
            SDK_VERSION_STRING = Matrix.getInstance(this).getDefaultSession().getVersion(true);
        } else {
            SDK_VERSION_STRING = "";
        }

        try {
            PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            SHORT_VERSION = pInfo.versionName;
        } catch (Exception e) {
        }

        mLogsDirectoryFile = new File(getCacheDir().getAbsolutePath() + "/logs");

        org.matrix.androidsdk.util.Log.setLogDirectory(mLogsDirectoryFile);
        org.matrix.androidsdk.util.Log.init("RiotLog");

        // log the application version to trace update
        // useful to track backward compatibility issues

        Log.d(LOG_TAG, "----------------------------------------------------------------");
        Log.d(LOG_TAG, "----------------------------------------------------------------");
        Log.d(LOG_TAG, " Application version: " + VECTOR_VERSION_STRING);
        Log.d(LOG_TAG, " SDK version: " + SDK_VERSION_STRING);
        Log.d(LOG_TAG,
                " Local time: " + (new SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US)).format(new Date()));
        Log.d(LOG_TAG, "----------------------------------------------------------------");
        Log.d(LOG_TAG, "----------------------------------------------------------------\n\n\n\n");

        mRageShake = new RageShake(this);

        // init the REST client
        MXSession.initUserAgent(getApplicationContext());

        this.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            final Map<String, String> mLocalesByActivity = new HashMap<>();

            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                Log.d(LOG_TAG, "onActivityCreated " + activity);
                mCreatedActivities.add(activity.toString());
                ThemeUtils.setActivityTheme(activity);
                // piwik
                onNewScreen(activity);
            }

            @Override
            public void onActivityStarted(Activity activity) {
                Log.d(LOG_TAG, "onActivityStarted " + activity);
            }

            /**
             * Compute the locale status value
             * @param activity the activity
             * @return the local status value
             */
            private String getActivityLocaleStatus(Activity activity) {
                return getApplicationLocale().toString() + "_" + getFontScale() + "_"
                        + ThemeUtils.getApplicationTheme(activity);
            }

            @Override
            public void onActivityResumed(final Activity activity) {
                Log.d(LOG_TAG, "onActivityResumed " + activity);
                setCurrentActivity(activity);

                String activityKey = activity.toString();

                if (mLocalesByActivity.containsKey(activityKey)) {
                    String prevActivityLocale = mLocalesByActivity.get(activityKey);

                    if (!TextUtils.equals(prevActivityLocale, getActivityLocaleStatus(activity))) {
                        Log.d(LOG_TAG,
                                "## onActivityResumed() : restart the activity " + activity
                                        + " because of the locale update from " + prevActivityLocale + " to "
                                        + getActivityLocaleStatus(activity));
                        restartActivity(activity);
                        return;
                    }
                }

                // it should never happen as there is a broadcast receiver (mLanguageReceiver)
                if (!TextUtils.equals(Locale.getDefault().toString(), getApplicationLocale().toString())) {
                    Log.d(LOG_TAG,
                            "## onActivityResumed() : the locale has been updated to "
                                    + Locale.getDefault().toString() + ", restore the expected value "
                                    + getApplicationLocale().toString());
                    updateApplicationSettings(getApplicationLocale(), getFontScale(),
                            ThemeUtils.getApplicationTheme(activity));
                    restartActivity(activity);
                }

                listPermissionStatuses();
            }

            @Override
            public void onActivityPaused(Activity activity) {
                Log.d(LOG_TAG, "onActivityPaused " + activity);
                mLocalesByActivity.put(activity.toString(), getActivityLocaleStatus(activity));
                setCurrentActivity(null);
                onAppPause();
            }

            @Override
            public void onActivityStopped(Activity activity) {
                Log.d(LOG_TAG, "onActivityStopped " + activity);
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
                Log.d(LOG_TAG, "onActivitySaveInstanceState " + activity);
            }

            @Override
            public void onActivityDestroyed(Activity activity) {
                Log.d(LOG_TAG, "onActivityDestroyed " + activity);
                mCreatedActivities.remove(activity.toString());
                mLocalesByActivity.remove(activity.toString());

                if (mCreatedActivities.size() > 1) {
                    Log.d(LOG_TAG, "onActivityDestroyed : \n" + mCreatedActivities);
                }
            }
        });

        // create the markdown parser
        try {
            mMarkdownParser = new VectorMarkdownParser(this);
        } catch (Exception e) {
            // reported by GA
            Log.e(LOG_TAG, "cannot create the mMarkdownParser " + e.getMessage());
        }

        // track external language updates
        // local update from the settings
        // or screen rotation !
        VectorApp.getInstance().registerReceiver(mLanguageReceiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
        VectorApp.getInstance().registerReceiver(mLanguageReceiver,
                new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));

        PreferencesManager.fixMigrationIssues(this);
        initApplicationLocale();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (!TextUtils.equals(Locale.getDefault().toString(), getApplicationLocale().toString())) {
            Log.d(LOG_TAG,
                    "## onConfigurationChanged() : the locale has been updated to " + Locale.getDefault().toString()
                            + ", restore the expected value " + getApplicationLocale().toString());
            updateApplicationSettings(getApplicationLocale(), getFontScale(), ThemeUtils.getApplicationTheme(this));
        }
    }

    /**
     * Parse a markdown text
     *
     * @param text     the text to parse
     * @param listener the result listener
     */
    public static void markdownToHtml(final String text,
            final VectorMarkdownParser.IVectorMarkdownParserListener listener) {
        if (null != getInstance().mMarkdownParser) {
            getInstance().mMarkdownParser.markdownToHtml(text, listener);
        } else {
            (new Handler(Looper.getMainLooper())).post(new Runnable() {
                @Override
                public void run() {
                    // GA issue
                    listener.onMarkdownParsed(text, null);
                }
            });
        }
    }

    /**
     * Suspend background threads.
     */
    private void suspendApp() {
        GcmRegistrationManager gcmRegistrationManager = Matrix.getInstance(VectorApp.this)
                .getSharedGCMRegistrationManager();

        // suspend the events thread if the client uses GCM
        if (!gcmRegistrationManager.isBackgroundSyncAllowed()
                || (gcmRegistrationManager.useGCM() && gcmRegistrationManager.hasRegistrationToken())) {
            Log.d(LOG_TAG, "suspendApp ; pause the event stream");
            CommonActivityUtils.pauseEventStream(VectorApp.this);
        } else {
            Log.d(LOG_TAG, "suspendApp ; the event stream is not paused because GCM is disabled.");
        }

        // the sessions are not anymore seen as "online"
        ArrayList<MXSession> sessions = Matrix.getInstance(this).getSessions();

        for (MXSession session : sessions) {
            if (session.isAlive()) {
                session.setIsOnline(false);
                session.setSyncDelay(gcmRegistrationManager.isBackgroundSyncAllowed()
                        ? gcmRegistrationManager.getBackgroundSyncDelay()
                        : 0);
                session.setSyncTimeout(gcmRegistrationManager.getBackgroundSyncTimeOut());

                // remove older medias
                if ((System.currentTimeMillis() - mLastMediasCheck) < (24 * 60 * 60 * 1000)) {
                    mLastMediasCheck = System.currentTimeMillis();
                    session.removeMediasBefore(VectorApp.this,
                            PreferencesManager.getMinMediasLastAccessTime(getApplicationContext()));
                }

                if (session.getDataHandler().areLeftRoomsSynced()) {
                    session.getDataHandler().releaseLeftRooms();
                }
            }
        }

        clearSyncingSessions();

        PIDsRetriever.getInstance().onAppBackgrounded();

        MyPresenceManager.advertiseAllUnavailable();

        mRageShake.stop();

        onAppPause();
    }

    /**
     * Test if application is put in background.
     * i.e wait 2s before assuming that the application is put in background.
     */
    private void startActivityTransitionTimer() {
        Log.d(LOG_TAG, "## startActivityTransitionTimer()");

        try {
            mActivityTransitionTimer = new Timer();
            mActivityTransitionTimerTask = new TimerTask() {
                @Override
                public void run() {
                    // reported by GA
                    try {
                        if (mActivityTransitionTimerTask != null) {
                            mActivityTransitionTimerTask.cancel();
                            mActivityTransitionTimerTask = null;
                        }

                        if (mActivityTransitionTimer != null) {
                            mActivityTransitionTimer.cancel();
                            mActivityTransitionTimer = null;
                        }
                    } catch (Exception e) {
                        Log.e(LOG_TAG, "## startActivityTransitionTimer() failed " + e.getMessage());
                    }

                    if (null != mCurrentActivity) {
                        Log.e(LOG_TAG,
                                "## startActivityTransitionTimer() : the timer expires but there is an active activity.");
                    } else {
                        VectorApp.this.mIsInBackground = true;
                        mIsCallingInBackground = (null != mCallsManager.getActiveCall());

                        // if there is a pending call
                        // the application is not suspended
                        if (!mIsCallingInBackground) {
                            Log.d(LOG_TAG, "Suspend the application because there was no resumed activity within "
                                    + (MAX_ACTIVITY_TRANSITION_TIME_MS / 1000) + " seconds");
                            CommonActivityUtils.displayMemoryInformation(null, " app suspended");
                            suspendApp();
                        } else {
                            Log.d(LOG_TAG, "App not suspended due to call in progress");
                        }
                    }
                }
            };

            mActivityTransitionTimer.schedule(mActivityTransitionTimerTask, MAX_ACTIVITY_TRANSITION_TIME_MS);
        } catch (Throwable throwable) {
            Log.e(LOG_TAG,
                    "## startActivityTransitionTimer() : failed to start the timer " + throwable.getMessage());

            if (null != mActivityTransitionTimer) {
                mActivityTransitionTimer.cancel();
                mActivityTransitionTimer = null;
            }
        }
    }

    /**
     * List the used permissions statuses.
     */
    private void listPermissionStatuses() {
        if (Build.VERSION.SDK_INT >= 23) {
            final List<String> permissions = Arrays.asList(android.Manifest.permission.CAMERA,
                    android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    android.Manifest.permission.READ_CONTACTS);

            Log.d(LOG_TAG, "## listPermissionStatuses() : list the permissions used by the app");
            for (String permission : permissions) {
                Log.d(LOG_TAG,
                        "Status of [" + permission + "] : "
                                + ((PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(instance,
                                        permission)) ? "PERMISSION_GRANTED" : "PERMISSION_DENIED"));
            }
        }
    }

    /**
     * Stop the background detection.
     */
    private void stopActivityTransitionTimer() {
        Log.d(LOG_TAG, "## stopActivityTransitionTimer()");

        if (mActivityTransitionTimerTask != null) {
            mActivityTransitionTimerTask.cancel();
            mActivityTransitionTimerTask = null;
        }

        if (mActivityTransitionTimer != null) {
            mActivityTransitionTimer.cancel();
            mActivityTransitionTimer = null;
        }

        if (isAppInBackground() && !mIsCallingInBackground) {
            // the event stream service has been killed
            if (EventStreamService.isStopped()) {
                CommonActivityUtils.startEventStreamService(VectorApp.this);
            } else {
                CommonActivityUtils.resumeEventStream(VectorApp.this);

                // try to perform a GCM registration if it failed
                // or if the GCM server generated a new push key
                GcmRegistrationManager gcmRegistrationManager = Matrix.getInstance(this)
                        .getSharedGCMRegistrationManager();

                if (null != gcmRegistrationManager) {
                    gcmRegistrationManager.checkRegistrations();
                }
            }

            // get the contact update at application launch
            ContactsManager.getInstance().clearSnapshot();
            ContactsManager.getInstance().refreshLocalContactsSnapshot();

            List<MXSession> sessions = Matrix.getInstance(this).getSessions();
            for (MXSession session : sessions) {
                session.getMyUser().refreshUserInfos(null);
                session.setIsOnline(true);
                session.setSyncDelay(0);
                session.setSyncTimeout(0);
                addSyncingSession(session);
            }

            mCallsManager.checkDeadCalls();
            Matrix.getInstance(this).getSharedGCMRegistrationManager().onAppResume();
        }

        MyPresenceManager.advertiseAllOnline();
        mRageShake.start();

        mIsCallingInBackground = false;
        mIsInBackground = false;
    }

    /**
     * Update the current active activity.
     * It manages the application background / foreground when it is required.
     *
     * @param activity the current activity, null if there is no more one.
     */
    private void setCurrentActivity(Activity activity) {
        Log.d(LOG_TAG, "## setCurrentActivity() : from " + mCurrentActivity + " to " + activity);

        if (VectorApp.isAppInBackground() && (null != activity)) {
            Matrix matrixInstance = Matrix.getInstance(activity.getApplicationContext());

            // sanity check
            if (null != matrixInstance) {
                matrixInstance.refreshPushRules();
            }

            Log.d(LOG_TAG, "The application is resumed");
            // display the memory usage when the application is put iun foreground..
            CommonActivityUtils.displayMemoryInformation(activity, " app resumed with " + activity);
        }

        // wait 2s to check that the application is put in background
        if (null != getInstance()) {
            if (null == activity) {
                getInstance().startActivityTransitionTimer();
            } else {
                getInstance().stopActivityTransitionTimer();
            }
        } else {
            Log.e(LOG_TAG, "The application is resumed but there is no active instance");
        }

        mCurrentActivity = activity;

        if (null != mCurrentActivity) {
            KeyRequestHandler.getSharedInstance().processNextRequest();
        }
    }

    /**
     * @return the current active activity
     */
    public static Activity getCurrentActivity() {
        return mCurrentActivity;
    }

    /**
     * Return true if the application is in background.
     */
    public static boolean isAppInBackground() {
        return (null == mCurrentActivity) && (null != getInstance()) && getInstance().mIsInBackground;
    }

    /**
     * Restart an activity to manage language update
     *
     * @param activity the activity to restart
     */
    private void restartActivity(Activity activity) {
        // avoid restarting activities when it is not required
        // some of them has no text
        if (!(activity instanceof VectorMediasPickerActivity) && !(activity instanceof VectorCallViewActivity)
                && !(activity instanceof JitsiCallActivity) && !(activity instanceof WidgetActivity)) {
            activity.startActivity(activity.getIntent());
            activity.finish();
        }
    }

    //==============================================================================================================
    // cert management : store the active activities.
    //==============================================================================================================

    private final EventEmitter<Activity> mOnActivityDestroyedListener = new EventEmitter<>();

    /**
     * @return the EventEmitter list.
     */
    public EventEmitter<Activity> getOnActivityDestroyedListener() {
        return mOnActivityDestroyedListener;
    }

    //==============================================================================================================
    // Media pickers : image backup
    //==============================================================================================================

    private static Bitmap mSavedPickerImagePreview = null;

    /**
     * The image taken from the medias picker is stored in a static variable because
     * saving it would take too much time.
     *
     * @return the saved image from medias picker
     */
    public static Bitmap getSavedPickerImagePreview() {
        return mSavedPickerImagePreview;
    }

    /**
     * Save the image taken in the medias picker
     *
     * @param aSavedCameraImagePreview the bitmap.
     */
    public static void setSavedCameraImagePreview(Bitmap aSavedCameraImagePreview) {
        if (aSavedCameraImagePreview != mSavedPickerImagePreview) {
            // force to release memory
            // reported by GA
            // it seems that the medias picker might be refreshed
            // while leaving the activity
            // recycle the bitmap trigger a rendering issue
            // Canvas: trying to use a recycled bitmap...

            /*if (null != mSavedPickerImagePreview) {
            mSavedPickerImagePreview.recycle();
            mSavedPickerImagePreview = null;
            System.gc();
            }*/

            mSavedPickerImagePreview = aSavedCameraImagePreview;
        }
    }

    //==============================================================================================================
    // Syncing mxSessions
    //==============================================================================================================

    /**
     * syncing sessions
     */
    private static final HashSet<MXSession> mSyncingSessions = new HashSet<>();

    /**
     * Add a session in the syncing sessions list
     *
     * @param session the session
     */
    public static void addSyncingSession(MXSession session) {
        synchronized (mSyncingSessions) {
            mSyncingSessions.add(session);
        }
    }

    /**
     * Remove a session in the syncing sessions list
     *
     * @param session the session
     */
    public static void removeSyncingSession(MXSession session) {
        if (null != session) {
            synchronized (mSyncingSessions) {
                mSyncingSessions.remove(session);
            }
        }
    }

    /**
     * Clear syncing sessions list
     */
    public static void clearSyncingSessions() {
        synchronized (mSyncingSessions) {
            mSyncingSessions.clear();
        }
    }

    /**
     * Tell if a session is syncing
     *
     * @param session the session
     * @return true if the session is syncing
     */
    public static boolean isSessionSyncing(MXSession session) {
        boolean isSyncing = false;

        if (null != session) {
            synchronized (mSyncingSessions) {
                isSyncing = mSyncingSessions.contains(session);
            }
        }

        return isSyncing;
    }

    /**
     * Tells if the application crashed
     *
     * @return true if the application crashed
     */
    public boolean didAppCrash() {
        final SharedPreferences preferences = PreferenceManager
                .getDefaultSharedPreferences(VectorApp.getInstance());
        return preferences.getBoolean(PREFS_CRASH_KEY, false);
    }

    /**
     * Clear the crash status
     */
    public void clearAppCrashStatus() {
        final SharedPreferences preferences = PreferenceManager
                .getDefaultSharedPreferences(VectorApp.getInstance());
        SharedPreferences.Editor editor = preferences.edit();
        editor.remove(PREFS_CRASH_KEY);
        editor.commit();
    }

    //==============================================================================================================
    // Locale management
    //==============================================================================================================

    // the supported application languages
    private static final Set<Locale> mApplicationLocales = new HashSet<>();

    private static final String APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY";
    private static final String APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY";
    private static final String APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY";
    private static final String APPLICATION_FONT_SCALE_KEY = "APPLICATION_FONT_SCALE_KEY";

    private static final String FONT_SCALE_TINY = "FONT_SCALE_TINY";
    private static final String FONT_SCALE_SMALL = "FONT_SCALE_SMALL";
    private static final String FONT_SCALE_NORMAL = "FONT_SCALE_NORMAL";
    private static final String FONT_SCALE_LARGE = "FONT_SCALE_LARGE";
    private static final String FONT_SCALE_LARGER = "FONT_SCALE_LARGER";
    private static final String FONT_SCALE_LARGEST = "FONT_SCALE_LARGEST";
    private static final String FONT_SCALE_HUGE = "FONT_SCALE_HUGE";

    private static final Locale mApplicationDefaultLanguage = new Locale("en", "US");

    private static final Map<Float, String> mPrefKeyByFontScale = new LinkedHashMap<Float, String>() {
        {
            put(0.70f, FONT_SCALE_TINY);
            put(0.85f, FONT_SCALE_SMALL);
            put(1.00f, FONT_SCALE_NORMAL);
            put(1.15f, FONT_SCALE_LARGE);
            put(1.30f, FONT_SCALE_LARGER);
            put(1.45f, FONT_SCALE_LARGEST);
            put(1.60f, FONT_SCALE_HUGE);
        }
    };

    private static final Map<String, Integer> mFontTextScaleIdByPrefKey = new LinkedHashMap<String, Integer>() {
        {
            put(FONT_SCALE_TINY, R.string.tiny);
            put(FONT_SCALE_SMALL, R.string.small);
            put(FONT_SCALE_NORMAL, R.string.normal);
            put(FONT_SCALE_LARGE, R.string.large);
            put(FONT_SCALE_LARGER, R.string.larger);
            put(FONT_SCALE_LARGEST, R.string.largest);
            put(FONT_SCALE_HUGE, R.string.huge);
        }
    };

    /**
     * Init the application locale from the saved one
     */
    private static void initApplicationLocale() {
        Context context = VectorApp.getInstance();
        Locale locale = getApplicationLocale();
        float fontScale = getFontScaleValue();
        String theme = ThemeUtils.getApplicationTheme(context);

        Locale.setDefault(locale);
        Configuration config = new Configuration(context.getResources().getConfiguration());
        config.locale = locale;
        config.fontScale = fontScale;
        context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());

        // init the theme
        ThemeUtils.setApplicationTheme(context, theme);

        // init the known locales in background
        AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                getApplicationLocales(VectorApp.getInstance());
                return null;
            }
        };

        // should never crash
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * Get the font scale
     *
     * @return the font scale
     */
    public static String getFontScale() {
        Context context = VectorApp.getInstance();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        String scalePreferenceKey;

        if (!preferences.contains(APPLICATION_FONT_SCALE_KEY)) {
            float fontScale = context.getResources().getConfiguration().fontScale;

            scalePreferenceKey = FONT_SCALE_NORMAL;

            if (mPrefKeyByFontScale.containsKey(fontScale)) {
                scalePreferenceKey = mPrefKeyByFontScale.get(fontScale);
            }

            SharedPreferences.Editor editor = preferences.edit();
            editor.putString(APPLICATION_FONT_SCALE_KEY, scalePreferenceKey);
            editor.commit();
        } else {
            scalePreferenceKey = preferences.getString(APPLICATION_FONT_SCALE_KEY, FONT_SCALE_NORMAL);
        }

        return scalePreferenceKey;
    }

    /**
     * Provides the font scale value
     *
     * @return the font scale
     */
    private static float getFontScaleValue() {
        String fontScale = getFontScale();

        if (mPrefKeyByFontScale.containsValue(fontScale)) {
            for (Map.Entry<Float, String> entry : mPrefKeyByFontScale.entrySet()) {
                if (TextUtils.equals(entry.getValue(), fontScale)) {
                    return entry.getKey();
                }
            }
        }

        return 1.0f;
    }

    /**
     * Provides the font scale description
     *
     * @return the font description
     */
    public static String getFontScaleDescription() {
        Context context = VectorApp.getInstance();
        String fontScale = getFontScale();

        if (mFontTextScaleIdByPrefKey.containsKey(fontScale)) {
            return context.getString(mFontTextScaleIdByPrefKey.get(fontScale));
        }

        return context.getString(R.string.normal);
    }

    /**
     * Update the font size from the locale description.
     *
     * @param fontScaleDescription the font scale description
     */
    public static void updateFontScale(String fontScaleDescription) {
        Context context = VectorApp.getInstance();
        for (Map.Entry<String, Integer> entry : mFontTextScaleIdByPrefKey.entrySet()) {
            if (TextUtils.equals(context.getString(entry.getValue()), fontScaleDescription)) {
                saveFontScale(entry.getKey());
            }
        }

        Configuration config = new Configuration(context.getResources().getConfiguration());
        config.fontScale = getFontScaleValue();
        context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
    }

    /**
     * Provides the current application locale
     *
     * @return the application locale
     */
    public static Locale getApplicationLocale() {
        Context context = VectorApp.getInstance();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        Locale locale;

        if (!preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) {
            locale = Locale.getDefault();

            // detect if the default language is used
            String defaultStringValue = getString(context, mApplicationDefaultLanguage, R.string.resouces_country);
            if (TextUtils.equals(defaultStringValue, getString(context, locale, R.string.resouces_country))) {
                locale = mApplicationDefaultLanguage;
            }

            saveApplicationLocale(locale);
        } else {
            locale = new Locale(preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, ""),
                    preferences.getString(APPLICATION_LOCALE_COUNTRY_KEY, ""),
                    preferences.getString(APPLICATION_LOCALE_VARIANT_KEY, ""));
        }

        return locale;
    }

    /**
     * Provides the device locale
     *
     * @return the device locale
     */
    public static Locale getDeviceLocale() {
        Context context = VectorApp.getInstance();
        Locale locale = getApplicationLocale();

        try {
            PackageManager packageManager = context.getPackageManager();
            Resources resources = packageManager.getResourcesForApplication("android");
            locale = resources.getConfiguration().locale;
        } catch (Exception e) {
            Log.e(LOG_TAG, "## getDeviceLocale() failed " + e.getMessage());
        }

        return locale;
    }

    /**
     * Save the new application locale.
     */
    private static void saveApplicationLocale(Locale locale) {
        Context context = VectorApp.getInstance();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);

        SharedPreferences.Editor editor = preferences.edit();

        String language = locale.getLanguage();
        if (!TextUtils.isEmpty(language)) {
            editor.putString(APPLICATION_LOCALE_LANGUAGE_KEY, language);
        } else {
            editor.remove(APPLICATION_LOCALE_LANGUAGE_KEY);
        }

        String country = locale.getCountry();
        if (!TextUtils.isEmpty(country)) {
            editor.putString(APPLICATION_LOCALE_COUNTRY_KEY, country);
        } else {
            editor.remove(APPLICATION_LOCALE_COUNTRY_KEY);
        }

        String variant = locale.getVariant();
        if (!TextUtils.isEmpty(variant)) {
            editor.putString(APPLICATION_LOCALE_VARIANT_KEY, variant);
        } else {
            editor.remove(APPLICATION_LOCALE_VARIANT_KEY);
        }

        editor.commit();
    }

    /**
     * Save the new font scale
     *
     * @param textScale the text scale
     */
    private static void saveFontScale(String textScale) {
        Context context = VectorApp.getInstance();

        if (!TextUtils.isEmpty(textScale)) {
            SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
            SharedPreferences.Editor editor = preferences.edit();
            editor.putString(APPLICATION_FONT_SCALE_KEY, textScale);
            editor.commit();
        }
    }

    /**
     * Update the application locale
     *
     * @param locale
     */
    public static void updateApplicationLocale(Locale locale) {
        updateApplicationSettings(locale, getFontScale(), ThemeUtils.getApplicationTheme(VectorApp.getInstance()));
    }

    /**
     * Update the application theme
     *
     * @param theme the new theme
     */
    public static void updateApplicationTheme(String theme) {
        ThemeUtils.setApplicationTheme(VectorApp.getInstance(), theme);
        updateApplicationSettings(getApplicationLocale(), getFontScale(),
                ThemeUtils.getApplicationTheme(VectorApp.getInstance()));
    }

    /**
     * Update the application locale.
     *
     * @param locale the locale
     * @param theme  the new theme
     */
    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    private static void updateApplicationSettings(Locale locale, String textSize, String theme) {
        Context context = VectorApp.getInstance();

        saveApplicationLocale(locale);
        saveFontScale(textSize);
        Locale.setDefault(locale);

        Configuration config = new Configuration(context.getResources().getConfiguration());
        config.locale = locale;
        config.fontScale = getFontScaleValue();
        context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());

        ThemeUtils.setApplicationTheme(context, theme);
        PhoneNumberUtils.onLocaleUpdate();
    }

    /**
     * Compute a localised context
     *
     * @param context the context
     * @return the localised context
     */
    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    public static Context getLocalisedContext(Context context) {
        try {
            Resources resources = context.getResources();
            Locale locale = getApplicationLocale();
            Configuration configuration = resources.getConfiguration();
            configuration.fontScale = getFontScaleValue();

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                configuration.setLocale(locale);
                configuration.setLayoutDirection(locale);
                return context.createConfigurationContext(configuration);
            } else {
                configuration.locale = locale;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    configuration.setLayoutDirection(locale);
                }
                resources.updateConfiguration(configuration, resources.getDisplayMetrics());
                return context;
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "## getLocalisedContext() failed : " + e.getMessage());
        }

        return context;
    }

    /**
     * Get String from a locale
     *
     * @param context    the context
     * @param locale     the locale
     * @param resourceId the string resource id
     * @return the localized string
     */
    private static String getString(Context context, Locale locale, int resourceId) {
        String result;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            Configuration config = new Configuration(context.getResources().getConfiguration());
            config.setLocale(locale);
            try {
                result = context.createConfigurationContext(config).getText(resourceId).toString();
            } catch (Exception e) {
                Log.e(LOG_TAG, "## getString() failed : " + e.getMessage());
                // use the default one
                result = context.getString(resourceId);
            }
        } else {
            Resources resources = context.getResources();
            Configuration conf = resources.getConfiguration();
            Locale savedLocale = conf.locale;
            conf.locale = locale;
            resources.updateConfiguration(conf, null);

            // retrieve resources from desired locale
            result = resources.getString(resourceId);

            // restore original locale
            conf.locale = savedLocale;
            resources.updateConfiguration(conf, null);
        }

        return result;
    }

    /**
     * Provides the supported application locales list
     *
     * @param context the context
     * @return the supported application locales list
     */
    public static List<Locale> getApplicationLocales(Context context) {
        if (mApplicationLocales.isEmpty()) {

            Set<Pair<String, String>> knownLocalesSet = new HashSet<>();

            try {
                final Locale[] availableLocales = Locale.getAvailableLocales();

                for (Locale locale : availableLocales) {
                    knownLocalesSet.add(new Pair<>(getString(context, locale, R.string.resouces_language),
                            getString(context, locale, R.string.resouces_country)));
                }
            } catch (Exception e) {
                Log.e(LOG_TAG, "## getApplicationLocales() : failed " + e.getMessage());
                knownLocalesSet.add(new Pair<>(context.getString(R.string.resouces_language),
                        context.getString(R.string.resouces_country)));
            }

            for (Pair<String, String> knownLocale : knownLocalesSet) {
                mApplicationLocales.add(new Locale(knownLocale.first, knownLocale.second));
            }
        }

        List<Locale> sortedLocalesList = new ArrayList<>(mApplicationLocales);

        // sort by human display names
        Collections.sort(sortedLocalesList, new Comparator<Locale>() {
            @Override
            public int compare(Locale lhs, Locale rhs) {
                return localeToLocalisedString(lhs).compareTo(localeToLocalisedString(rhs));
            }
        });

        return sortedLocalesList;
    }

    /**
     * Convert a locale to a string
     *
     * @param locale the locale to convert
     * @return the string
     */
    public static String localeToLocalisedString(Locale locale) {
        String res = locale.getDisplayLanguage(locale);

        if (!TextUtils.isEmpty(locale.getDisplayCountry(locale))) {
            res += " (" + locale.getDisplayCountry(locale) + ")";
        }

        return res;
    }

    //==============================================================================================================
    // Piwik management
    //==============================================================================================================

    // the piwik tracker
    private Tracker mPiwikTracker;

    /**
     * Set the visit variable
     *
     * @param trackMe
     * @param id
     * @param name
     * @param value
     */
    private static final void visitVariables(TrackMe trackMe, int id, String name, String value) {
        CustomVariables customVariables = new CustomVariables(
                trackMe.get(QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES));
        customVariables.put(id, name, value);
        trackMe.set(QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES, customVariables.toString());
    }

    /**
     * @return the piwik instance
     */
    private Tracker getPiwikTracker() {
        if (mPiwikTracker == null) {
            try {
                mPiwikTracker = Piwik.getInstance(this)
                        .newTracker(new TrackerConfig("https://piwik.riot.im/", 1, "AndroidPiwikTracker"));
                // sends the tracking information each minute
                // the app might be killed in background
                mPiwikTracker.setDispatchInterval(30 * 1000);

                //
                TrackMe trackMe = mPiwikTracker.getDefaultTrackMe();

                visitVariables(trackMe, 1, "App Platform", "Android Platform");
                visitVariables(trackMe, 2, "App Version", SHORT_VERSION);
                visitVariables(trackMe, 4, "Chosen Language", getApplicationLocale().toString());

                if (null != Matrix.getInstance(this).getDefaultSession()) {
                    MXSession session = Matrix.getInstance(this).getDefaultSession();

                    visitVariables(trackMe, 7, "Homeserver URL",
                            session.getHomeServerConfig().getHomeserverUri().toString());
                    visitVariables(trackMe, 8, "Identity Server URL",
                            session.getHomeServerConfig().getIdentityServerUri().toString());
                }
            } catch (Throwable t) {
                Log.e(LOG_TAG, "## getPiwikTracker() : newTracker failed " + t.getMessage());
            }
        }

        return mPiwikTracker;
    }

    /**
     * Add the stats variables to the piwik screen.
     *
     * @return the piwik screen
     */
    private TrackHelper.Screen addCustomVariables(TrackHelper.Screen screen) {
        screen.variable(1, "App Platform", "Android Platform");
        screen.variable(2, "App Version", SHORT_VERSION);
        screen.variable(4, "Chosen Language", getApplicationLocale().toString());

        if (null != Matrix.getInstance(this).getDefaultSession()) {
            MXSession session = Matrix.getInstance(this).getDefaultSession();

            screen.variable(7, "Homeserver URL", session.getHomeServerConfig().getHomeserverUri().toString());
            screen.variable(8, "Identity Server URL",
                    session.getHomeServerConfig().getIdentityServerUri().toString());
        }

        return screen;
    }

    /**
     * A new activity has been resumed
     *
     * @param activity the new activity
     */
    private void onNewScreen(Activity activity) {
        if (PreferencesManager.useAnalytics(this)) {
            Tracker tracker = getPiwikTracker();
            if (null != tracker) {
                try {
                    TrackHelper.Screen screen = TrackHelper.track()
                            .screen("/android/" + Matrix.getApplicationName() + "/"
                                    + this.getString(R.string.flavor_description) + "/" + SHORT_VERSION + "/"
                                    + activity.getClass().getName().replace(".", "/"));
                    addCustomVariables(screen).with(tracker);
                } catch (Throwable t) {
                    Log.e(LOG_TAG, "## onNewScreen() : failed " + t.getMessage());
                }
            }
        }
    }

    /**
     * The application is paused.
     */
    private void onAppPause() {
        if (PreferencesManager.useAnalytics(this)) {
            Tracker tracker = getPiwikTracker();
            if (null != tracker) {
                try {
                    // force to send the pending actions
                    tracker.dispatch();
                } catch (Throwable t) {
                    Log.e(LOG_TAG, "## onAppPause() : failed " + t.getMessage());
                }
            }
        }
    }
}