com.achep.base.ui.activities.SettingsActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.achep.base.ui.activities.SettingsActivity.java

Source

/*
 * Copyright (C) 2015 AChep@xda <artemchep@gmail.com>
 *
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */
package com.achep.base.ui.activities;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.annotation.XmlRes;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.transition.TransitionManager;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import android.view.ViewGroup;

import com.achep.acdisplay.R;
import com.achep.acdisplay.ui.activities.settings.Settings2;
import com.achep.base.Device;
import com.achep.base.dashboard.DashboardCategory;
import com.achep.base.dashboard.DashboardTile;
import com.achep.base.ui.fragments.DashboardFragment;
import com.achep.base.utils.xml.XmlUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public abstract class SettingsActivity extends ActivityBase implements
        PreferenceFragment.OnPreferenceStartFragmentCallback, FragmentManager.OnBackStackChangedListener {

    private static final String LOG_TAG = "Settings";

    // Constants for loading dashboard from xml resource.
    private static final String RESOURCE_TAG_DASHBOARD = "dashboard";
    private static final String RESOURCE_TAG_DASHBOARD_CATEGORY = "dashboard-category";
    private static final String RESOURCE_TAG_DASHBOARD_TILE = "dashboard-tile";
    private static final String RESOURCE_TAG_DASHBOARD_TILE_EXTRA = "extra";
    private static final String RESOURCE_TAG_DASHBOARD_TILE_INTENT = "intent";

    // Constants for state save/restore
    private static final String SAVE_KEY_CATEGORIES = ":settings:categories";
    private static final String SAVE_KEY_SHOW_HOME_AS_UP = ":settings:show_home_as_up";

    /**
     * When starting this activity, the invoking Intent can contain this extra
     * string to specify which fragment should be initially displayed.
     * <p/>Starting from Key Lime Pie, when this argument is passed in, the activity
     * will call isValidFragment() to confirm that the fragment class name is valid for this
     * activity.
     */
    public static final String EXTRA_SHOW_FRAGMENT = ":settings:show_fragment";

    /**
     * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT},
     * this extra can also be specified to supply a Bundle of arguments to pass
     * to that fragment when it is instantiated during the initial creation
     * of the activity.
     */
    public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args";

    /**
     * Fragment "key" argument passed thru {@link #EXTRA_SHOW_FRAGMENT_ARGUMENTS}
     */
    public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";

    public static final String BACK_STACK_PREFS = ":settings:prefs";

    /**
     * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT},
     * those extra can also be specify to supply the title or title res id to be shown for
     * that fragment.
     */
    public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":settings:show_fragment_title";

    public static final String EXTRA_SHOW_FRAGMENT_TITLE_RESID = ":settings:show_fragment_title_resid";

    public static final String EXTRA_SHOW_FRAGMENT_AS_SHORTCUT = ":settings:show_fragment_as_shortcut";

    public static final String EXTRA_SHOW_FRAGMENT_AS_SUBSETTING = ":settings:show_fragment_as_subsetting";

    private static final String META_DATA_KEY_FRAGMENT_CLASS = "com.android.settings.FRAGMENT_CLASS";

    private static final String EXTRA_UI_OPTIONS = "settings:ui_options";

    private String mFragmentClass;

    private CharSequence mInitialTitle;
    private int mInitialTitleResId;

    private static final String[] LIKE_SHORTCUT_INTENT_ACTION_ARRAY = {
            "android.settings.APPLICATION_DETAILS_SETTINGS" };

    private ViewGroup mContent;

    private boolean mDisplayHomeAsUpEnabled;
    private boolean mIsShortcut;

    private ArrayList<DashboardCategory> mCategories = new ArrayList<>();

    public List<DashboardCategory> getDashboardCategories(boolean forceRefresh) {
        if (forceRefresh || mCategories.size() == 0) {
            buildDashboard(mCategories);
        }
        return mCategories;
    }

    @Override
    public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference p) {
        startPreferencePanel(p.getFragment(), p.getExtras(), p.getTitleRes(), p.getTitle(), null, 0);
        return true;
    }

    private static boolean isShortCutIntent(final Intent intent) {
        Set<String> categories = intent.getCategories();
        return (categories != null) && categories.contains("com.android.settings.SHORTCUT");
    }

    private static boolean isLikeShortCutIntent(final Intent intent) {
        String action = intent.getAction();
        if (action == null)
            return false;

        for (String item : LIKE_SHORTCUT_INTENT_ACTION_ARRAY)
            if (item.equals(action))
                return true;
        return false;
    }

    @Override
    protected void onCreate(Bundle savedState) {
        super.onCreate(savedState);

        // Should happen before any call to getIntent()
        getMetaData();

        final Intent intent = getIntent();
        if (intent.hasExtra(EXTRA_UI_OPTIONS)) {
            getWindow().setUiOptions(intent.getIntExtra(EXTRA_UI_OPTIONS, 0));
        }

        // Getting Intent properties can only be done after the super.onCreate(...)
        final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);

        mIsShortcut = isShortCutIntent(intent) || isLikeShortCutIntent(intent)
                || intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, false);

        final ComponentName cn = intent.getComponent();
        final String className = cn.getClassName();

        boolean isShowingDashboard = className.equals(Settings2.class.getName());

        // This is a "Sub Settings" when:
        // - this is a real SubSettings
        // - or :settings:show_fragment_as_subsetting is passed to the Intent
        final boolean isSubSettings = className.equals(SubSettings.class.getName())
                || intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false);

        // If this is a sub settings, then apply the SubSettings Theme for the ActionBar content insets
        if (isSubSettings) {
            // Check also that we are not a Theme Dialog as we don't want to override them
            /*
            final int themeResId = getTheme(). getThemeResId();
            if (themeResId != R.style.Theme_DialogWhenLarge &&
                themeResId != R.style.Theme_SubSettingsDialogWhenLarge) {
            setTheme(R.style.Theme_SubSettings);
            }
            */
        }

        setContentView(R.layout.settings_main_dashboard);

        mContent = (ViewGroup) findViewById(android.R.id.content);

        getSupportFragmentManager().addOnBackStackChangedListener(this);

        if (savedState != null) {
            // We are restarting from a previous saved state; used that to initialize, instead
            // of starting fresh.

            setTitleFromIntent(intent);

            ArrayList<DashboardCategory> categories = savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
            if (categories != null) {
                mCategories.clear();
                mCategories.addAll(categories);
                setTitleFromBackStack();
            }

            mDisplayHomeAsUpEnabled = savedState.getBoolean(SAVE_KEY_SHOW_HOME_AS_UP);
        } else {
            if (!isShowingDashboard) {
                mDisplayHomeAsUpEnabled = isSubSettings;
                setTitleFromIntent(intent);

                Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
                switchToFragment(initialFragmentName, initialArguments, true, false, mInitialTitleResId,
                        mInitialTitle, false);
            } else {
                mDisplayHomeAsUpEnabled = false;
                mInitialTitleResId = R.string.app_name;
                switchToFragment(DashboardFragment.class.getName(), null, false, false, mInitialTitleResId,
                        mInitialTitle, false);
            }
        }

        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(mDisplayHomeAsUpEnabled);
            actionBar.setHomeButtonEnabled(mDisplayHomeAsUpEnabled);
        }
    }

    /*
    @Override
    public boolean onNavigateUp() {
    startActivity(new Intent(this, MainActivity.class)
            .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
            .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
    return true;
    }
    */

    private void setTitleFromIntent(Intent intent) {
        final int initialTitleResId = intent.getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, -1);
        if (initialTitleResId > 0) {
            mInitialTitle = null;
            mInitialTitleResId = initialTitleResId;
            setTitle(mInitialTitleResId);
        } else {
            mInitialTitleResId = -1;
            final String initialTitle = intent.getStringExtra(EXTRA_SHOW_FRAGMENT_TITLE);
            mInitialTitle = (initialTitle != null) ? initialTitle : getTitle();
            setTitle(mInitialTitle);
        }
    }

    @Override
    public void onBackStackChanged() {
        setTitleFromBackStack();
    }

    private int setTitleFromBackStack() {
        final int count = getFragmentManager().getBackStackEntryCount();

        if (count == 0) {
            if (mInitialTitleResId > 0) {
                setTitle(mInitialTitleResId);
            } else {
                setTitle(mInitialTitle);
            }
            return 0;
        }

        FragmentManager.BackStackEntry bse = getSupportFragmentManager().getBackStackEntryAt(count - 1);
        setTitleFromBackStackEntry(bse);

        return count;
    }

    private void setTitleFromBackStackEntry(FragmentManager.BackStackEntry bse) {
        final CharSequence title;
        final int titleRes = bse.getBreadCrumbTitleRes();
        if (titleRes > 0) {
            title = getText(titleRes);
        } else {
            title = bse.getBreadCrumbTitle();
        }
        if (title != null) {
            setTitle(title);
        }
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);

        if (mCategories.size() > 0) {
            outState.putParcelableArrayList(SAVE_KEY_CATEGORIES, mCategories);
        }

        outState.putBoolean(SAVE_KEY_SHOW_HOME_AS_UP, mDisplayHomeAsUpEnabled);
    }

    protected boolean isValidFragment(@NonNull String fragmentName) {
        return false;
    }

    @XmlRes
    protected int getDashboardResource() {
        return 0;
    }

    protected abstract boolean isTileSupported(@NonNull DashboardTile tile);

    @Override
    public Intent getIntent() {
        Intent superIntent = super.getIntent();
        String startingFragment = getStartingFragmentClass(superIntent);
        // This is called from super.onCreate, isMultiPane() is not yet reliable
        // Do not use onIsHidingHeaders either, which relies itself on this method
        if (startingFragment != null) {
            Intent modIntent = new Intent(superIntent);
            modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
            Bundle args = superIntent.getExtras();
            if (args != null) {
                args = new Bundle(args);
            } else {
                args = new Bundle();
            }
            args.putParcelable("intent", superIntent);
            modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
            return modIntent;
        }
        return superIntent;
    }

    /**
     * Checks if the component name in the intent is different from the Settings class and
     * returns the class name to load as a fragment.
     */
    private String getStartingFragmentClass(Intent intent) {
        if (mFragmentClass != null)
            return mFragmentClass;

        String intentClass = intent.getComponent().getClassName();
        return intentClass.equals(getClass().getName()) ? null : intentClass;
    }

    /**
     * Start a new fragment containing a preference panel.  If the preferences
     * are being displayed in multi-pane mode, the given fragment class will
     * be instantiated and placed in the appropriate pane.  If running in
     * single-pane mode, a new activity will be launched in which to show the
     * fragment.
     *
     * @param fragmentClass     Full name of the class implementing the fragment.
     * @param args              Any desired arguments to supply to the fragment.
     * @param titleRes          Optional resource identifier of the title of this
     *                          fragment.
     * @param titleText         Optional text of the title of this fragment.
     * @param resultTo          Optional fragment that result data should be sent to.
     *                          If non-null, resultTo.onActivityResult() will be called when this
     *                          preference panel is done.  The launched panel must use
     *                          {@link #finishPreferencePanel(Fragment, int, Intent)} when done.
     * @param resultRequestCode If resultTo is non-null, this is the caller's
     *                          request code to be received with the result.
     */
    public void startPreferencePanel(String fragmentClass, Bundle args, int titleRes, CharSequence titleText,
            Fragment resultTo, int resultRequestCode) {
        String title = null;
        if (titleRes < 0) {
            if (titleText != null) {
                title = titleText.toString();
            } else {
                // There not much we can do in that case
                title = "";
            }
        }

        Utils.startWithFragment(this, fragmentClass, args, resultTo, resultRequestCode, titleRes, title,
                mIsShortcut);
    }

    /**
     * Switch to a specific Fragment with taking care of validation, Title and BackStack
     */
    private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate, boolean addToBackStack,
            int titleResId, CharSequence title, boolean withTransition) {
        if (validate && !isValidFragment(fragmentName)) {
            String message = "Invalid fragment for this activity: " + fragmentName;
            throw new IllegalArgumentException(message);
        }

        Fragment f = Fragment.instantiate(this, fragmentName, args);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(android.R.id.content, f);

        if (withTransition && Device.hasKitKatApi())
            TransitionManager.beginDelayedTransition(mContent);
        if (addToBackStack)
            transaction.addToBackStack(SettingsActivity.BACK_STACK_PREFS);
        if (titleResId > 0) {
            transaction.setBreadCrumbTitle(titleResId);
        } else if (title != null) {
            transaction.setBreadCrumbTitle(title);
        }

        transaction.commitAllowingStateLoss();
        getFragmentManager().executePendingTransactions();
        return f;
    }

    /**
     * Called when the activity needs its list of categories/tiles built.
     *
     * @param categories The list in which to place the tiles categories.
     */
    private void buildDashboard(@NonNull List<DashboardCategory> categories) {
        categories.clear();
        loadDashboardFromResource(getDashboardResource(), categories);
        updateDashboardTilesList(categories);
    }

    /**
     * Parse the given XML file as a categories description, adding each
     * parsed categories and tiles into the target list.
     *
     * @param resourceId The XML resource to load and parse.
     * @param target     The list in which the parsed categories and tiles should be placed.
     */
    protected final void loadDashboardFromResource(@XmlRes int resourceId,
            @NonNull List<DashboardCategory> target) {
        XmlResourceParser parser = null;
        try {
            parser = getResources().getXml(resourceId);
            AttributeSet attrs = Xml.asAttributeSet(parser);

            int type;

            for (type = parser.next(); type != XmlPullParser.END_DOCUMENT; type = parser.next()) {
                if (type == XmlPullParser.START_TAG)
                    break;
            }

            String nodeName = parser.getName();
            if (!RESOURCE_TAG_DASHBOARD.equals(nodeName))
                throw new RuntimeException(String.format("XML document must start with <%s> tag; found %s at %s",
                        RESOURCE_TAG_DASHBOARD, nodeName, parser.getPositionDescription()));

            for (type = parser.next(); type != XmlPullParser.END_DOCUMENT; type = parser.next()) {
                if (type == XmlPullParser.END_TAG && parser.getDepth() <= 1 /* root tag */)
                    break;
                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                    continue;
                }

                switch (parser.getName()) {
                case RESOURCE_TAG_DASHBOARD_CATEGORY:
                    DashboardCategory category = new DashboardCategory(this, attrs);

                    final int categoryDepth = parser.getDepth();
                    for (type = parser.next(); type != XmlPullParser.END_DOCUMENT; type = parser.next()) {
                        if (type == XmlPullParser.END_TAG && parser.getDepth() <= categoryDepth)
                            break;
                        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                            continue;
                        }

                        switch (parser.getName()) {
                        case RESOURCE_TAG_DASHBOARD_TILE:
                            DashboardTile tile = new DashboardTile(this, attrs);
                            Bundle bundle = null;

                            final int tileDepth = parser.getDepth();
                            for (type = parser.next(); type != XmlPullParser.END_DOCUMENT; type = parser.next()) {
                                if (type == XmlPullParser.END_TAG && parser.getDepth() <= tileDepth)
                                    break;
                                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                                    continue;
                                }

                                switch (parser.getName()) {
                                case RESOURCE_TAG_DASHBOARD_TILE_EXTRA:
                                    if (bundle == null) {
                                        bundle = new Bundle();
                                    }

                                    getResources().parseBundleExtra(RESOURCE_TAG_DASHBOARD_TILE_EXTRA, attrs,
                                            bundle);
                                    XmlUtils.skipCurrentTag(parser);
                                    break;
                                case RESOURCE_TAG_DASHBOARD_TILE_INTENT:
                                    tile.intent = Intent.parseIntent(getResources(), parser, attrs);
                                    break;
                                default:
                                    XmlUtils.skipCurrentTag(parser);
                                }
                            }

                            tile.fragmentArguments = bundle;
                            category.add(tile);
                            break;
                        default:
                            XmlUtils.skipCurrentTag(parser);
                        }
                    }

                    target.add(category);
                    break;
                default:
                    XmlUtils.skipCurrentTag(parser);
                }
            }
        } catch (XmlPullParserException | IOException e) {
            throw new RuntimeException("Error parsing categories", e);
        } finally {
            if (parser != null)
                parser.close();
        }
    }

    private void updateDashboardTilesList(List<DashboardCategory> target) {
        for (int i = target.size() - 1; i >= 0; i--) {
            final DashboardCategory category = target.get(i);

            for (int j = category.size() - 1; j >= 0; j--) {
                final DashboardTile tile = category.get(j);
                final boolean removeTile = !isTileSupported(tile);
                if (removeTile) {
                    category.remove(j);
                }
            }

            if (category.isEmpty()) {
                target.remove(i);
            }
        }
    }

    private void getMetaData() {
        try {
            ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
            if (ai == null || ai.metaData == null)
                return;
            mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
        } catch (NameNotFoundException nnfe) {
            // No recovery
            Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());
        }
    }

    @Override
    public boolean shouldUpRecreateTask(@NonNull Intent unused) {
        return super.shouldUpRecreateTask(new Intent(this, SettingsActivity.class));
    }

    public static final class Utils {

        /**
         * Start a new instance of the activity, showing only the given fragment.
         * When launched in this mode, the given preference fragment will be instantiated and fill the
         * entire activity.
         *
         * @param context           The context.
         * @param fragmentName      The name of the fragment to display.
         * @param args              Optional arguments to supply to the fragment.
         * @param resultTo          Option fragment that should receive the result of the activity launch.
         * @param resultRequestCode If resultTo is non-null, this is the request code in which
         *                          to report the result.
         * @param titleResId        resource id for the String to display for the title of this set
         *                          of preferences.
         * @param title             String to display for the title of this set of preferences.
         */
        public static void startWithFragment(Context context, String fragmentName, Bundle args, Fragment resultTo,
                int resultRequestCode, int titleResId, CharSequence title) {
            startWithFragment(context, fragmentName, args, resultTo, resultRequestCode, titleResId, title,
                    false /* not a shortcut */);
        }

        public static void startWithFragment(Context context, String fragmentName, Bundle args, Fragment resultTo,
                int resultRequestCode, int titleResId, CharSequence title, boolean isShortcut) {
            Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResId, title, isShortcut);
            if (resultTo == null) {
                context.startActivity(intent);
            } else {
                resultTo.startActivityForResult(intent, resultRequestCode);
            }
        }

        /**
         * Build an Intent to launch a new activity showing the selected fragment.
         * The implementation constructs an Intent that re-launches the current activity with the
         * appropriate arguments to display the fragment.
         *
         * @param context      The Context.
         * @param fragmentName The name of the fragment to display.
         * @param args         Optional arguments to supply to the fragment.
         * @param titleResId   Optional title resource id to show for this item.
         * @param title        Optional title to show for this item.
         * @param isShortcut   tell if this is a Launcher Shortcut or not
         * @return Returns an Intent that can be launched to display the given
         * fragment.
         */
        public static Intent onBuildStartFragmentIntent(Context context, String fragmentName, Bundle args,
                int titleResId, CharSequence title, boolean isShortcut) {
            Intent intent = new Intent(Intent.ACTION_MAIN);
            intent.setClass(context, SubSettings.class);
            intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName);
            intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
            intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, titleResId);
            intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE, title);
            intent.putExtra(EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, isShortcut);
            return intent;
        }

    }
}