com.github.michalbednarski.intentslab.editor.BundleAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.github.michalbednarski.intentslab.editor.BundleAdapter.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.editor;

import android.os.BadParcelableException;
import android.os.Bundle;
import android.os.RemoteException;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;

import com.github.michalbednarski.intentslab.R;
import com.github.michalbednarski.intentslab.Utils;
import com.github.michalbednarski.intentslab.sandbox.ClassLoaderDescriptor;
import com.github.michalbednarski.intentslab.sandbox.ISandboxedBundle;
import com.github.michalbednarski.intentslab.sandbox.SandboxManager;
import com.github.michalbednarski.intentslab.sandbox.SandboxedObject;
import com.github.michalbednarski.intentslab.valueeditors.framework.EditorLauncher;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.regex.Pattern;

public class BundleAdapter<OwnerFragment extends Fragment & BundleAdapter.BundleAdapterAggregate>
        extends BaseAdapter implements OnClickListener, OnItemClickListener, View.OnCreateContextMenuListener,
        EditorLauncher.EditorLauncherWithSandboxCallback {
    private static final String TAG = "BundleAdapter";
    private Bundle mBundle;
    private String[] mKeys = new String[0];
    private int mKeysCount = 0;

    private boolean mShowAddNewOption = true;

    private boolean mUseSandbox = false;
    private ISandboxedBundle mSandboxedBundle = null;
    private ArrayList<Runnable> mSandboxedBundleReadyCallbacks = null;

    /*static final String[] bundleContainableTypes = { "Byte", "Char", "Short",
     "Int", "Long", "Float", "Double", "String", "CharSequence",
     "Parcelable", "ParcelableArray", "ParcelableArrayList",
     "SparseParcelableArray", "IntegerArrayList", "StringArrayList",
     "CharSequenceArrayList", "Serializable", "BooleanArray",
     "ByteArray", "ShortArray", "CharArray", "IntArray", "LongArray",
     "FloatArray", "DoubleArray", "StringArray", "CharSequenceArray",
     "Bundle", "IBinder" };*/

    public static void putInBundle(Bundle bundle, String key, Object value) {
        if (value == null) {
            bundle.putString(key, null);
            return;
        }

        Pattern putMethodName = Pattern.compile("put[A-Z][A-Za-z]+");
        for (Method method : Bundle.class.getMethods()) {
            if (putMethodName.matcher(method.getName()).matches() && !method.isVarArgs()) {
                final Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == 2 && parameterTypes[0] == String.class
                        && Utils.toWrapperClass(parameterTypes[1]).isInstance(value)) {
                    try {
                        method.invoke(bundle, key, value);
                        return;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                        // continue
                    } catch (InvocationTargetException e) {
                        throw new RuntimeException("Method " + method.getName() + " of bundle thrown exception", e);
                    }
                }
            }
        }
        throw new RuntimeException("No put* method found");
    }

    private final EditorLauncher mEditorLauncher;
    private OwnerFragment mOwnerFragment;

    public BundleAdapter(Bundle map, EditorLauncher editorLauncher, OwnerFragment ownerFragment) {
        mEditorLauncher = editorLauncher;
        mOwnerFragment = ownerFragment;
        setBundle(map);
    }

    private void updateKeySet() {
        // Try to unpack bundle without sandbox
        if (!mUseSandbox) {
            try {
                if (mBundle.keySet() != null) {
                    mKeys = mBundle.keySet().toArray(mKeys);
                    mKeysCount = mBundle.size();
                } else {
                    mKeys = new String[0];
                    mKeysCount = 0;
                }
                return;
            } catch (BadParcelableException ignored) {
            }
        }

        // If it failed or was disabled
        sandboxBundle(true, null);
    }

    private void sandboxBundle(boolean alwaysUpdateKeySet, final Runnable whenDone) {
        if (mUseSandbox) {
            // Sandbox already enabled, return if it's still alive, that is, there's no need to reinitialize
            if (mSandboxedBundle != null && mSandboxedBundle.asBinder().isBinderAlive()) {
                if (alwaysUpdateKeySet) {
                    try {
                        mKeys = mSandboxedBundle.getKeySet();
                        mKeysCount = mKeys.length;
                        notifyDataSetChanged();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                if (whenDone != null) {
                    whenDone.run();
                }
                return;
            }
        } else {
            // Start using sandbox
            mUseSandbox = true;
            SandboxManager.refSandbox();
        }

        // If it's being prepared, add to ready callbacks
        if (mSandboxedBundleReadyCallbacks != null) {
            if (whenDone != null) {
                mSandboxedBundleReadyCallbacks.add(whenDone);
            }
            return;
        }

        // Set flag we're working and add ready callback
        mSandboxedBundleReadyCallbacks = new ArrayList<Runnable>();
        if (whenDone != null) {
            mSandboxedBundleReadyCallbacks.add(whenDone);
        }

        // Initialize sandbox
        SandboxManager.initSandboxAndRunWhenReady(mOwnerFragment.getActivity(), new Runnable() {
            @Override
            public void run() {

                try {
                    // Wrap bundle
                    // TODO: don't hardcode package name
                    mSandboxedBundle = SandboxManager.getSandbox().sandboxBundle(mBundle,
                            new ClassLoaderDescriptor("com.github.michalbednarski.intentslab.samples"));

                    // Update key set
                    mKeys = mSandboxedBundle.getKeySet();
                    mKeysCount = mKeys.length;

                    // Notify all callbacks that we're ready
                    for (Runnable readyCallback : mSandboxedBundleReadyCallbacks) {
                        readyCallback.run();
                    }
                    mSandboxedBundleReadyCallbacks = null;

                    notifyDataSetChanged();
                } catch (Exception e) {
                    mSandboxedBundle = null;
                }

            }
        });
    }

    public void shutdown() {
        if (mUseSandbox) {
            SandboxManager.unrefSandbox();
        }
    }

    void setBundle(Bundle newMap) {
        if (newMap != null) {
            mBundle = new Bundle(newMap);
        } else {
            mBundle = new Bundle();
        }
        mUseSandbox = false;
        mSandboxedBundle = null;
        updateKeySet();
    }

    public Bundle getBundle() {
        if (mUseSandbox && mSandboxedBundle != null) {
            try {
                mBundle = mSandboxedBundle.getBundle();
            } catch (RemoteException e) {
                // There was some problem with sandbox,
                // but we can't do anything about that
                e.printStackTrace();
            }
        }
        return mBundle;
    }

    @Override
    public int getCount() {
        if (mUseSandbox && mSandboxedBundle == null) {
            return 0;
        }
        return mKeysCount + (mShowAddNewOption ? 1 : 0);
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getViewTypeCount() {
        return mShowAddNewOption ? 2 : 1;
    }

    @Override
    public int getItemViewType(int position) {
        if (mUseSandbox && mSandboxedBundle == null) {
            return IGNORE_ITEM_VIEW_TYPE;
        }
        return position == mKeysCount ? 1 : 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (position == mKeysCount) { // 'New' button
            if (convertView != null) {
                return convertView;
            }
            Button btn = new Button(parent.getContext());
            btn.setText(R.string.btn_add);
            btn.setOnClickListener(this);
            return btn;
        }

        View view = convertView;
        if (view == null) {
            view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_2, parent,
                    false);
        }

        final String key = mKeys[position];
        String valueAsString;

        if (mUseSandbox) {
            try {
                valueAsString = mSandboxedBundle.getAsString(key);
            } catch (RemoteException e) {
                // TODO: resandbox bundle
                valueAsString = "[Sandbox error]";
            }
        } else {
            valueAsString = String.valueOf(mBundle.get(key));
        }

        ((TextView) view.findViewById(android.R.id.text1)).setText(key);
        ((TextView) view.findViewById(android.R.id.text2)).setText(valueAsString);
        return view;
    }

    @Override
    public void onClick(View v) { // For add new button
        FragmentManager topFragmentManager = mOwnerFragment.getFragmentManager();

        if (mOwnerFragment instanceof IntentExtrasFragment
                && NewExtraPickerDialog.mayHaveSomethingForIntent(mOwnerFragment.getActivity())) {
            // We're in intent editor and may guess some extras from it
            // Launch suggestion picker
            NewExtraPickerDialog newExtraPickerDialog = new NewExtraPickerDialog(
                    (IntentExtrasFragment) mOwnerFragment);
            newExtraPickerDialog.show(topFragmentManager, "newExtraPicker");
        } else {
            // Launch generic add extra dialog
            NewBundleEntryDialog newBundleEntryDialog = new NewBundleEntryDialog(mOwnerFragment);
            newBundleEntryDialog.show(topFragmentManager, "newBundleEntryDialog");
        }
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (position != mKeysCount) {
            String key = mKeys[position];
            if (mUseSandbox) {
                SandboxedObject wrappedValue;
                try {
                    wrappedValue = mSandboxedBundle.getWrapped(key);
                } catch (RemoteException e) {
                    return;
                }
                try {
                    mEditorLauncher.launchEditor(key, wrappedValue.unwrap(null));
                } catch (BadParcelableException e) {
                    mEditorLauncher.launchEditorForSandboxedObject(key, key, wrappedValue);
                }
            } else {
                mEditorLauncher.launchEditor(key, mBundle.get(key));
            }
        }
    }

    @Override
    public void onEditorResult(final String key, final Object newValue) {

        if (mUseSandbox) {
            // Async wrap in sandbox
            sandboxBundle(false, new Runnable() {
                @Override
                public void run() {
                    boolean keySetChange = false;
                    try {
                        keySetChange = !mSandboxedBundle.containsKey(key);
                        mSandboxedBundle.putWrapped(key, new SandboxedObject(newValue));
                    } catch (Exception e) {
                        e.printStackTrace(); // Cannot recover
                    }
                    if (keySetChange) {
                        updateKeySet();
                    }
                    notifyDataSetChanged();
                    dispatchModified();
                }
            });
        } else {
            // No sandbox
            boolean keySetChange;
            keySetChange = !mBundle.containsKey(key);
            putInBundle(mBundle, key, newValue);
            if (keySetChange) {
                updateKeySet();
            }
            notifyDataSetChanged();
            dispatchModified();
        }
    }

    @Override
    public void onSandboxedEditorResult(final String key, final SandboxedObject newWrappedValue) {
        // Check if sandbox isn't needed
        skipSandbox: {
            if (!mUseSandbox) {
                Object unwrapped = null;
                try {
                    unwrapped = newWrappedValue.unwrap(null);
                } catch (Exception e) {
                    break skipSandbox;
                }
                onEditorResult(key, unwrapped);
                return;
            }
        }

        // Really use sandbox
        sandboxBundle(false, new Runnable() {
            @Override
            public void run() {
                boolean keySetChange = false;
                try {
                    keySetChange = !mSandboxedBundle.containsKey(key);
                    mSandboxedBundle.putWrapped(key, newWrappedValue);
                } catch (Exception e) {
                    e.printStackTrace(); // Cannot recover
                }
                if (keySetChange) {
                    updateKeySet();
                }
                notifyDataSetChanged();
                dispatchModified();
            }
        });
    }

    private void dispatchModified() {
        mOwnerFragment.onBundleModified();
    }

    public void settleOnList(ListView listView) {
        listView.setAdapter(this);
        listView.setOnItemClickListener(this);
        listView.setOnCreateContextMenuListener(this);
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        final AdapterView.AdapterContextMenuInfo aMenuInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
        if (aMenuInfo.position == mKeysCount) {
            return;
        }
        menu.add(mOwnerFragment.getString(R.string.delete))
                .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        final String key = mKeys[aMenuInfo.position];
                        if (mUseSandbox) {
                            try {
                                mSandboxedBundle.remove(key);
                            } catch (RemoteException e) {
                                e.printStackTrace(); // TODO: resandbox bundle
                            }
                        } else {
                            mBundle.remove(key);
                        }
                        updateKeySet();
                        notifyDataSetChanged();
                        return true;
                    }
                });
    }

    public void launchEditorForNewEntry(String key, Object initialValue) {
        mEditorLauncher.launchEditor(key, initialValue);
    }

    public interface BundleAdapterAggregate {
        public BundleAdapter getBundleAdapter();

        void onBundleModified();
    }
}