ActivityNavigator.java

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.navigation;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.AttributeSet;

import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;

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

/**
 * ActivityNavigator implements cross-activity navigation.
 */
@Navigator.Name("activity")
public class ActivityNavigator extends Navigator<ActivityNavigator.Destination> {
    private static final String EXTRA_NAV_SOURCE =
            "android-support-navigation:ActivityNavigator:source";
    private static final String EXTRA_NAV_CURRENT =
            "android-support-navigation:ActivityNavigator:current";
    private static final String EXTRA_POP_ENTER_ANIM =
            "android-support-navigation:ActivityNavigator:popEnterAnim";
    private static final String EXTRA_POP_EXIT_ANIM =
            "android-support-navigation:ActivityNavigator:popExitAnim";

    private Context mContext;
    private Activity mHostActivity;

    public ActivityNavigator(@NonNull Context context) {
        mContext = context;
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                mHostActivity = (Activity) context;
                break;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
    }

    /**
     * Apply any pop animations in the Intent of the given Activity to a pending transition.
     * This should be used in place of {@link Activity#overridePendingTransition(int, int)}
     * to get the appropriate pop animations.
     * @param activity An activity started from the {@link ActivityNavigator}.
     * @see NavOptions#getPopEnterAnim()
     * @see NavOptions#getPopExitAnim()
     */
    public static void applyPopAnimationsToPendingTransition(@NonNull Activity activity) {
        Intent intent = activity.getIntent();
        if (intent == null) {
            return;
        }
        int popEnterAnim = intent.getIntExtra(EXTRA_POP_ENTER_ANIM, -1);
        int popExitAnim = intent.getIntExtra(EXTRA_POP_EXIT_ANIM, -1);
        if (popEnterAnim != -1 || popExitAnim != -1) {
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            activity.overridePendingTransition(popEnterAnim, popExitAnim);
        }
    }

    @NonNull
    final Context getContext() {
        return mContext;
    }

    @NonNull
    @Override
    public Destination createDestination() {
        return new Destination(this);
    }

    @Override
    public boolean popBackStack() {
        if (mHostActivity != null) {
            mHostActivity.finish();
            return true;
        }
        return false;
    }

    @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (destination.getIntent() == null) {
            throw new IllegalStateException("Destination " + destination.getId()
                    + " does not have an Intent set.");
        }
        Intent intent = new Intent(destination.getIntent());
        if (args != null) {
            intent.putExtras(args);
            String dataPattern = destination.getDataPattern();
            if (!TextUtils.isEmpty(dataPattern)) {
                // Fill in the data pattern with the args to build a valid URI
                StringBuffer data = new StringBuffer();
                Pattern fillInPattern = Pattern.compile("\{(.+?)\}");
                Matcher matcher = fillInPattern.matcher(dataPattern);
                while (matcher.find()) {
                    String argName = matcher.group(1);
                    if (args.containsKey(argName)) {
                        matcher.appendReplacement(data, "");
                        //noinspection ConstantConditions
                        data.append(Uri.encode(args.get(argName).toString()));
                    } else {
                        throw new IllegalArgumentException("Could not find " + argName + " in "
                                + args + " to fill data pattern " + dataPattern);
                    }
                }
                matcher.appendTail(data);
                intent.setData(Uri.parse(data.toString()));
            }
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            intent.addFlags(extras.getFlags());
        }
        if (!(mContext instanceof Activity)) {
            // If we're not launching from an Activity context we have to launch in a new task.
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        }
        if (mHostActivity != null) {
            final Intent hostIntent = mHostActivity.getIntent();
            if (hostIntent != null) {
                final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
                if (hostCurrentId != 0) {
                    intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
                }
            }
        }
        final int destId = destination.getId();
        intent.putExtra(EXTRA_NAV_CURRENT, destId);
        if (navOptions != null) {
            // For use in applyPopAnimationsToPendingTransition()
            intent.putExtra(EXTRA_POP_ENTER_ANIM, navOptions.getPopEnterAnim());
            intent.putExtra(EXTRA_POP_EXIT_ANIM, navOptions.getPopExitAnim());
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            ActivityOptionsCompat activityOptions = extras.getActivityOptions();
            if (activityOptions != null) {
                ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
            } else {
                mContext.startActivity(intent);
            }
        } else {
            mContext.startActivity(intent);
        }
        if (navOptions != null && mHostActivity != null) {
            int enterAnim = navOptions.getEnterAnim();
            int exitAnim = navOptions.getExitAnim();
            if (enterAnim != -1 || exitAnim != -1) {
                enterAnim = enterAnim != -1 ? enterAnim : 0;
                exitAnim = exitAnim != -1 ? exitAnim : 0;
                mHostActivity.overridePendingTransition(enterAnim, exitAnim);
            }
        }

        // You can't pop the back stack from the caller of a new Activity,
        // so we don't add this navigator to the controller's back stack
        return null;
    }

    /**
     * NavDestination for activity navigation
     */
    @NavDestination.ClassType(Activity.class)
    public static class Destination extends NavDestination {
        private Intent mIntent;
        private String mDataPattern;

        /**
         * Construct a new activity destination. This destination is not valid until you set the
         * Intent via {@link #setIntent(Intent)} or one or more of the other set method.
         *
         *
         * @param navigatorProvider The {@link NavController} which this destination
         *                          will be associated with.
         */
        public Destination(@NonNull NavigatorProvider navigatorProvider) {
            this(navigatorProvider.getNavigator(ActivityNavigator.class));
        }

        /**
         * Construct a new activity destination. This destination is not valid until you set the
         * Intent via {@link #setIntent(Intent)} or one or more of the other set method.
         *
         * @param activityNavigator The {@link ActivityNavigator} which this destination
         *                          will be associated with. Generally retrieved via a
         *                          {@link NavController}'s
         *                          {@link NavigatorProvider#getNavigator(Class)} method.
         */
        public Destination(@NonNull Navigator<? extends Destination> activityNavigator) {
            super(activityNavigator);
        }

        @CallSuper
        @Override
        public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.ActivityNavigator);
            String targetPackage = a.getString(R.styleable.ActivityNavigator_targetPackage);
            if (targetPackage != null) {
                targetPackage = targetPackage.replace(NavInflater.APPLICATION_ID_PLACEHOLDER,
                        context.getPackageName());
            }
            setTargetPackage(targetPackage);
            String className = a.getString(R.styleable.ActivityNavigator_android_name);
            if (className != null) {
                if (className.charAt(0) == '.') {
                    className = context.getPackageName() + className;
                }
                setComponentName(new ComponentName(context, className));
            }
            setAction(a.getString(R.styleable.ActivityNavigator_action));
            String data = a.getString(R.styleable.ActivityNavigator_data);
            if (data != null) {
                setData(Uri.parse(data));
            }
            setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
            a.recycle();
        }

        /**
         * Set the Intent to start when navigating to this destination.
         * @param intent Intent to associated with this destination.
         * @return this {@link Destination}
         */
        @NonNull
        public final Destination setIntent(@Nullable Intent intent) {
            mIntent = intent;
            return this;
        }

        /**
         * Gets the Intent associated with this destination.
         * @return
         */
        @Nullable
        public final Intent getIntent() {
            return mIntent;
        }

        /**
         * Set an explicit application package name that limits
         * the components this destination will navigate to.
         * <p>
         * When inflated from XML, you can use <code>${applicationId}</code> as the
         * package name to automatically use {@link Context#getPackageName()}.
         *
         * @param packageName packageName to set
         * @return this {@link Destination}
         */
        @NonNull
        public final Destination setTargetPackage(@Nullable String packageName) {
            if (mIntent == null) {
                mIntent = new Intent();
            }
            mIntent.setPackage(packageName);
            return this;
        }

        /**
         * Get the explicit application package name associated with this destination, if any
         */
        @Nullable
        public final String getTargetPackage() {
            if (mIntent == null) {
                return null;
            }
            return mIntent.getPackage();
        }

        /**
         * Set an explicit {@link ComponentName} to navigate to.
         *
         * @param name The component name of the Activity to start.
         * @return this {@link Destination}
         */
        @NonNull
        public final Destination setComponentName(@Nullable ComponentName name) {
            if (mIntent == null) {
                mIntent = new Intent();
            }
            mIntent.setComponent(name);
            return this;
        }

        /**
         * Get the explicit {@link ComponentName} associated with this destination, if any
         * @return
         */
        @Nullable
        public final ComponentName getComponent() {
            if (mIntent == null) {
                return null;
            }
            return mIntent.getComponent();
        }

        /**
         * Sets the action sent when navigating to this destination.
         * @param action The action string to use.
         * @return this {@link Destination}
         */
        @NonNull
        public final Destination setAction(@Nullable String action) {
            if (mIntent == null) {
                mIntent = new Intent();
            }
            mIntent.setAction(action);
            return this;
        }

        /**
         * Get the action used to start the Activity, if any
         */
        @Nullable
        public final String getAction() {
            if (mIntent == null) {
                return null;
            }
            return mIntent.getAction();
        }

        /**
         * Sets a static data URI that is sent when navigating to this destination.
         *
         * <p>To use a dynamic URI that changes based on the arguments passed in when navigating,
         * use {@link #setDataPattern(String)}, which will take precedence when arguments are
         * present.</p>
         *
         * @param data A static URI that should always be used.
         * @see #setDataPattern(String)
         * @return this {@link Destination}
         */
        @NonNull
        public final Destination setData(@Nullable Uri data) {
            if (mIntent == null) {
                mIntent = new Intent();
            }
            mIntent.setData(data);
            return this;
        }

        /**
         * Get the data URI used to start the Activity, if any
         */
        @Nullable
        public final Uri getData() {
            if (mIntent == null) {
                return null;
            }
            return mIntent.getData();
        }

        /**
         * Sets a dynamic data URI pattern that is sent when navigating to this destination.
         *
         * <p>If a non-null arguments Bundle is present when navigating, any segments in the form
         * <code>{argName}</code> will be replaced with a URI encoded string from the arguments.</p>
         * @param dataPattern A URI pattern with segments in the form of <code>{argName}</code> that
         *                    will be replaced with URI encoded versions of the Strings in the
         *                    arguments Bundle.
         * @see #setData
         * @return this {@link Destination}
         */
        @NonNull
        public final Destination setDataPattern(@Nullable String dataPattern) {
            mDataPattern = dataPattern;
            return this;
        }

        /**
         * Gets the dynamic data URI pattern, if any
         */
        @Nullable
        public final String getDataPattern() {
            return mDataPattern;
        }

        @Override
        boolean supportsActions() {
            return false;
        }

        @NonNull
        @Override
        public String toString() {
            ComponentName componentName = getComponent();
            StringBuilder sb = new StringBuilder();
            sb.append(super.toString());
            if (componentName != null) {
                sb.append(" class=");
                sb.append(componentName.getClassName());
            } else {
                String action = getAction();
                if (action != null) {
                    sb.append(" action=");
                    sb.append(action);
                }
            }
            return sb.toString();
        }
    }

    /**
     * Extras that can be passed to ActivityNavigator to customize what
     * {@link ActivityOptionsCompat} and flags are passed through to the call to
     * {@link ActivityCompat#startActivity(Context, Intent, Bundle)}.
     */
    public static final class Extras implements Navigator.Extras {
        private final int mFlags;
        private final ActivityOptionsCompat mActivityOptions;

        Extras(int flags, @Nullable ActivityOptionsCompat activityOptions) {
            mFlags = flags;
            mActivityOptions = activityOptions;
        }

        /**
         * Gets the <code>Intent.FLAG_ACTIVITY_</code> flags that should be added to the Intent.
         */
        public int getFlags() {
            return mFlags;
        }

        /**
         * Gets the {@link ActivityOptionsCompat} that should be used with
         * {@link ActivityCompat#startActivity(Context, Intent, Bundle)}.
         */
        @Nullable
        public ActivityOptionsCompat getActivityOptions() {
            return mActivityOptions;
        }

        /**
         * Builder for constructing new {@link Extras} instances. The resulting instances are
         * immutable.
         */
        public static final class Builder {
            private int mFlags;
            private ActivityOptionsCompat mActivityOptions;

            /**
             * Adds one or more <code>Intent.FLAG_ACTIVITY_</code> flags
             *
             * @param flags the flags to add
             * @return this {@link Builder}
             */
            @NonNull
            public Builder addFlags(int flags) {
                mFlags |= flags;
                return this;
            }

            /**
             * Sets the {@link ActivityOptionsCompat} that should be used with
             * {@link ActivityCompat#startActivity(Context, Intent, Bundle)}.
             *
             * @param activityOptions The {@link ActivityOptionsCompat} to pass through
             * @return this {@link Builder}
             */
            @NonNull
            public Builder setActivityOptions(@NonNull ActivityOptionsCompat activityOptions) {
                mActivityOptions = activityOptions;
                return this;
            }

            /**
             * Constructs the final {@link Extras} instance.
             *
             * @return An immutable {@link Extras} instance.
             */
            @NonNull
            public Extras build() {
                return new Extras(mFlags, mActivityOptions);
            }
        }
    }
}