arun.com.chromer.browsing.customtabs.CustomTabs.java Source code

Java tutorial

Introduction

Here is the source code for arun.com.chromer.browsing.customtabs.CustomTabs.java

Source

/*
 * Lynket
 *
 * Copyright (C) 2019 Arunkumar
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package arun.com.chromer.browsing.customtabs;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsSession;
import android.support.v4.graphics.ColorUtils;
import android.widget.Toast;

import com.mikepenz.community_material_typeface_library.CommunityMaterial;
import com.mikepenz.iconics.IconicsDrawable;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.inject.Inject;

import arun.com.chromer.R;
import arun.com.chromer.browsing.customtabs.callbacks.CopyToClipboardService;
import arun.com.chromer.browsing.customtabs.callbacks.FavShareBroadcastReceiver;
import arun.com.chromer.browsing.customtabs.callbacks.MinimizeBroadcastReceiver;
import arun.com.chromer.browsing.customtabs.callbacks.OpenInChromeReceiver;
import arun.com.chromer.browsing.customtabs.callbacks.SecondaryBrowserReceiver;
import arun.com.chromer.browsing.customtabs.callbacks.ShareBroadcastReceiver;
import arun.com.chromer.browsing.openwith.OpenIntentWithActivity;
import arun.com.chromer.browsing.optionspopup.ChromerOptionsActivity;
import arun.com.chromer.browsing.webview.WebViewActivity;
import arun.com.chromer.settings.Preferences;
import arun.com.chromer.util.Utils;
import arun.com.chromer.webheads.WebHeadService;
import timber.log.Timber;

import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.graphics.Color.WHITE;
import static arun.com.chromer.browsing.customtabs.bottombar.BottomBarManager.createBottomBarRemoteViews;
import static arun.com.chromer.browsing.customtabs.bottombar.BottomBarManager.getClickableIDs;
import static arun.com.chromer.browsing.customtabs.bottombar.BottomBarManager.getOnClickPendingIntent;
import static arun.com.chromer.settings.Preferences.ANIMATION_MEDIUM;
import static arun.com.chromer.settings.Preferences.ANIMATION_SHORT;
import static arun.com.chromer.settings.Preferences.PREFERRED_ACTION_BROWSER;
import static arun.com.chromer.settings.Preferences.PREFERRED_ACTION_FAV_SHARE;
import static arun.com.chromer.settings.Preferences.PREFERRED_ACTION_GEN_SHARE;
import static arun.com.chromer.shared.Constants.EXTRA_KEY_ORIGINAL_URL;
import static arun.com.chromer.shared.Constants.NO_COLOR;

/**
 * A helper class that builds up the view intent according to user Preferences.get(activity) and
 * launches custom tab.
 */
public class CustomTabs {
    private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
    private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE";
    private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
    private static final String STABLE_PACKAGE = "com.android.chrome";
    private static final String BETA_PACKAGE = "com.chrome.beta";
    private static final String DEV_PACKAGE = "com.chrome.dev";

    /**
     * Fallback in case there was en error launching custom tabs
     */
    private final static CustomTabsFallback CUSTOM_TABS_FALLBACK = (activity, uri) -> {
        if (activity != null) {
            final String string = activity.getString(R.string.fallback_msg);
            Toast.makeText(activity, string, Toast.LENGTH_SHORT).show();
            try {
                final Intent intent = new Intent(activity, WebViewActivity.class);
                intent.setData(uri);
                activity.startActivity(intent);
            } catch (ActivityNotFoundException e) {
                Toast.makeText(activity, activity.getString(R.string.unxp_err), Toast.LENGTH_SHORT).show();
            }
        }
    };
    /**
     * The context to work with
     */
    private Activity activity;
    /**
     * The url for which the custom tab should be launched;
     */
    private String url;
    /**
     * The builder used to customize the custom tab intent
     */
    private CustomTabsIntent.Builder builder;
    /**
     * Client provided custom tab session
     */
    private CustomTabsSession customTabsSession;

    /**
     * Toolbar color that overrides the default toolbar color generated by this helper.
     */
    @ColorInt
    private int toolbarColor = NO_COLOR;
    private boolean noAnimation = false;

    /**
     * Create an one time usable instance
     *
     * @param activity the context to work with
     */
    @Inject
    public CustomTabs(@NonNull Activity activity) {
        this.activity = activity;
        noAnimation = false;
    }

    /**
     * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
     */
    private void openCustomTab() {
        final String packageName = getCustomTabPackage(activity);
        final CustomTabsIntent customTabsIntent = builder.build();
        final Uri uri = Uri.parse(url);
        if (packageName != null) {
            customTabsIntent.intent.setPackage(packageName);
            final Intent keepAliveIntent = new Intent();
            keepAliveIntent.setClassName(activity.getPackageName(), KeepAliveService.class.getCanonicalName());
            customTabsIntent.intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
            try {
                customTabsIntent.launchUrl(activity, uri);
                Timber.d("Launched url: %s", uri.toString());
            } catch (Exception e) {
                CUSTOM_TABS_FALLBACK.openUri(activity, uri);
                Timber.e("Called fallback even though a package was found, weird Exception : %s", e.toString());
            }
        } else {
            Timber.e("Called fallback since no package found!");
            CUSTOM_TABS_FALLBACK.openUri(activity, uri);
        }
    }

    /**
     * Attempts to find the custom the best custom tab package to use.
     *
     * @return A package that supports custom tab, null if not present
     */
    @Nullable
    private static String getCustomTabPackage(Context context) {
        final String userPackage = Preferences.get(context).customTabPackage();
        if (userPackage != null && userPackage.length() > 0) {
            return userPackage;
        }
        if (isPackageSupportCustomTabs(context, STABLE_PACKAGE))
            return STABLE_PACKAGE;
        if (isPackageSupportCustomTabs(context, LOCAL_PACKAGE))
            return LOCAL_PACKAGE;

        final List<String> supportingPackages = getCustomTabSupportingPackages(context);
        if (!supportingPackages.isEmpty()) {
            return supportingPackages.get(0);
        } else
            return null;
    }

    /**
     * Returns all valid custom tab supporting browser packages on the system. Does not respect if
     * the package is default or not.
     *
     * @param context context to work with
     * @return list of packages supporting CCT
     */
    @TargetApi(Build.VERSION_CODES.M)
    @NonNull
    public static List<String> getCustomTabSupportingPackages(Context context) {
        final PackageManager pm = context.getApplicationContext().getPackageManager();
        final Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
        // Get all apps that can handle VIEW intents.
        final List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent,
                PackageManager.MATCH_ALL);
        final List<String> packagesSupportingCustomTabs = new ArrayList<>();
        for (ResolveInfo info : resolvedActivityList) {
            if (isPackageSupportCustomTabs(context, info.activityInfo.packageName)) {
                packagesSupportingCustomTabs.add(info.activityInfo.packageName);
            }
        }
        return packagesSupportingCustomTabs;
    }

    /**
     * Determines if the provided package name is a valid custom tab provider or not.
     *
     * @param context     Context to work with
     * @param packageName Package name of the app
     * @return true if a provider, false otherwise
     */
    public static boolean isPackageSupportCustomTabs(Context context, @Nullable String packageName) {
        if (packageName == null) {
            return false;
        }
        final PackageManager pm = context.getApplicationContext().getPackageManager();
        final Intent serviceIntent = new Intent();
        serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
        serviceIntent.setPackage(packageName);
        return pm.resolveService(serviceIntent, 0) != null;
    }

    public CustomTabs withSession(@Nullable CustomTabsSession session) {
        if (session != null) {
            customTabsSession = session;
        }
        return this;
    }

    /**
     * Exposed method to set the url for which this CCT should be launched
     *
     * @param url Url of the web site
     */
    public CustomTabs forUrl(@NonNull final String url) {
        this.url = url.trim();
        return this;
    }

    public CustomTabs toolbarColor(@ColorInt int overrideColor) {
        toolbarColor = ColorUtils.setAlphaComponent(overrideColor, 0xFF);
        return this;
    }

    /**
     * Facade method that does all the heavy work of building up the builder based on user Preferences.get(activity)
     *
     * @return Instance of this class
     */
    @NonNull
    private CustomTabs prepare() {
        builder = new CustomTabsIntent.Builder(getSession());
        // set defaults
        builder.setShowTitle(true);
        builder.enableUrlBarHiding();
        builder.addDefaultShareMenuItem();
        builder.setToolbarColor(toolbarColor).setSecondaryToolbarColor(toolbarColor);

        prepareAnimations();
        prepareActionButton();
        prepareMenuItems();
        prepareBottomBar();
        return this;
    }

    /**
     * Builds custom tab intent from the builder we created so far and launches the custom tab.
     */
    public void launch() {
        prepare();
        assertBuilderInitialized();
        openCustomTab();

        // Dispose reference
        activity = null;
        customTabsSession = null;
    }

    /**
     * Tries to find available sessions for the url to launch in.
     *
     * @return Instance of this class
     */
    @Nullable
    private CustomTabsSession getSession() {
        if (customTabsSession != null) {
            return customTabsSession;
        }
        if (WebHeadService.getTabSession() != null) {
            Timber.d("Using webhead session");
            return WebHeadService.getTabSession();
        }
        return null;
    }

    /**
     * Used to set the correct custom tab opening/closing animations. Will re use last used animations
     * if the preference did not change from before.
     */
    private void prepareAnimations() {
        assertBuilderInitialized();
        if (Preferences.get(activity).isAnimationEnabled() && !noAnimation) {
            final int type = Preferences.get(activity).animationType();
            final int speed = Preferences.get(activity).animationSpeed();
            int start[] = new int[] {};
            int exit[] = new int[] {};
            switch (speed) {
            case ANIMATION_MEDIUM:
                switch (type) {
                case 1:
                    start = new int[] { R.anim.slide_in_right_medium, R.anim.slide_out_left_medium };
                    exit = new int[] { R.anim.slide_in_left_medium, R.anim.slide_out_right_medium };
                    break;
                case 2:
                    start = new int[] { R.anim.slide_up_right_medium, R.anim.slide_down_left_medium };
                    exit = new int[] { R.anim.slide_up_left_medium, R.anim.slide_down_right_medium };
                    break;
                }
                break;
            case ANIMATION_SHORT:
                switch (type) {
                case 1:
                    start = new int[] { R.anim.slide_in_right, R.anim.slide_out_left };
                    exit = new int[] { R.anim.slide_in_left, R.anim.slide_out_right };
                    break;
                case 2:
                    start = new int[] { R.anim.slide_up_right, R.anim.slide_down_left };
                    exit = new int[] { R.anim.slide_up_left, R.anim.slide_down_right };
                    break;
                }
                break;
            }
            // set it to builder
            builder.setStartAnimations(activity, start[0], start[1]).setExitAnimations(activity, exit[0], exit[1]);
            activity.overridePendingTransition(start[0], start[1]);
        }
    }

    /**
     * Used to set the action button based on user Preferences.get(activity). Usually secondary browser or favorite share app.
     */
    private void prepareActionButton() {
        assertBuilderInitialized();
        switch (Preferences.get(activity).preferredAction()) {
        case PREFERRED_ACTION_BROWSER:
            String pakage = Preferences.get(activity).secondaryBrowserPackage();
            if (Utils.isPackageInstalled(activity, pakage)) {
                final Bitmap icon = getAppIconBitmap(pakage);
                final Intent intent = new Intent(activity, SecondaryBrowserReceiver.class);
                final PendingIntent openBrowserPending = PendingIntent.getBroadcast(activity, 0, intent,
                        FLAG_UPDATE_CURRENT);
                //noinspection ConstantConditions
                builder.setActionButton(icon, activity.getString(R.string.choose_secondary_browser),
                        openBrowserPending);
            }
            break;
        case PREFERRED_ACTION_FAV_SHARE:
            pakage = Preferences.get(activity).favSharePackage();
            if (Utils.isPackageInstalled(activity, pakage)) {
                final Bitmap icon = getAppIconBitmap(pakage);
                final Intent intent = new Intent(activity, FavShareBroadcastReceiver.class);
                final PendingIntent favSharePending = PendingIntent.getBroadcast(activity, 0, intent,
                        FLAG_UPDATE_CURRENT);
                //noinspection ConstantConditions
                builder.setActionButton(icon, activity.getString(R.string.fav_share_app), favSharePending);
            }
            break;
        case PREFERRED_ACTION_GEN_SHARE:
            final Bitmap shareIcon = new IconicsDrawable(activity).icon(CommunityMaterial.Icon.cmd_share_variant)
                    .color(WHITE).sizeDp(24).toBitmap();
            final Intent intent = new Intent(activity, ShareBroadcastReceiver.class);
            final PendingIntent sharePending = PendingIntent.getBroadcast(activity, 0, intent, FLAG_UPDATE_CURRENT);
            //noinspection ConstantConditions
            builder.setActionButton(shareIcon, activity.getString(R.string.share_via), sharePending, true);
            break;
        }
    }

    /**
     * Prepares all the menu items and adds to builder
     */
    private void prepareMenuItems() {
        assertBuilderInitialized();
        preparePreferredAction();
        prepareMinimize();
        prepareCopyLink();
        // prepareAddToHomeScreen();
        // prepareOpenWith();
        prepareOpenInChrome();
        prepareChromerOptions();
    }

    private void prepareChromerOptions() {
        final Intent moreMenuActivity = new Intent(activity, ChromerOptionsActivity.class);
        moreMenuActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        moreMenuActivity.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            moreMenuActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
        }
        moreMenuActivity.putExtra(EXTRA_KEY_ORIGINAL_URL, url);
        final PendingIntent moreMenuPending = PendingIntent.getActivity(activity, 0, moreMenuActivity,
                FLAG_UPDATE_CURRENT);
        builder.addMenuItem(activity.getString(R.string.chromer_options), moreMenuPending);
    }

    /**
     * Adds a menu item tapping which will minimize the current custom tab back to overview. This requires
     * merge tabs and apps and
     */
    private void prepareMinimize() {
        if (!Preferences.get(activity).bottomBar() && Utils.ANDROID_LOLLIPOP) {
            final Intent minimizeIntent = new Intent(activity, MinimizeBroadcastReceiver.class);
            minimizeIntent.putExtra(EXTRA_KEY_ORIGINAL_URL, url);
            final PendingIntent pendingMin = PendingIntent.getBroadcast(activity, new Random().nextInt(),
                    minimizeIntent, FLAG_UPDATE_CURRENT);
            builder.addMenuItem(activity.getString(R.string.minimize), pendingMin);
        }
    }

    /**
     * Opposite of what {@link #prepareActionButton()} does. Fills a menu item with either secondary
     * browser or favorite share app.
     */
    private void preparePreferredAction() {
        assertBuilderInitialized();
        switch (Preferences.get(activity).preferredAction()) {
        case PREFERRED_ACTION_BROWSER:
            String pkg = Preferences.get(activity).favSharePackage();
            if (Utils.isPackageInstalled(activity, pkg)) {
                final String app = Utils.getAppNameWithPackage(activity, pkg);
                final String label = String.format(activity.getString(R.string.share_with), app);
                final Intent shareIntent = new Intent(activity, FavShareBroadcastReceiver.class);
                final PendingIntent pendingShareIntent = PendingIntent.getBroadcast(activity, 0, shareIntent,
                        FLAG_UPDATE_CURRENT);
                builder.addMenuItem(label, pendingShareIntent);
            }
            break;
        case PREFERRED_ACTION_FAV_SHARE:
            pkg = Preferences.get(activity).secondaryBrowserPackage();
            if (Utils.isPackageInstalled(activity, pkg)) {
                if (!pkg.equalsIgnoreCase(STABLE_PACKAGE)) {
                    final String app = Utils.getAppNameWithPackage(activity, pkg);
                    final String label = String.format(activity.getString(R.string.open_in_browser), app);
                    final Intent browseIntent = new Intent(activity, SecondaryBrowserReceiver.class);
                    final PendingIntent pendingBrowseIntent = PendingIntent.getBroadcast(activity, 0, browseIntent,
                            FLAG_UPDATE_CURRENT);
                    builder.addMenuItem(label, pendingBrowseIntent);
                } else {
                    Timber.d("Excluded secondary browser menu as it was Chrome");
                }
            }
            break;
        }
    }

    private void prepareCopyLink() {
        final Intent clipboardIntent = new Intent(activity, CopyToClipboardService.class);
        clipboardIntent.putExtra(EXTRA_KEY_ORIGINAL_URL, url);
        final PendingIntent serviceIntentPending = PendingIntent.getService(activity, 0, clipboardIntent,
                FLAG_UPDATE_CURRENT);
        builder.addMenuItem(activity.getString(R.string.copy_link), serviceIntentPending);
    }

    /**
     * Adds an open in chrome option
     */
    private void prepareOpenInChrome() {
        final String customTabPkg = Preferences.get(activity).customTabPackage();
        if (Utils.isPackageInstalled(activity, customTabPkg)) {
            if (customTabPkg.equalsIgnoreCase(BETA_PACKAGE) || customTabPkg.equalsIgnoreCase(DEV_PACKAGE)
                    || customTabPkg.equalsIgnoreCase(STABLE_PACKAGE)) {

                final Intent chromeReceiver = new Intent(activity, OpenInChromeReceiver.class);
                final PendingIntent openChromePending = PendingIntent.getBroadcast(activity, 0, chromeReceiver,
                        FLAG_UPDATE_CURRENT);

                final String app = Utils.getAppNameWithPackage(activity, customTabPkg);
                final String label = String.format(activity.getString(R.string.open_in_browser), app);
                builder.addMenuItem(label, openChromePending);
            }
        }
    }

    private void prepareOpenWith() {
        final Intent openWithActivity = new Intent(activity, OpenIntentWithActivity.class);
        openWithActivity.putExtra(EXTRA_KEY_ORIGINAL_URL, url);
        final PendingIntent openWithActivityPending = PendingIntent.getActivity(activity, 0, openWithActivity,
                FLAG_UPDATE_CURRENT);
        builder.addMenuItem(activity.getString(R.string.open_with), openWithActivityPending);
    }

    /**
     * Add all bottom bar actions
     */
    private void prepareBottomBar() {
        if (!Preferences.get(activity).bottomBar()) {
            return;
        }
        builder.setSecondaryToolbarViews(createBottomBarRemoteViews(activity, toolbarColor), getClickableIDs(),
                getOnClickPendingIntent(activity, url));
    }

    /**
     * Method to check if the builder was initialized. Will fail fast if not.
     */
    private void assertBuilderInitialized() {
        if (builder == null) {
            throw new IllegalStateException("Intent builder null. Are you sure you called prepare()");
        }
    }

    /**
     * Returns the bitmap of the app icon. It is assumed, the package is installed.
     *
     * @return App icon bitmap
     */
    @Nullable
    private Bitmap getAppIconBitmap(@NonNull final String packageName) {
        try {
            final Drawable drawable = activity.getPackageManager().getApplicationIcon(packageName);
            final Bitmap appIcon = Utils.drawableToBitmap(drawable);
            return Utils.scale(appIcon, Utils.dpToPx(24), true);
        } catch (Exception e) {
            Timber.e("App icon fetching for %s failed", packageName);
        }
        return null;
    }

    private int chromeVariantVersion() {
        final String customTabPackage = Preferences.get(activity).customTabPackage();
        if (Utils.isPackageInstalled(activity, customTabPackage)
                && (customTabPackage.equalsIgnoreCase(STABLE_PACKAGE)
                        || customTabPackage.equalsIgnoreCase(DEV_PACKAGE)
                        || customTabPackage.equalsIgnoreCase(BETA_PACKAGE)
                        || customTabPackage.equalsIgnoreCase(LOCAL_PACKAGE))) {
            try {
                final PackageInfo packageInfo = activity.getPackageManager().getPackageInfo(customTabPackage, 0);
                return Integer.parseInt(packageInfo.versionName.split("\\.")[0]);
            } catch (Exception e) {
                return -1;
            }
        }
        return -1;
    }

    /**
     * To be used as a fallback to open the Uri when Custom Tabs is not available.
     */
    interface CustomTabsFallback {
        /**
         * @param activity The Activity that wants to open the Uri.
         * @param uri      The uri to be opened by the fallback.
         */
        void openUri(Activity activity, Uri uri);
    }
}