package com.battlelancer.seriesguide.backend; import android.accounts.Account; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import butterknife.ButterKnife; import com.battlelancer.seriesguide.R; import com.battlelancer.seriesguide.SgApp; import com.battlelancer.seriesguide.backend.settings.HexagonSettings; import com.battlelancer.seriesguide.settings.TraktCredentials; import com.battlelancer.seriesguide.sync.SgSyncAdapter; import com.battlelancer.seriesguide.ui.dialogs.RemoveCloudAccountDialogFragment; import com.battlelancer.seriesguide.util.Utils; import com.google.android.gms.auth.api.Auth; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInResult; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.OptionalPendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import timber.log.Timber; /** * Helps connecting a device to Hexagon: sign in via Google account, initial uploading of shows. */ public class CloudSetupFragment extends Fragment { private static final int REQUEST_SIGN_IN = 1; private static final String ACTION_SIGN_IN = "sign-in"; private Button buttonAction; private TextView textViewDescription; private TextView textViewUsername; private ProgressBar progressBar; private Button buttonRemoveAccount; private TextView textViewWarning; private Snackbar snackbar; private GoogleApiClient googleApiClient; @Nullable private GoogleSignInAccount signInAccount; private HexagonTools hexagonTools; private HexagonSetupTask hexagonSetupTask; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /* * Try to keep the fragment around on config changes so the setup task * does not have to be finished. */ setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_cloud_setup, container, false); textViewDescription = (TextView) v.findViewById(R.id.textViewCloudDescription); textViewUsername = ButterKnife.findById(v, R.id.textViewCloudUsername); textViewWarning = ButterKnife.findById(v, R.id.textViewCloudWarnings); progressBar = (ProgressBar) v.findViewById(R.id.progressBarCloud); buttonAction = (Button) v.findViewById(R.id.buttonCloudAction); buttonRemoveAccount = ButterKnife.findById(v, R.id.buttonCloudRemoveAccount); buttonRemoveAccount.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setProgressVisible(true); DialogFragment f = new RemoveCloudAccountDialogFragment(); f.show(getFragmentManager(), "remove-cloud-account"); } }); updateViews(); setProgressVisible(true); return v; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); hexagonTools = SgApp.from(getActivity()).getHexagonTools(); googleApiClient = new GoogleApiClient.Builder(getContext()) .enableAutoManage(getActivity(), onGoogleConnectionFailedListener) .addApi(Auth.GOOGLE_SIGN_IN_API, HexagonTools.getGoogleSignInOptions()) .build(); } @Override public void onStart() { super.onStart(); if (!isHexagonSetupRunning()) { // check if the user is still signed in OptionalPendingResult<GoogleSignInResult> pendingResult = Auth.GoogleSignInApi .silentSignIn(googleApiClient); if (pendingResult.isDone()) { // If the user's cached credentials are valid, the OptionalPendingResult will be "done" // and the GoogleSignInResult will be available instantly. Timber.d("Got cached sign-in"); handleSignInResult(pendingResult.get()); } else { // If the user has not previously signed in on this device or the sign-in has expired, // this asynchronous branch will attempt to sign in the user silently. Cross-device // single sign-on will occur in this branch. Timber.d("Trying async sign-in"); pendingResult.setResultCallback(new ResultCallback<GoogleSignInResult>() { @Override public void onResult(@NonNull GoogleSignInResult googleSignInResult) { handleSignInResult(googleSignInResult); } }); } } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_SIGN_IN) { GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data); handleSignInResult(result); } } @Override public void onResume() { super.onResume(); EventBus.getDefault().register(this); } @Override public void onPause() { super.onPause(); EventBus.getDefault().unregister(this); } @Override public void onDestroy() { super.onDestroy(); if (isHexagonSetupRunning()) { hexagonSetupTask.cancel(true); } hexagonSetupTask = null; } @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(RemoveCloudAccountDialogFragment.CanceledEvent event) { setProgressVisible(false); } @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(RemoveCloudAccountDialogFragment.AccountRemovedEvent event) { event.handle(getActivity()); setProgressVisible(false); updateViews(); } /** * On sign-in success, saves the signed in Google account and auto-starts setup if Cloud is not * enabled, yet. On sign-in failure disables Cloud. */ private void handleSignInResult(GoogleSignInResult result) { boolean signedIn = result.isSuccess(); if (signedIn) { Timber.i("Signed in with Google."); signInAccount = result.getSignInAccount(); } else { // not or no longer signed in hexagonTools.trackSignInFailure(ACTION_SIGN_IN, result.getStatus()); signInAccount = null; hexagonTools.setDisabled(); } setProgressVisible(false); updateViews(); if (signedIn && Utils.hasAccessToX(getContext()) && !HexagonSettings.isEnabled(getContext())) { // auto-start setup if sign in succeeded and Cloud can be, but is not enabled, yet Timber.i("Auto-start Cloud setup."); startHexagonSetup(); } } private void signIn() { Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient); startActivityForResult(signInIntent, REQUEST_SIGN_IN); } private void signOut() { setProgressVisible(true); Auth.GoogleSignInApi.signOut(googleApiClient).setResultCallback( new ResultCallback<Status>() { @Override public void onResult(@NonNull Status status) { setProgressVisible(false); if (status.isSuccess()) { Timber.i("Signed out of Google."); signInAccount = null; hexagonTools.setDisabled(); updateViews(); } else { hexagonTools.trackSignInFailure("sign-out", status); } } }); } private void updateViews() { // warn about changes in behavior with trakt textViewWarning.setVisibility(TraktCredentials.get(getActivity()).hasCredentials() ? View.VISIBLE : View.GONE); // hexagon enabled and account looks fine? if (HexagonSettings.isEnabled(getContext()) && !HexagonSettings.shouldValidateAccount(getContext())) { textViewUsername.setText(HexagonSettings.getAccountName(getActivity())); textViewUsername.setVisibility(View.VISIBLE); textViewDescription.setText(R.string.hexagon_signed_in); // enable sign-out buttonAction.setText(R.string.hexagon_signout); buttonAction.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { signOut(); } }); // enable account removal buttonRemoveAccount.setVisibility(View.VISIBLE); } else { // did try to setup, but failed? if (!HexagonSettings.hasCompletedSetup(getActivity())) { // show error message textViewDescription.setText(R.string.hexagon_setup_incomplete); } else { textViewDescription.setText(R.string.hexagon_description); } textViewUsername.setVisibility(View.GONE); // enable sign-in buttonAction.setText(R.string.hexagon_signin); buttonAction.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // restrict access to supporters if (Utils.hasAccessToX(getActivity())) { startHexagonSetup(); } else { Utils.advertiseSubscription(getActivity()); } } }); // disable account removal buttonRemoveAccount.setVisibility(View.GONE); } } /** * Disables buttons and shows a progress bar. */ private void setProgressVisible(boolean isVisible) { progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); buttonAction.setEnabled(!isVisible); buttonRemoveAccount.setEnabled(!isVisible); } /** * Disables all buttons (use if signing in with Google seems not possible). */ private void setDisabled() { buttonAction.setEnabled(false); buttonRemoveAccount.setEnabled(false); } private void startHexagonSetup() { setProgressVisible(true); if (signInAccount == null) { signIn(); } else if (!isHexagonSetupRunning()) { HexagonSettings.setSetupIncomplete(getContext()); hexagonSetupTask = new HexagonSetupTask(hexagonTools, signInAccount, onHexagonSetupFinishedListener); hexagonSetupTask.execute(); } } private boolean isHexagonSetupRunning() { return hexagonSetupTask != null && hexagonSetupTask.getStatus() != AsyncTask.Status.FINISHED; } private static class HexagonSetupTask extends AsyncTask<String, Void, Integer> { public static final int SUCCESS_SYNC_REQUIRED = 1; public static final int FAILURE = -1; public static final int FAILURE_AUTH = -2; public interface OnSetupFinishedListener { void onSetupFinished(int resultCode); } private final HexagonTools hexagonTools; @NonNull private final GoogleSignInAccount signInAccount; private OnSetupFinishedListener onSetupFinishedListener; /** * Checks for local and remote shows and uploads shows accordingly. If there are some shows * in the local database as well as on hexagon, will download and merge data first, then * upload. */ public HexagonSetupTask(HexagonTools hexagonTools, @NonNull GoogleSignInAccount signInAccount, OnSetupFinishedListener listener) { this.hexagonTools = hexagonTools; this.signInAccount = signInAccount; onSetupFinishedListener = listener; } @Override protected Integer doInBackground(String... params) { // set setup incomplete flag Timber.i("Setting up Hexagon..."); // validate account data Account account = signInAccount.getAccount(); if (TextUtils.isEmpty(signInAccount.getEmail()) || account == null) { return FAILURE_AUTH; } // at last reset sync state, store the new credentials and enable hexagon integration if (!hexagonTools.setEnabled(signInAccount)) { return FAILURE; } return SUCCESS_SYNC_REQUIRED; } @Override protected void onPostExecute(Integer result) { if (onSetupFinishedListener != null) { onSetupFinishedListener.onSetupFinished(result); } } } private HexagonSetupTask.OnSetupFinishedListener onHexagonSetupFinishedListener = new HexagonSetupTask.OnSetupFinishedListener() { @Override public void onSetupFinished(int resultCode) { switch (resultCode) { case HexagonSetupTask.SUCCESS_SYNC_REQUIRED: { // schedule full sync Timber.d("Setting up Hexagon...SUCCESS_SYNC_REQUIRED"); SgSyncAdapter.requestSyncImmediate(getActivity(), SgSyncAdapter.SyncType.FULL, 0, false); HexagonSettings.setSetupCompleted(getActivity()); break; } case HexagonSetupTask.FAILURE_AUTH: { // show setup incomplete message + error toast if (getView() != null) { Snackbar.make(getView(), R.string.hexagon_setup_fail_auth, Snackbar.LENGTH_LONG).show(); } Timber.d("Setting up Hexagon...FAILURE_AUTH"); break; } case HexagonSetupTask.FAILURE: { // show setup incomplete message Timber.d("Setting up Hexagon...FAILURE"); break; } } if (getView() == null) { return; } setProgressVisible(false); // allow new task updateViews(); } }; private GoogleApiClient.OnConnectionFailedListener onGoogleConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() { @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { // using auto managed connection so only called if unresolvable error hexagonTools.trackSignInFailure(ACTION_SIGN_IN, connectionResult); if (getView() == null) { return; } setProgressVisible(false); setDisabled(); if (snackbar != null) { snackbar.dismiss(); } snackbar = Snackbar.make(getView(), R.string.hexagon_google_play_missing, Snackbar.LENGTH_INDEFINITE); snackbar.show(); } }; }