it.scoppelletti.mobilepower.app.FragmentLayoutController.java Source code

Java tutorial

Introduction

Here is the source code for it.scoppelletti.mobilepower.app.FragmentLayoutController.java

Source

/*
 * Copyright (C) 2013 Dario Scoppelletti, <http://www.scoppelletti.it/>.
 * 
 * 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 it.scoppelletti.mobilepower.app;

import java.util.*;
import android.app.*;
import android.os.*;
import android.support.v4.app.*;
import org.apache.commons.lang3.*;
import org.slf4j.*;
import it.scoppelletti.mobilepower.os.*;

/**
 * Controllore dei frammenti di dettaglio.
 * 
 * <P>Se cambia la configurazione del dispositivo, ad esempio
 * l&rsquo;orientamento, e quindi il layout, Android si limita a visualizzare i
 * soli frammenti inseriti in pannelli ancora esistenti nel nuovo layout mentre
 * gli altri frammenti rimangono invisibili; anche il back stack resta
 * invariato, quindi la pressione del bottone back potrebbe, ad esempio, 
 * rimuovere uno dei frammenti invisibili senza alcuna evidenza dal punto di
 * vista dell&rsquo;interfaccia utente.<BR>
 * La classe {@code FragmentLayoutController}, al ripristino dell&rsquo;istanza
 * dell&rsquo;attivit&agrave;, annulla le transazioni nel back stack fino alla
 * configurazione dei frammenti dell&rsquo;avvio dell&rsquo;applicazione e poi
 * reinserisce le stesse transazioni nel back stack riproducendo le
 * configurazioni dei frammenti come se le transazioni fossero state
 * originariamente eseguite con la configurazione del dispositivo corrente.<BR>
 * La classe {@code FragmentLayoutController} assume che l&rsquo;applicazione
 * implementi le transazioni sui frammenti con una logica di pannelli nel layout
 * organizzati in una gerarchia di livelli di dettaglio nei quali ogni frammento
 * &egrave; inserito nei pannelli disponibili con logica 
 * <ACRONYM TITLE="First In-First Out">FIFO</ACRONYM>.</P>
 * 
 * @since 1.0
 */
public final class FragmentLayoutController {

    /**
     * Stato 
     * {@code it.scoppelletti.mobilepower.app.FragmentLayoutController.panelCount}:
     * Numero di pannelli a disposizione per i frammenti di dettaglio.
     */
    public static final String STATE_PANELCOUNT = "it.scoppelletti.mobilepower.app.FragmentLayoutController.panelCount";

    private static final Logger myLogger = LoggerFactory.getLogger("FragmentLayout");
    private final Activity myActivity;
    private final int myFrameCount;
    private final int[] myFrameIds;

    /**
     * Costruttore.
     * 
     * <P>L&rsquo;attivit&agrave; istanzia un oggetto
     * {@code FragmentLayoutController} all&rsquo;interno del proprio metodo 
     * {@code onCreate} dopo aver costruito la propria vista.</P>
     * 
     * @param activity Attivit&agrave;. 
     * @param frameIds Identificatori dei pannelli a disposizione per i 
     *                 frammenti di dettaglio.
     */
    public FragmentLayoutController(Activity activity, int... frameIds) {
        int frameCount;

        if (activity == null) {
            throw new NullPointerException("Argument activity is null.");
        }
        if (ArrayUtils.isEmpty(frameIds)) {
            throw new NullPointerException("Argument frameIds is null.");
        }

        myActivity = activity;
        frameCount = 0;
        myFrameIds = Arrays.copyOf(frameIds, frameIds.length);
        for (int frameId : myFrameIds) {
            if (myActivity.findViewById(frameId) != null) {
                frameCount++;
            }
        }

        if (frameCount == 0) {
            throw new IllegalStateException("No panel found.");
        }

        myFrameCount = frameCount;
    }

    /**
     * Restituisce il numero di pannelli a disposizione per i frammenti di
     * dettaglio.
     * 
     * @return Valore.
     */
    public int getFrameCount() {
        return myFrameCount;
    }

    /**
     * Salva lo stato dell&rsquo;istanza.
     * 
     * <P>L&rsquo;attivit&agrave; salva lo stato dell&rsquo;istanza
     * {@code FragmentLayoutController} all&rsquo;interno del proprio metodo
     * {@code onSaveInstanceState}.</P>
     * 
     * @param outState Stato da salvare.
     */
    public void onSaveInstanceState(Bundle outState) {
        if (outState == null) {
            throw new NullPointerException("Argument outState is null.");
        }

        outState.putInt(FragmentLayoutController.STATE_PANELCOUNT, myFrameCount);
    }

    /**
     * Ripristina lo stato dell&rsquo;istanza.
     * 
     * <P>L&rsquo;attivit&agrave; ripristina lo stato dell&rsquo;istanza
     * {@code FragmentLayoutController} all&rsquo;interno del proprio metodo
     * {@code onRestoreInstanceState}.</P>
     * 
     * @param savedInstanceState Stato dell&rsquo;istanza.
     * @param fragmentCollector  Collettore dei frammenti di dettaglio. 
     */
    public void onRestoreInstanceState(Bundle savedInstanceState,
            FragmentLayoutController.FragmentCollector fragmentCollector) {
        int n, oldPanelCount, tnId;
        String tag;
        ActivitySupport activitySupport;
        FragmentSupport fragment;
        FragmentManager fragmentMgr;
        FragmentLayoutController.BackStackChangedListener backStackListener;
        Queue<FragmentSupport> fragmentQueue;
        Queue<FragmentLayoutController.FragmentEntry> clonedQueue;

        if (savedInstanceState == null) {
            throw new NullPointerException("Argument savedInstanceState is null.");
        }
        if (fragmentCollector == null) {
            throw new NullPointerException("Argument fragmentCollector is null.");
        }

        if (!(myActivity instanceof ActivitySupport)) {
            myLogger.warn("Activity not implement interface ActivitySupport.");
            return;
        }

        oldPanelCount = savedInstanceState.getInt(FragmentLayoutController.STATE_PANELCOUNT, 0);
        if (oldPanelCount < 1) {
            myLogger.warn("Unexpected {}={} in saved instance state.", FragmentLayoutController.STATE_PANELCOUNT,
                    oldPanelCount);
            return;
        }

        myLogger.debug("{}: current={}, saved instance state={}.",
                new Object[] { FragmentLayoutController.STATE_PANELCOUNT, myFrameCount, oldPanelCount });
        if (oldPanelCount == myFrameCount) {
            // Il numero di pannelli non e' cambiato:
            // Il sistema ha gia' ripristinato correttamente i frammenti.
            return;
        }

        fragmentQueue = new ArrayDeque<FragmentSupport>();
        fragmentCollector.collectFragments(fragmentQueue);

        // Ad ogni frammento associo il tag con il quale &egrave; stato
        // inserito
        clonedQueue = new ArrayDeque<FragmentLayoutController.FragmentEntry>();
        while (!fragmentQueue.isEmpty()) {
            fragment = fragmentQueue.remove();
            if (fragment == null) {
                myLogger.warn("Ignoring null.");
                continue;
            }

            tag = fragment.asFragment().getTag();
            if (StringUtils.isBlank(tag)) {
                myLogger.warn("Ignoring fragment with empty tag.");
                continue;
            }

            clonedQueue.offer(new FragmentLayoutController.FragmentEntry(fragment.cloneFragment(), tag));
        }

        fragmentQueue = null; // free memory

        activitySupport = (ActivitySupport) myActivity;
        fragmentMgr = activitySupport.getSupportFragmentManager();

        // Ripristino la configurazione dei frammenti iniziale
        for (n = fragmentMgr.getBackStackEntryCount(); n > 0; n--) {
            fragmentMgr.popBackStack();
        }

        if (myFrameCount > 1) {
            tnId = arrangeFragments(fragmentMgr, clonedQueue);
        } else {
            tnId = arrangePanel(fragmentMgr, clonedQueue);
        }

        if (Build.VERSION.SDK_INT < BuildCompat.VERSION_CODES.HONEYCOMB) {
            return;
        }

        // - Android 4.1.2
        // La barra delle azioni non e' correttamente aggiornata forse perche'
        // si assume che non ce ne sia bisogno con transazioni schedulate
        // durante il ripristino dell'attivita' (o magari perche' non e' proprio
        // previsto che si schedulino transazioni durante il ripristino
        // dell'attivita'):
        // Visto che l'esecuzione delle transazioni e' asincrona, devo
        // utilizzare un gestore degli eventi di modifica del back stack che
        // gestisca l&rsquo;ultima transazione che ho schedulato.
        backStackListener = new FragmentLayoutController.BackStackChangedListener(myActivity, fragmentMgr, tnId);
        fragmentMgr.addOnBackStackChangedListener(backStackListener);
    }

    /**
     * Ricostruisce la successione dei frammenti nell&rsquo;unico pannello.
     * 
     * @param  fragmentMgr   Gestore dei frammenti.
     * @param  fragmentQueue Frammenti.
     * @return               Identificatore dell&rsquo;ultimo elemento inserito
     *                       nel back stack. 
     */
    private int arrangePanel(FragmentManager fragmentMgr,
            Queue<FragmentLayoutController.FragmentEntry> fragmentQueue) {
        int tnId, lastTnId;
        FragmentLayoutController.FragmentEntry entry;
        FragmentTransaction fragmentTn = null;

        lastTnId = -1;
        while (!fragmentQueue.isEmpty()) {
            tnId = -1;
            entry = fragmentQueue.remove();

            try {
                fragmentTn = fragmentMgr.beginTransaction();

                fragmentTn.replace(myFrameIds[0], entry.getFragment().asFragment(), entry.getTag());

                fragmentTn.addToBackStack(null);
            } finally {
                if (fragmentTn != null) {
                    tnId = fragmentTn.commit();
                    fragmentTn = null;
                }
            }

            if (tnId >= 0) {
                lastTnId = tnId;
            }
        }

        return lastTnId;
    }

    /**
     * Ricostruisce la successione della disposizione dei frammenti nei
     * pannelli.
     * 
     * @param  fragmentMgr   Gestore dei frammenti.
     * @param  fragmentQueue Frammenti.
     * @return               Identificatore dell&rsquo;ultimo elemento inserito
     *                       nel back stack.
     */
    private int arrangeFragments(FragmentManager fragmentMgr,
            Queue<FragmentLayoutController.FragmentEntry> fragmentQueue) {
        int i;
        int frameCount, tnId, lastTnId;
        FragmentLayoutController.FragmentEntry entry;
        FragmentSupport newFragment, oldFragment;
        FragmentLayoutController.FragmentEntry[] frames;
        FragmentTransaction fragmentTn = null;

        frameCount = 1;
        frames = new FragmentLayoutController.FragmentEntry[myFrameCount];
        Arrays.fill(frames, null);

        lastTnId = -1;
        while (!fragmentQueue.isEmpty()) {
            tnId = -1;
            entry = fragmentQueue.remove();

            try {
                fragmentTn = fragmentMgr.beginTransaction();

                if (frameCount == myFrameCount) {
                    // Tutti i pannelli sono occupati:
                    // Sposto ogni frammento nel pannello precedente per
                    // liberare l'ultimo.
                    for (i = 0; i < frameCount; i++) {
                        if (frames[i] == null) {
                            // Inizialmente il primo pannello risulta vuoto
                            // anche se in realta' e' occupato dal frammento
                            // principale (non di dettaglio).
                            continue;
                        }

                        oldFragment = frames[i].getFragment();
                        newFragment = (i > 0) ? oldFragment.cloneFragment() : null;
                        fragmentTn.remove(oldFragment.asFragment());
                        frames[i] = null;

                        if (newFragment != null) {
                            fragmentTn.replace(myFrameIds[i - 1], newFragment.asFragment(), entry.getTag());
                            frames[i - 1] = new FragmentLayoutController.FragmentEntry(newFragment, entry.getTag());
                        }
                    }

                    frameCount--;
                }

                fragmentTn.add(myFrameIds[frameCount], entry.getFragment().asFragment(), entry.getTag());
                frames[frameCount++] = entry;

                fragmentTn.addToBackStack(null);
            } finally {
                if (fragmentTn != null) {
                    tnId = fragmentTn.commit();
                    fragmentTn = null;
                }
            }

            if (tnId >= 0) {
                lastTnId = tnId;
            }
        }

        return lastTnId;
    }

    /**
     * Collettore di frammenti di dettaglio.
     * 
     * @since 1.0.0
     */
    public interface FragmentCollector {

        /**
         * Accoda i frammenti di dettaglio da ridisporre nei pannelli a 
         * disposizione.
         * 
         * @param queue Coda.
         */
        void collectFragments(Queue<FragmentSupport> queue);
    }

    /**
     * Frammento da reinserire.
     */
    private static final class FragmentEntry {
        private final FragmentSupport myFragment;
        private final String myTag;

        /**
         * Costruttore.
         * 
         * @param fragment Frammento.
         * @param tag      Tag.
         */
        FragmentEntry(FragmentSupport fragment, String tag) {
            myFragment = fragment;
            myTag = tag;
        }

        /**
         * Restituisce il frammento.
         * 
         * @return Oggetto.
         */
        FragmentSupport getFragment() {
            return myFragment;
        }

        /**
         * Restituisce il tag.
         * 
         * @return Valore.
         */
        String getTag() {
            return myTag;
        }
    }

    /**
     * Gestore della modifica del back-stack.
     */
    private static final class BackStackChangedListener
            implements FragmentManager.OnBackStackChangedListener, Runnable {
        private static final Logger myLogger = LoggerFactory.getLogger("FragmentLayout");
        private final Activity myActivity;
        private final FragmentManager myFragmentMgr;
        private final int myWaitingForEntryId;
        private final Handler myHandler;

        /**
         * Costruttore.
         * 
         * @param activity          Attivit&agrave;.
         * @param fragmentMgr       Gestore dei frammenti.
         * @param waitingForEntryId Identificatore dell&rsquo;elemento atteso.
         */
        BackStackChangedListener(Activity activity, FragmentManager fragmentMgr, int waitingForEntryId) {
            myActivity = activity;
            myFragmentMgr = fragmentMgr;
            myWaitingForEntryId = waitingForEntryId;
            myHandler = new Handler();
            myLogger.trace("Waiting for back stack entry {}.", myWaitingForEntryId);
        }

        /**
         * Gestisce la modifica del back-stack.
         */
        public void onBackStackChanged() {
            int n, tnId;
            FragmentManager.BackStackEntry entry;

            n = myFragmentMgr.getBackStackEntryCount();
            if (n == 0) {
                myLogger.trace("Back stack is empty.");
                return;
            }

            entry = myFragmentMgr.getBackStackEntryAt(n - 1);
            tnId = entry.getId();
            myLogger.trace("Back stack entry {} intercepted.", tnId);
            if (tnId == myWaitingForEntryId) {
                myFragmentMgr.removeOnBackStackChangedListener(this);
                myHandler.post(this);
            }
        }

        /**
         * Esegue l&rsquo;operazione.
         */
        public void run() {
            ActivityCompat.invalidateOptionsMenu(myActivity);
        }
    }
}