gecko-dev/mobile/android/base/home/RemoteTabsPanel.java

244 lines
10 KiB
Java

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import org.mozilla.gecko.GeckoScreenOrientation;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.AccountLoader;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.SyncConstants;
import org.mozilla.gecko.util.HardwareUtils;
import android.accounts.Account;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.support.v4.util.Pair;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A <code>HomeFragment</code> that, depending on the state of accounts on the
* device:
* <ul>
* <li>displays remote tabs from other devices;</li>
* <li>offers to re-connect a Firefox Account;</li>
* <li>offers to create a new Firefox Account.</li>
* </ul>
*/
public class RemoteTabsPanel extends HomeFragment {
private static final String LOGTAG = "GeckoRemoteTabsPanel";
// Loader ID for Android Account loader.
private static final int LOADER_ID_ACCOUNT = 0;
private static final String FRAGMENT_ACTION = "FRAGMENT_ACTION";
private static final String FRAGMENT_ORIENTATION = "FRAGMENT_ORIENTATION";
private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
private static final String NO_ACCOUNT = "NO_ACCOUNT";
// Callback for loaders.
private AccountLoaderCallbacks mAccountLoaderCallbacks;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.home_remote_tabs_panel, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Create callbacks before the initial loader is started.
mAccountLoaderCallbacks = new AccountLoaderCallbacks();
loadIfVisible();
}
@Override
protected void loadIfVisible() {
// Force reload fragment only in tablets when there is valid account and the orientation has changed.
Pair<String, Integer> actionOrientationPair;
if (canLoad() && HardwareUtils.isTablet() && (actionOrientationPair = getActionAndOrientationForFragmentInBackStack()) != null) {
if (actionOrientationPair.first.equals(Action.None.name()) && actionOrientationPair.second != GeckoScreenOrientation.getInstance().getAndroidOrientation()) {
// As the fragment becomes visible only after onStart callback, we can safely remove it from the back-stack.
// If a portrait fragment is in the back-stack and then a landscape fragment should be shown, there can
// be a brief flash as the fragment as replaced.
getChildFragmentManager()
.beginTransaction()
.addToBackStack(null)
.remove(getChildFragmentManager().findFragmentByTag(FRAGMENT_TAG))
.commitAllowingStateLoss();
getChildFragmentManager().executePendingTransactions();
load();
return;
}
}
super.loadIfVisible();
}
@Override
public void load() {
getLoaderManager().initLoader(LOADER_ID_ACCOUNT, null, mAccountLoaderCallbacks);
}
private void showSubPanel(Account account) {
final Action actionNeeded = getActionNeeded(account);
final String actionString = actionNeeded != null ? actionNeeded.name() : NO_ACCOUNT;
final int orientation = HardwareUtils.isTablet() ? GeckoScreenOrientation.getInstance().getAndroidOrientation()
: Configuration.ORIENTATION_UNDEFINED;
// Check if fragment for given action and orientation is in the back-stack.
final Pair<String, Integer> actionOrientationPair = getActionAndOrientationForFragmentInBackStack();
if (actionOrientationPair != null && actionOrientationPair.first.equals(actionString) && (actionOrientationPair.second == orientation)) {
return;
}
// Instantiate the fragment for the action and update the arguments.
Fragment subPanel = makeFragmentForAction(actionNeeded);
final Bundle args = new Bundle();
args.putBoolean(HomePager.CAN_LOAD_ARG, getCanLoadHint());
args.putString(FRAGMENT_ACTION, actionString);
args.putInt(FRAGMENT_ORIENTATION, orientation);
subPanel.setArguments(args);
// Add the fragment to the back-stack.
getChildFragmentManager()
.beginTransaction()
.addToBackStack(null)
.replace(R.id.remote_tabs_container, subPanel, FRAGMENT_TAG)
.commitAllowingStateLoss();
}
private Pair<String, Integer> getActionAndOrientationForFragmentInBackStack() {
final Fragment currentFragment = getChildFragmentManager().findFragmentByTag(FRAGMENT_TAG);
if (currentFragment != null && currentFragment.getArguments() != null) {
final String fragmentAction = currentFragment.getArguments().getString(FRAGMENT_ACTION);
final int fragmentOrientation = currentFragment.getArguments().getInt(FRAGMENT_ORIENTATION);
return Pair.create(fragmentAction, fragmentOrientation);
}
return null;
}
/**
* Get whatever <code>Action</code> is required to continue healthy syncing
* of Remote Tabs.
* <p>
* A Firefox Account can be in many states, from healthy to requiring a
* Fennec upgrade to continue use. If we have a Firefox Account, but the
* state seems corrupt, the best we can do is ask for a password, which
* resets most of the Account state. The health of a Sync account is
* essentially opaque in this respect.
* <p>
* A null Account means there is no Account (Sync or Firefox) on the device.
*
* @param account
* Android Account (Sync or Firefox); may be null.
*/
private Action getActionNeeded(Account account) {
if (account == null) {
return null;
}
if (SyncConstants.ACCOUNTTYPE_SYNC.equals(account.type)) {
return Action.None;
}
if (!FxAccountConstants.ACCOUNT_TYPE.equals(account.type)) {
Log.wtf(LOGTAG, "Non Sync, non Firefox Android Account returned by AccountLoader; returning null.");
return null;
}
final State state = FirefoxAccounts.getFirefoxAccountState(getActivity());
if (state == null) {
Log.wtf(LOGTAG, "Firefox Account with null state found; offering needs password.");
return Action.NeedsPassword;
}
final Action actionNeeded = state.getNeededAction();
if (actionNeeded == null) {
Log.wtf(LOGTAG, "Firefox Account with non-null state but null action needed; offering needs password.");
return Action.NeedsPassword;
}
return actionNeeded;
}
private Fragment makeFragmentForAction(Action action) {
if (action == null) {
// This corresponds to no Account: neither Sync nor Firefox.
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_setup);
}
switch (action) {
case None:
if (HardwareUtils.isTablet() && GeckoScreenOrientation.getInstance().getAndroidOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
return new RemoteTabsSplitPlaneFragment();
} else {
return new RemoteTabsExpandableListFragment();
}
case NeedsVerification:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_verification);
case NeedsPassword:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_password);
case NeedsUpgrade:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_upgrade);
case NeedsFinishMigrating:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_finish_migrating);
default:
// This should never happen, but we're confident we have a Firefox
// Account at this point, so let's show the needs password screen.
// That's our best hope of righting the ship.
Log.wtf(LOGTAG, "Got unexpected action needed; offering needs password.");
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_password);
}
}
/**
* Update the UI to reflect the given <code>Account</code> and its state.
* <p>
* A null Account means there is no Account (Sync or Firefox) on the device.
*
* @param account
* Android Account (Sync or Firefox); may be null.
*/
protected void updateUiFromAccount(Account account) {
if (getView() == null) {
// Early abort. When the fragment is detached, we get a loader
// reset, which calls this with a null account parameter. A null
// account is valid (it means there is no account, either Sync or
// Firefox), and so we start to offer the setup flow. But this all
// happens after the view has been destroyed, which means inserting
// the setup flow fails. In this case, just abort.
return;
}
showSubPanel(account);
}
private class AccountLoaderCallbacks implements LoaderCallbacks<Account> {
@Override
public Loader<Account> onCreateLoader(int id, Bundle args) {
return new AccountLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Account> loader, Account account) {
updateUiFromAccount(account);
}
@Override
public void onLoaderReset(Loader<Account> loader) {
updateUiFromAccount(null);
}
}
}