org.openhab.habdroid.ui.activity.ContentController.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.habdroid.ui.activity.ContentController.java

Source

/*
 * Copyright (c) 2018, openHAB.org and others.
 *
 *   All rights reserved. This program and the accompanying materials
 *   are made available under the terms of the Eclipse Public License v1.0
 *   which accompanies this distribution, and is available at
 *   http://www.eclipse.org/legal/epl-v10.html
 */

package org.openhab.habdroid.ui.activity;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.AnimRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import org.openhab.habdroid.R;
import org.openhab.habdroid.core.connection.Connection;
import org.openhab.habdroid.core.connection.ConnectionFactory;
import org.openhab.habdroid.model.OpenHABLinkedPage;
import org.openhab.habdroid.model.OpenHABSitemap;
import org.openhab.habdroid.model.OpenHABWidget;
import org.openhab.habdroid.ui.OpenHABMainActivity;
import org.openhab.habdroid.ui.OpenHABNotificationFragment;
import org.openhab.habdroid.ui.OpenHABPreferencesActivity;
import org.openhab.habdroid.ui.OpenHABWidgetListFragment;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;

/**
 * Controller class for the content area of {@link OpenHABMainActivity}
 *
 * It manages the stack of widget lists shown, and shows error UI if needed.
 * The layout of the content area is up to the respective subclasses.
 */
public abstract class ContentController implements PageConnectionHolderFragment.ParentCallback {
    private static final String TAG = ContentController.class.getSimpleName();

    private final OpenHABMainActivity mActivity;
    protected final FragmentManager mFm;

    protected Fragment mNoConnectionFragment;
    protected Fragment mDefaultProgressFragment;
    private PageConnectionHolderFragment mConnectionFragment;
    private Fragment mTemporaryPage;

    protected OpenHABSitemap mCurrentSitemap;
    protected OpenHABWidgetListFragment mSitemapFragment;
    protected final Stack<Pair<OpenHABLinkedPage, OpenHABWidgetListFragment>> mPageStack = new Stack<>();
    private Set<String> mPendingDataLoadUrls = new HashSet<>();

    protected ContentController(OpenHABMainActivity activity) {
        mActivity = activity;
        mFm = activity.getSupportFragmentManager();

        mConnectionFragment = (PageConnectionHolderFragment) mFm.findFragmentByTag("connections");
        if (mConnectionFragment == null) {
            mConnectionFragment = new PageConnectionHolderFragment();
            mFm.beginTransaction().add(mConnectionFragment, "connections").commit();
        }
        mDefaultProgressFragment = ProgressFragment.newInstance(null, false);
        mConnectionFragment.setCallback(this);
    }

    /**
     * Saves the controller's instance state
     * To be called from the onSaveInstanceState callback of the activity
     *
     * @param state Bundle to save state into
     */
    public void onSaveInstanceState(Bundle state) {
        ArrayList<OpenHABLinkedPage> pages = new ArrayList<>();
        for (Pair<OpenHABLinkedPage, OpenHABWidgetListFragment> item : mPageStack) {
            pages.add(item.first);
            if (item.second.isAdded()) {
                mFm.putFragment(state, "pageFragment-" + item.first.link(), item.second);
            }
        }
        state.putParcelable("controllerSitemap", mCurrentSitemap);
        if (mSitemapFragment != null && mSitemapFragment.isAdded()) {
            mFm.putFragment(state, "sitemapFragment", mSitemapFragment);
        }
        if (mDefaultProgressFragment.isAdded()) {
            mFm.putFragment(state, "progressFragment", mDefaultProgressFragment);
        }
        state.putParcelableArrayList("controllerPages", pages);
        if (mTemporaryPage != null) {
            mFm.putFragment(state, "temporaryPage", mTemporaryPage);
        }
    }

    /**
     * Restore instance state previously saved by onSaveInstanceState
     * To be called from the onRestoreInstanceState or onCreate callbacks of the activity
     *
     * @param state Bundle including previously saved state
     */
    public void onRestoreInstanceState(Bundle state) {
        mCurrentSitemap = state.getParcelable("controllerSitemap");
        if (mCurrentSitemap != null) {
            mSitemapFragment = (OpenHABWidgetListFragment) mFm.getFragment(state, "sitemapFragment");
            if (mSitemapFragment == null) {
                mSitemapFragment = makeSitemapFragment(mCurrentSitemap);
            }
        }
        Fragment progressFragment = mFm.getFragment(state, "progressFragment");
        if (progressFragment != null) {
            mDefaultProgressFragment = progressFragment;
        }

        ArrayList<OpenHABLinkedPage> oldStack = state.getParcelableArrayList("controllerPages");
        mPageStack.clear();
        for (OpenHABLinkedPage page : oldStack) {
            OpenHABWidgetListFragment f = (OpenHABWidgetListFragment) mFm.getFragment(state,
                    "pageFragment-" + page.link());
            mPageStack.add(Pair.create(page, f != null ? f : makePageFragment(page)));
        }
        mTemporaryPage = mFm.getFragment(state, "temporaryPage");
    }

    /**
     * Show contents of a sitemap
     * Sets up UI to show the sitemap's contents
     *
     * @param sitemap Sitemap to show
     */
    public void openSitemap(OpenHABSitemap sitemap) {
        Log.d(TAG, "Opening sitemap " + sitemap);
        mCurrentSitemap = sitemap;
        // First clear the old fragment stack to show the progress spinner...
        mPageStack.clear();
        mSitemapFragment = null;
        updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
        // ...then create the new sitemap fragment and trigger data loading.
        mSitemapFragment = makeSitemapFragment(sitemap);
        handleNewWidgetFragment(mSitemapFragment);
    }

    /**
     * Follow a link in a sitemap page
     * Sets up UI to show the contents of the given page
     *
     * @param page Page link to follow
     * @param source Fragment this action was triggered from
     */
    public void openPage(OpenHABLinkedPage page, OpenHABWidgetListFragment source) {
        Log.d(TAG, "Opening page " + page);
        OpenHABWidgetListFragment f = makePageFragment(page);
        while (!mPageStack.isEmpty() && mPageStack.peek().second != source) {
            mPageStack.pop();
        }
        mPageStack.push(Pair.create(page, f));
        handleNewWidgetFragment(f);
        mActivity.setProgressIndicatorVisible(true);
    }

    /**
     * Follow a sitemap page link via URL
     * If a page with the given URL is already present in the back stack,
     * that page is brought to the front; otherwise a temporary page with showing
     * the contents of the linked page is opened.
     *
     * @param url URL to follow
     */
    public final void openPage(String url) {
        int toPop = -1;
        for (int i = 0; i < mPageStack.size(); i++) {
            if (mPageStack.get(i).first.link().equals(url)) {
                // page is already present
                toPop = mPageStack.size() - i - 1;
                break;
            }
        }
        Log.d(TAG, "Opening page " + url + " (pop count " + toPop + ")");
        if (toPop >= 0) {
            while (toPop-- > 0) {
                mPageStack.pop();
            }
            updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
            updateConnectionState();
            mActivity.updateTitle();
        } else {
            // we didn't find it
            showTemporaryPage(OpenHABWidgetListFragment.withPage(url, null));
        }
    }

    /**
     * Indicate to the user that no network connectivity is present
     *
     * @param message Error message to show
     */
    public void indicateNoNetwork(CharSequence message) {
        Log.d(TAG, "Indicate no network (message " + message + ")");
        resetState();
        mNoConnectionFragment = NoNetworkFragment.newInstance(message);
        updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
        mActivity.updateTitle();
    }

    /**
     * Indicate to the user that server configuration is missing
     */
    public void indicateMissingConfiguration() {
        Log.d(TAG, "Indicate missing configuration");
        resetState();
        mNoConnectionFragment = MissingConfigurationFragment.newInstance(mActivity);
        updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
        mActivity.updateTitle();
    }

    /**
     * Indicate to the user that there was a failure in talking to the server
     *
     * @param message Error message to show
     */
    public void indicateServerCommunicationFailure(CharSequence message) {
        Log.d(TAG, "Indicate server failure (message " + message + ")");
        mNoConnectionFragment = CommunicationFailureFragment.newInstance(message);
        updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
        mActivity.updateTitle();
    }

    /**
     * Clear the error previously set by {@link #indicateServerCommunicationFailure}
     */
    public void clearServerCommunicationFailure() {
        if (mNoConnectionFragment instanceof CommunicationFailureFragment) {
            mNoConnectionFragment = null;
            updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
            mActivity.updateTitle();
        }
    }

    /**
     * Update the used connection.
     * To be called when the available connection changes.
     *
     * @param connection New connection to use; might be null if none is currently available
     * @param progressMessage Message to show to the user if no connection is available
     */
    public void updateConnection(Connection connection, CharSequence progressMessage) {
        Log.d(TAG, "Update to connection " + connection + " (message " + progressMessage + ")");
        if (connection == null) {
            mNoConnectionFragment = ProgressFragment.newInstance(progressMessage, progressMessage != null);
        } else {
            mNoConnectionFragment = null;
        }
        resetState();
        updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
        // Make sure dropped fragments are destroyed immediately to get their views recycled
        mFm.executePendingTransactions();
    }

    /**
     * Open a temporary page showing the notification list
     */
    public final void openNotifications() {
        showTemporaryPage(OpenHABNotificationFragment.newInstance());
    }

    /**
     * Recreate all UI state
     * To be called from the activity's onCreate callback if the used controller changes
     */
    public void recreateFragmentState() {
        FragmentTransaction ft = mFm.beginTransaction();
        for (Fragment f : mFm.getFragments()) {
            if (!f.getRetainInstance()) {
                ft.remove(f);
            }
        }
        ft.commitNow();

        updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
    }

    /**
     * Inflate controller views
     * To be called after activity content view inflation
     *
     * @param stub View stub to inflate controller views into
     */
    public abstract void inflateViews(ViewStub stub);

    /**
     * Ask the connection controller to deliver content updates for a given page
     *
     * @param pageUrl URL of the content page
     * @param forceReload Whether to discard previously cached state
     */
    public void triggerPageUpdate(String pageUrl, boolean forceReload) {
        mConnectionFragment.triggerUpdate(pageUrl, forceReload);
    }

    /**
     * Get title describing current UI state
     *
     * @return Title to show in action bar, or null if none can be determined
     */
    public CharSequence getCurrentTitle() {
        if (mNoConnectionFragment != null) {
            return null;
        } else if (mTemporaryPage != null) {
            if (mTemporaryPage instanceof OpenHABNotificationFragment) {
                return mActivity.getString(R.string.app_notifications);
            } else if (mTemporaryPage instanceof OpenHABWidgetListFragment) {
                return ((OpenHABWidgetListFragment) mTemporaryPage).getTitle();
            }
            return null;
        } else {
            OpenHABWidgetListFragment f = getFragmentForTitle();
            return f != null ? f.getTitle() : null;
        }
    }

    /**
     * Checks whether the controller currently can consume the back key
     *
     * @return true if back key can be consumed, false otherwise
     */
    public boolean canGoBack() {
        return mTemporaryPage != null || !mPageStack.empty();
    }

    /**
     * Consumes the back key
     * To be called from activity onBackKeyPressed callback
     *
     * @return true if back key was consumed, false otherwise
     */
    public boolean goBack() {
        if (mTemporaryPage != null) {
            mTemporaryPage = null;
            mActivity.updateTitle();
            updateFragmentState(FragmentUpdateReason.PAGE_UPDATE);
            updateConnectionState();
            return true;
        }
        if (!mPageStack.empty()) {
            mPageStack.pop();
            mActivity.updateTitle();
            updateFragmentState(FragmentUpdateReason.BACK_NAVIGATION);
            updateConnectionState();
            return true;
        }
        return false;
    }

    @Override
    public boolean serverReturnsJson() {
        return mActivity.getOpenHABVersion() != 1;
    }

    @Override
    public String getIconFormat() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
        return prefs.getString("iconFormatType", "PNG");
    }

    @Override
    public void onPageUpdated(String pageUrl, String pageTitle, List<OpenHABWidget> widgets) {
        Log.d(TAG, "Got update for URL " + pageUrl + ", pending " + mPendingDataLoadUrls);
        for (OpenHABWidgetListFragment f : collectWidgetFragments()) {
            if (pageUrl.equals(f.getDisplayPageUrl())) {
                f.update(pageTitle, widgets);
                break;
            }
        }
        if (mPendingDataLoadUrls.remove(pageUrl) && mPendingDataLoadUrls.isEmpty()) {
            mActivity.setProgressIndicatorVisible(false);
            mActivity.updateTitle();
            updateFragmentState(
                    mPageStack.isEmpty() ? FragmentUpdateReason.PAGE_UPDATE : FragmentUpdateReason.PAGE_ENTER);
        }
    }

    @Override
    public void onWidgetUpdated(OpenHABWidget widget) {
        for (OpenHABWidgetListFragment f : collectWidgetFragments()) {
            if (f.onWidgetUpdated(widget)) {
                break;
            }
        }
    }

    protected abstract void updateFragmentState(FragmentUpdateReason reason);

    protected abstract OpenHABWidgetListFragment getFragmentForTitle();

    private void handleNewWidgetFragment(OpenHABWidgetListFragment f) {
        mPendingDataLoadUrls.add(f.getDisplayPageUrl());
        // no fragment update yet; fragment state will be updated when data arrives
        updateConnectionState();
    }

    private void showTemporaryPage(Fragment page) {
        mTemporaryPage = page;
        updateFragmentState(FragmentUpdateReason.TEMPORARY_PAGE);
        updateConnectionState();
        mActivity.updateTitle();
    }

    protected Fragment getOverridingFragment() {
        if (mNoConnectionFragment != null) {
            return mNoConnectionFragment;
        }
        if (mTemporaryPage != null) {
            return mTemporaryPage;
        }
        return null;
    }

    protected void updateConnectionState() {
        List<String> pageUrls = new ArrayList<>();
        for (OpenHABWidgetListFragment f : collectWidgetFragments()) {
            pageUrls.add(f.getDisplayPageUrl());
        }
        Iterator<String> pendingIter = mPendingDataLoadUrls.iterator();
        while (pendingIter.hasNext()) {
            if (!pageUrls.contains(pendingIter.next())) {
                pendingIter.remove();
            }
        }
        mConnectionFragment.updateActiveConnections(pageUrls, mActivity.getConnection());
    }

    private void resetState() {
        mCurrentSitemap = null;
        mSitemapFragment = null;
        mPageStack.clear();
        updateConnectionState();
    }

    private List<OpenHABWidgetListFragment> collectWidgetFragments() {
        List<OpenHABWidgetListFragment> result = new ArrayList<>();
        if (mSitemapFragment != null) {
            result.add(mSitemapFragment);
        }
        for (Pair<OpenHABLinkedPage, OpenHABWidgetListFragment> item : mPageStack) {
            result.add(item.second);
        }
        if (mTemporaryPage instanceof OpenHABWidgetListFragment) {
            result.add((OpenHABWidgetListFragment) mTemporaryPage);
        }
        return result;
    }

    private OpenHABWidgetListFragment makeSitemapFragment(OpenHABSitemap sitemap) {
        return OpenHABWidgetListFragment.withPage(sitemap.homepageLink(), sitemap.label());
    }

    private OpenHABWidgetListFragment makePageFragment(OpenHABLinkedPage page) {
        return OpenHABWidgetListFragment.withPage(page.link(), page.title());
    }

    protected enum FragmentUpdateReason {
        PAGE_ENTER, BACK_NAVIGATION, TEMPORARY_PAGE, PAGE_UPDATE
    }

    protected static @AnimRes int determineEnterAnim(FragmentUpdateReason reason) {
        switch (reason) {
        case PAGE_ENTER:
            return R.anim.slide_in_right;
        case TEMPORARY_PAGE:
            return R.anim.slide_in_bottom;
        case BACK_NAVIGATION:
            return R.anim.slide_in_left;
        default:
            return 0;
        }
    }

    protected static @AnimRes int determineExitAnim(FragmentUpdateReason reason) {
        switch (reason) {
        case PAGE_ENTER:
            return R.anim.slide_out_left;
        case TEMPORARY_PAGE:
            return R.anim.slide_out_bottom;
        case BACK_NAVIGATION:
            return R.anim.slide_out_right;
        default:
            return 0;
        }
    }

    protected static int determineTransition(FragmentUpdateReason reason) {
        switch (reason) {
        case PAGE_ENTER:
            return FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
        case BACK_NAVIGATION:
            return FragmentTransaction.TRANSIT_FRAGMENT_CLOSE;
        default:
            return FragmentTransaction.TRANSIT_FRAGMENT_FADE;
        }
    }

    public static class CommunicationFailureFragment extends StatusFragment {
        public static CommunicationFailureFragment newInstance(CharSequence message) {
            CommunicationFailureFragment f = new CommunicationFailureFragment();
            f.setArguments(buildArgs(message, R.drawable.ic_openhab_appicon_24dp /* FIXME */,
                    R.string.try_again_button, false));
            return f;
        }

        @Override
        public void onClick(View view) {
            ((OpenHABMainActivity) getActivity()).retryServerPropertyQuery();
        }
    }

    public static class ProgressFragment extends StatusFragment {
        public static ProgressFragment newInstance(CharSequence message, boolean showImage) {
            ProgressFragment f = new ProgressFragment();
            f.setArguments(buildArgs(message, showImage ? R.drawable.ic_openhab_appicon_24dp : 0, 0, true));
            return f;
        }

        @Override
        public void onClick(View view) {
            // no-op, we don't show the button
        }
    }

    public static class NoNetworkFragment extends StatusFragment {
        public static NoNetworkFragment newInstance(CharSequence message) {
            NoNetworkFragment f = new NoNetworkFragment();
            f.setArguments(buildArgs(message, R.drawable.ic_signal_cellular_off_black_24dp,
                    R.string.try_again_button, false));
            return f;
        }

        @Override
        public void onClick(View view) {
            ConnectionFactory.restartNetworkCheck();
            getActivity().recreate();
        }
    }

    public static class MissingConfigurationFragment extends StatusFragment {
        public static MissingConfigurationFragment newInstance(Context context) {
            MissingConfigurationFragment f = new MissingConfigurationFragment();
            f.setArguments(buildArgs(context.getString(R.string.configuration_missing),
                    R.drawable.ic_openhab_appicon_24dp, /* FIXME? */
                    R.string.go_to_settings_button, false));
            return f;
        }

        @Override
        public void onClick(View view) {
            Intent preferencesIntent = new Intent(getActivity(), OpenHABPreferencesActivity.class);
            TaskStackBuilder.create(getActivity()).addNextIntentWithParentStack(preferencesIntent)
                    .startActivities();
        }
    }

    private abstract static class StatusFragment extends Fragment implements View.OnClickListener {
        protected static Bundle buildArgs(CharSequence message, @DrawableRes int drawableResId,
                @StringRes int buttonTextResId, boolean showProgress) {
            Bundle args = new Bundle();
            args.putCharSequence("message", message);
            args.putInt("drawable", drawableResId);
            args.putInt("buttontext", buttonTextResId);
            args.putBoolean("progress", showProgress);
            return args;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            Bundle arguments = getArguments();

            View view = inflater.inflate(R.layout.fragment_status, container, false);

            TextView descriptionText = view.findViewById(R.id.description);
            CharSequence message = arguments.getCharSequence("message");
            if (!TextUtils.isEmpty(message)) {
                descriptionText.setText(message);
            } else {
                descriptionText.setVisibility(View.GONE);
            }

            view.findViewById(R.id.progress)
                    .setVisibility(arguments.getBoolean("progress") ? View.VISIBLE : View.GONE);

            final ImageView watermark = view.findViewById(R.id.image);
            @DrawableRes
            int drawableResId = arguments.getInt("drawable");
            if (drawableResId != 0) {
                Drawable drawable = ContextCompat.getDrawable(getActivity(), drawableResId);
                drawable.setColorFilter(ContextCompat.getColor(getActivity(), R.color.empty_list_text_color),
                        PorterDuff.Mode.SRC_IN);
                watermark.setImageDrawable(drawable);
            } else {
                watermark.setVisibility(View.GONE);
            }

            final Button button = view.findViewById(R.id.button);
            int buttonTextResId = arguments.getInt("buttontext");
            if (buttonTextResId != 0) {
                button.setText(buttonTextResId);
                button.setOnClickListener(this);
            } else {
                button.setVisibility(View.GONE);
            }

            return view;
        }
    }
}