com.github.michalbednarski.intentslab.valueeditors.framework.EditorLauncher.java Source code

Java tutorial

Introduction

Here is the source code for com.github.michalbednarski.intentslab.valueeditors.framework.EditorLauncher.java

Source

/*
 * IntentsLab - Android app for playing with Intents and Binder IPC
 * Copyright (C) 2014 Micha Bednarski
 *
 * 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 com.github.michalbednarski.intentslab.valueeditors.framework;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.util.SparseArray;
import android.widget.Toast;

import com.github.michalbednarski.intentslab.BuildConfig;
import com.github.michalbednarski.intentslab.R;
import com.github.michalbednarski.intentslab.Utils;
import com.github.michalbednarski.intentslab.editor.BundleAdapter;
import com.github.michalbednarski.intentslab.editor.IntentEditorActivity;
import com.github.michalbednarski.intentslab.sandbox.SandboxedObject;
import com.github.michalbednarski.intentslab.sandbox.SandboxedType;
import com.github.michalbednarski.intentslab.valueeditors.ArrayEditorFragment;
import com.github.michalbednarski.intentslab.valueeditors.BundleEditorFragment;
import com.github.michalbednarski.intentslab.valueeditors.EnumEditor;
import com.github.michalbednarski.intentslab.valueeditors.StringLikeItemEditor;
import com.github.michalbednarski.intentslab.valueeditors.object.ObjectEditorFragment;

import java.util.HashMap;

/**
 * Editor launcher for object editing
 */
public class EditorLauncher {
    /**
     * The editor registry
     *
     * @see Editor
     */
    private static final Editor[] EDITOR_REGISTRY = {
            // Boolean flipper
            new Editor.InPlaceValueToggler() {
                @Override
                public boolean canEdit(Object value) {
                    return value instanceof Boolean;
                }

                @Override
                public Object toggleObjectValue(Object originalValue) {
                    return !((Boolean) originalValue);
                }
            },

            // String like editor
            new StringLikeItemEditor.LaunchableEditor(),

            // Bundle editor
            new BundleEditorFragment.LaunchableEditor(),

            // Intent editor
            new IntentEditorActivity.LaunchableEditor(),

            // Enum editor
            new EnumEditor.LaunchableEditor(),

            // Array editor
            new ArrayEditorFragment.LaunchableEditor(),

            // Generic Parcelable object editor
            new ObjectEditorFragment.LaunchableEditor() };

    interface EditorLauncherCallbackBase {
    }

    /**
     * EditorLauncher Callback
     *
     * This is interface implemented by caller of launchEditor
     * returning data after edit, passed to constructor
     */
    public interface EditorLauncherCallback extends EditorLauncherCallbackBase {
        void onEditorResult(String key, Object newValue);
    }

    /**
     * EditorLauncher Callback with support for {@link #launchEditorForSandboxedObject(String, String, SandboxedObject)}
     *
     * This is interface implemented by caller of launchEditor
     * returning data after edit, passed to constructor
     */
    public interface EditorLauncherWithSandboxCallback extends EditorLauncherCallback {
        void onSandboxedEditorResult(String key, SandboxedObject newWrappedValue);
    }

    /**
     * EditorLauncher Callback Delegate
     *
     * This is interface implemented by caller of launchEditor
     * returning data after edit, passed to constructor
     */
    public interface EditorLauncherCallbackDelegate extends EditorLauncherCallbackBase {
        EditorLauncherCallback getEditorLauncherCallback();
    }

    private static final int REQUEST_CODE_INTERNAL_EDITOR = 0;

    private HelperFragment mHelperFragment;

    HelperFragment getHelperFragment() {
        return mHelperFragment;
    }

    private Fragment getOwnerFragment() {
        return mHelperFragment.getParentFragment();
    }

    private EditorLauncherCallback getCallback() {
        Fragment ownerFragment = getOwnerFragment();
        if (ownerFragment instanceof EditorLauncherCallbackDelegate) {
            return ((EditorLauncherCallbackDelegate) ownerFragment).getEditorLauncherCallback();
        } else {
            return (EditorLauncherCallback) ownerFragment;
        }
    }

    /**
     * For state restoring after process being killed
     *
     * ownerFragment -> editorLauncher
     */
    private static HashMap<Fragment, EditorLauncher> sPendingInitializations = new HashMap<Fragment, EditorLauncher>();

    EditorLauncher(final Fragment ownerFragment) {
        // Verify interface
        if (BuildConfig.DEBUG && !(ownerFragment instanceof EditorLauncherCallback
                || ownerFragment instanceof EditorLauncherCallbackDelegate)) {
            throw new AssertionError("Fragment doesn't implement callback interface");
        }

        // Find existing helper fragment
        FragmentManager fragmentManager = ownerFragment.getChildFragmentManager();
        mHelperFragment = (HelperFragment) fragmentManager.findFragmentByTag(TAG_HELPER_FRAGMENT);

        // Create it if it doesn't exist
        if (mHelperFragment == null) {
            mHelperFragment = new HelperFragment();
            fragmentManager.beginTransaction().add(mHelperFragment, TAG_HELPER_FRAGMENT).commit();
            fragmentManager.executePendingTransactions();

            // If our process has been restarted fragment manager won't
            // use fragment we added above.
            // Add owner (parent) fragment to pending initializations and let it
            // associate with us in it's onCreate
            sPendingInitializations.put(ownerFragment, this);
            (new Handler()).post(new Runnable() {
                @Override
                public void run() {
                    sPendingInitializations.remove(ownerFragment);
                }
            });
        }

        // Assign to helper fragment
        mHelperFragment.mEditorLauncher = this;
    }

    static EditorLauncher getExistingForFragment(Fragment ownerFragment) {
        HelperFragment helperFragment = (HelperFragment) ownerFragment.getChildFragmentManager()
                .findFragmentByTag(TAG_HELPER_FRAGMENT);
        if (helperFragment != null) {
            return helperFragment.mEditorLauncher;
        }
        return null;
    }

    public static <F extends Fragment & EditorLauncher.EditorLauncherCallbackBase> EditorLauncher getForFragment(
            F ownerFragment) {
        EditorLauncher editorLauncher = getExistingForFragment(ownerFragment);
        if (editorLauncher == null) {
            editorLauncher = new EditorLauncher(ownerFragment);
        }
        return editorLauncher;
    }

    /**
     * Start an editor or show Toast message if no applicable editor is available
     *
     * Results are returned to {@link EditorLauncher.EditorLauncherCallback#onEditorResult(String, Object) your onEditorResult method}
     *
     * @param key Key/title, will be visible to user, returned intact to onEditorResult
     * @param value Value to be edited
     */
    public void launchEditor(final String key, Object value) {
        launchEditor(key, key, value);
    }

    /**
     * Start an editor or show Toast message if no applicable editor is available
     *
     * Results are returned to {@link EditorLauncherCallback#onEditorResult(String, Object) your onEditorResult method}
     *
     * @param key Key, returned to onEditorResult
     * @param title Title that may be displayed on editor
     * @param value Value to be edited
     */
    public void launchEditor(final String key, String title, Object value) {
        launchEditor(key, title, value, null);
    }

    /**
     * Start an editor or show Toast message if no applicable editor is available
     *
     * Results are returned to {@link EditorLauncherCallback#onEditorResult(String, Object) your onEditorResult method}
     *
     * @param key Key, returned to onEditorResult
     * @param title Title that may be displayed on editor
     * @param value Value to be edited
     * @param type Type of value
     */
    public void launchEditor(final String key, String title, Object value, Class<?> type) {
        // Create new if value is null
        if (value == null && type != null) {
            launchEditorForNew(key, new SandboxedType(type));
            return;
        }

        // Find applicable editor
        for (Editor editor : EDITOR_REGISTRY) {
            if (editor.canEdit(value)) {
                if (editor instanceof Editor.EditorActivity) {
                    // Editor in activity
                    Intent editorIntent = ((Editor.EditorActivity) editor)
                            .getEditorIntent(mHelperFragment.getActivity());
                    mHelperFragment.startEditorInActivity(editorIntent, key, value);
                } else if (editor instanceof Editor.DialogFragmentEditor) {
                    // Editor in DialogFragment
                    ValueEditorDialogFragment d = ((Editor.DialogFragmentEditor) editor).getEditorDialogFragment();
                    Bundle args = new Bundle();
                    args.putString(ValueEditorDialogFragment.EXTRA_KEY, key);
                    args.putString(Editor.EXTRA_TITLE, title);
                    BundleAdapter.putInBundle(args, Editor.EXTRA_VALUE, value);
                    d.setArguments(args);
                    d.setTargetFragment(mHelperFragment, 0);
                    d.show(mHelperFragment.getActivity().getSupportFragmentManager(),
                            "DFEditorFor" + key + Math.random());
                } else if (editor instanceof Editor.FragmentEditor) {
                    Bundle args = new Bundle();
                    args.putString(SingleEditorActivity.EXTRA_ECHOED_KEY, key);
                    args.putParcelable(ValueEditorFragment.ARG_EDITED_OBJECT, new SandboxedObject(value));
                    openEditorFragment(((Editor.FragmentEditor) editor).getEditorFragment(), args);
                } else if (editor instanceof Editor.InPlaceValueToggler) {
                    // In place value toggler (eg. for Boolean)
                    Object newValue = ((Editor.InPlaceValueToggler) editor).toggleObjectValue(value);
                    getCallback().onEditorResult(key, newValue);
                }
                return;
            }
        }
        Toast.makeText(mHelperFragment.getActivity(), R.string.type_unsupported, Toast.LENGTH_SHORT).show();
    }

    public void launchEditorForNew(String key, SandboxedType type) {
        CreateNewDialog d = new CreateNewDialog();
        Bundle args = new Bundle();
        args.putString(ValueEditorDialogFragment.EXTRA_KEY, key);
        args.putParcelable(CreateNewDialog.ARG_SANDBOXED_TYPE, type);
        args.putBoolean(CreateNewDialog.ARG_ALLOW_SANDBOX, false);
        d.setArguments(args);
        d.setTargetFragment(mHelperFragment, 0);
        d.show(mHelperFragment.getActivity().getSupportFragmentManager(), "DFNewFor" + key);
    }

    /**
     * Start an editor or show Toast message if no applicable editor is available
     *
     * Results are returned to {@link EditorLauncherWithSandboxCallback#onSandboxedEditorResult(String, com.github.michalbednarski.intentslab.sandbox.SandboxedObject)}
     *
     * @param key Key, returned to onEditorResult
     * @param title Title that may be displayed on editor
     * @param wrappedValue Wrapped value to be edited
     */
    public void launchEditorForSandboxedObject(final String key, String title, SandboxedObject wrappedValue) {
        unwrap: {
            Object unwrapped;
            try {
                unwrapped = wrappedValue.unwrap(null);
            } catch (Throwable e) {
                break unwrap;
            }
            launchEditor(key, title, unwrapped);
            return;
        }

        Bundle args = new Bundle();
        args.putString(SingleEditorActivity.EXTRA_ECHOED_KEY, key);
        args.putParcelable(ValueEditorFragment.ARG_EDITED_OBJECT, wrappedValue);
        openEditorFragment(ObjectEditorFragment.class, args);
    }

    void openEditorFragment(Class<? extends ValueEditorFragment> editorFragment, Bundle args) {
        args.putString(SingleEditorActivity.EXTRA_FRAGMENT_CLASS_NAME, editorFragment.getName());
        Intent intent = new Intent(mHelperFragment.getActivity(), SingleEditorActivity.class);
        intent.replaceExtras(args);
        ChildFragmentWorkaround.startActivityForResultFromFragment(mHelperFragment, intent,
                REQUEST_CODE_INTERNAL_EDITOR);
    }

    /**
     * Extra info about started activity
     */
    private static class ActivityRequestInfo {
        String key;
        boolean isSandboxed;
        Class requestedType;

        ActivityRequestInfo(String key, boolean isSandboxed) {
            this.key = key;
            this.isSandboxed = isSandboxed;
        }

        public ActivityRequestInfo(String key, Class requestedType) {
            this.key = key;
            this.requestedType = requestedType;
        }

        Object deepCastIfNeeded(Object object) {
            if (object instanceof Object[] && requestedType != null && requestedType.isArray()
                    && !requestedType.getComponentType().isPrimitive()) {
                return Utils.deepCastArray((Object[]) object, requestedType);
            }
            return object;
        }

        // Pseudo parcelable
        ActivityRequestInfo(Parcel source) {
            key = source.readString();
            isSandboxed = source.readInt() != 0;
            String requestedTypeName = source.readString();
            if (requestedTypeName != null) {
                try {
                    requestedType = Class.forName(requestedTypeName);
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        void writeToParcel(Parcel dest) {
            dest.writeString(key);
            dest.writeInt(isSandboxed ? 1 : 0);
            dest.writeString(requestedType != null ? requestedType.getName() : null);
        }
    }

    /**
     * Parcelable class managing mapping of activity request codes and key passed to launchEditor
     */
    private static class EditorLauncherState implements Parcelable {
        private int nextFreeRequestCode = 1;
        private SparseArray<ActivityRequestInfo> requestInfos = new SparseArray<ActivityRequestInfo>();

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            final int count = requestInfos.size();
            dest.writeInt(count);
            for (int i = 0; i < count; i++) {
                dest.writeInt(requestInfos.keyAt(0));
                requestInfos.valueAt(0).writeToParcel(dest);
            }
        }

        public static final Creator<EditorLauncherState> CREATOR = new Creator<EditorLauncherState>() {
            @Override
            public EditorLauncherState createFromParcel(Parcel source) {
                final EditorLauncherState editorLauncherState = new EditorLauncherState();
                int count = source.readInt();
                int tmpNextFreeRequestCode = 1;
                for (int i = 0; i < count; i++) {
                    int requestCode = source.readInt();
                    if (requestCode >= tmpNextFreeRequestCode) {
                        tmpNextFreeRequestCode = requestCode + 1;
                    }
                    editorLauncherState.requestInfos.put(requestCode, new ActivityRequestInfo(source));
                }
                editorLauncherState.nextFreeRequestCode = tmpNextFreeRequestCode;
                return editorLauncherState;
            }

            @Override
            public EditorLauncherState[] newArray(int size) {
                return new EditorLauncherState[size];
            }
        };

        int saveRequestInfoAndGetCode(ActivityRequestInfo key) {
            int requestCode = nextFreeRequestCode++;
            requestInfos.put(requestCode, key);
            return requestCode;
        }

        ActivityRequestInfo getRequestInfoAndReleaseCode(int requestCode) {
            final ActivityRequestInfo requestInfo = requestInfos.get(requestCode);
            requestInfos.remove(requestCode);
            return requestInfo;
        }
    }

    /**
     * Fragment used for activity handling and retaining state
     * Added as child fragment of EditorLauncher owner fragment
     */
    public static class HelperFragment extends Fragment {
        public HelperFragment() {
        }

        private EditorLauncher mEditorLauncher;
        private EditorLauncherState mState;

        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            if (requestCode == REQUEST_CODE_INTERNAL_EDITOR) {
                if (resultCode == Activity.RESULT_OK) {
                    final String key = data.getStringExtra(SingleEditorActivity.EXTRA_ECHOED_KEY);
                    final SandboxedObject sandboxedObject = data
                            .getParcelableExtra(SingleEditorActivity.EXTRA_RESULT);
                    EditorLauncherCallback callback = mEditorLauncher.getCallback();
                    if (callback instanceof EditorLauncherWithSandboxCallback) {
                        ((EditorLauncherWithSandboxCallback) callback).onSandboxedEditorResult(key,
                                sandboxedObject);
                    } else {
                        callback.onEditorResult(key, sandboxedObject.unwrap(null));
                    }
                }
            } else if (requestCode >= 1 && requestCode <= 0xffff) {
                final ActivityRequestInfo requestInfo = mState.getRequestInfoAndReleaseCode(requestCode);
                final String key = requestInfo.key;
                if (data != null && data.getExtras() != null) {
                    Object newValue = data.getExtras().get(Editor.EXTRA_VALUE);
                    if (requestInfo.isSandboxed) {
                        ((EditorLauncherWithSandboxCallback) mEditorLauncher.getCallback())
                                .onSandboxedEditorResult(key, (SandboxedObject) newValue);
                    } else {
                        mEditorLauncher.getCallback().onEditorResult(key, requestInfo.deepCastIfNeeded(newValue));
                    }
                }
            } else {
                super.onActivityResult(requestCode, resultCode, data);
            }
        }

        void startEditorInActivity(Intent baseEditorIntent, String key, Object value) {
            Bundle extras = new Bundle(1);
            BundleAdapter.putInBundle(extras, Editor.EXTRA_VALUE, value);
            int requestCode = mState.saveRequestInfoAndGetCode(new ActivityRequestInfo(key, value.getClass()));
            ChildFragmentWorkaround.startActivityForResultFromFragment(this, baseEditorIntent.putExtras(extras),
                    requestCode);
        }

        void handleDialogResponse(String key, Object newValue) {
            mEditorLauncher.getCallback().onEditorResult(key, newValue);
        }

        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putParcelable(STATE_EDITOR_LAUNCHER_STATE, mState);
            Utils.putLiveRefInBundle(outState, STATE_EDITOR_LAUNCHER_INSTANCE, mEditorLauncher);
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if (savedInstanceState == null) {
                mState = new EditorLauncherState();
            } else {
                mState = savedInstanceState.getParcelable(STATE_EDITOR_LAUNCHER_STATE);
                if (mEditorLauncher == null) {
                    mEditorLauncher = Utils.getLiveRefFromBundle(savedInstanceState,
                            STATE_EDITOR_LAUNCHER_INSTANCE);
                    if (mEditorLauncher == null) {
                        mEditorLauncher = sPendingInitializations.get(getParentFragment());
                    }
                    if (mEditorLauncher != null) {
                        mEditorLauncher.mHelperFragment = this;
                    }
                }
            }
        }
    }

    private static final String STATE_EDITOR_LAUNCHER_STATE = "EditorLauncher.internal.genericState";
    private static final String STATE_EDITOR_LAUNCHER_INSTANCE = "EditorLauncher.internal.this";

    private static final String TAG_HELPER_FRAGMENT = "EditorLauncher.HF";

}