/*
* Copyright 2014 Google Inc. All rights reserved.
*
* 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 com.google.samples.apps.iosched.ui;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncStatusObserver;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v4.app.NavUtils;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.samples.apps.iosched.BuildConfig;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.injection.LoginAndAuthProvider;
import com.google.samples.apps.iosched.injection.MessagingRegistrationProvider;
import com.google.samples.apps.iosched.login.LoginAndAuth;
import com.google.samples.apps.iosched.login.LoginAndAuthListener;
import com.google.samples.apps.iosched.login.LoginStateListener;
import com.google.samples.apps.iosched.messaging.MessagingRegistration;
import com.google.samples.apps.iosched.navigation.AppNavigationViewAsDrawerImpl;
import com.google.samples.apps.iosched.navigation.NavigationModel.NavigationItemEnum;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.service.DataBootstrapService;
import com.google.samples.apps.iosched.sync.SyncHelper;
import com.google.samples.apps.iosched.sync.account.Account;
import com.google.samples.apps.iosched.ui.widget.MultiSwipeRefreshLayout;
import com.google.samples.apps.iosched.util.AccountUtils;
import com.google.samples.apps.iosched.util.ImageLoader;
import com.google.samples.apps.iosched.util.LUtils;
import com.google.samples.apps.iosched.util.RecentTasksStyler;
import com.google.samples.apps.iosched.welcome.WelcomeActivity;
import static com.google.samples.apps.iosched.util.LogUtils.LOGD;
import static com.google.samples.apps.iosched.util.LogUtils.LOGE;
import static com.google.samples.apps.iosched.util.LogUtils.LOGW;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;
/**
* A base activity that handles common functionality in the app. This includes the navigation
* drawer, login and authentication, Action Bar tweaks, amongst others.
*/
public abstract class BaseActivity extends AppCompatActivity implements
LoginAndAuthListener,
SharedPreferences.OnSharedPreferenceChangeListener,
MultiSwipeRefreshLayout.CanChildScrollUpCallback, LoginStateListener,
AppNavigationViewAsDrawerImpl.NavigationDrawerStateListener {
private static final String TAG = makeLogTag(BaseActivity.class);
public static final int SWITCH_USER_RESULT = 9998;
private static final int SELECT_GOOGLE_ACCOUNT_RESULT = 9999;
// the LoginAndAuthHelper handles signing in to Google Play Services and OAuth
private LoginAndAuth mLoginAndAuthProvider;
// Navigation drawer
private AppNavigationViewAsDrawerImpl mAppNavigationViewAsDrawer;
// Toolbar
private Toolbar mToolbar;
// Helper methods for L APIs
private LUtils mLUtils;
private static final int MAIN_CONTENT_FADEIN_DURATION = 250;
// SwipeRefreshLayout allows the user to swipe the screen down to trigger a manual refresh
private SwipeRefreshLayout mSwipeRefreshLayout;
// Registration with GCM for notifications
private MessagingRegistration mMessagingRegistration;
// handle to our sync observer (that notifies us about changes in our sync state)
private Object mSyncObserverHandle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RecentTasksStyler.styleRecentTasksEntry(this);
// Check if the EULA has been accepted; if not, show it.
if (WelcomeActivity.shouldDisplay(this)) {
Intent intent = new Intent(this, WelcomeActivity.class);
startActivity(intent);
finish();
return;
}
mMessagingRegistration = MessagingRegistrationProvider.provideMessagingRegistration(this);
Account.createSyncAccount(this);
if (savedInstanceState == null) {
mMessagingRegistration.registerDevice();
}
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.registerOnSharedPreferenceChangeListener(this);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
mLUtils = LUtils.getInstance(this);
}
private void trySetupSwipeRefresh() {
mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setColorSchemeResources(
R.color.flat_button_text);
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
requestDataRefresh();
}
});
if (mSwipeRefreshLayout instanceof MultiSwipeRefreshLayout) {
MultiSwipeRefreshLayout mswrl = (MultiSwipeRefreshLayout) mSwipeRefreshLayout;
mswrl.setCanChildScrollUpCallback(this);
}
}
}
/**
* Returns the navigation drawer item that corresponds to this Activity. Subclasses of
* BaseActivity override this to indicate what nav drawer item corresponds to them Return
* NAVDRAWER_ITEM_INVALID to mean that this Activity should not have a Nav Drawer.
*/
protected NavigationItemEnum getSelfNavDrawerItem() {
return NavigationItemEnum.INVALID;
}
@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
getToolbar();
}
@Override
public void onNavDrawerStateChanged(boolean isOpen, boolean isAnimating) {
// Nothing to do
}
@Override
public void onNavDrawerSlide(float offset) {
}
@Override
public void onBackPressed() {
if (mAppNavigationViewAsDrawer.isNavDrawerOpen()) {
mAppNavigationViewAsDrawer.closeNavDrawer();
} else {
super.onBackPressed();
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key != null && key.equals(BuildConfig.PREF_ATTENDEE_AT_VENUE)) {
LOGD(TAG, "Attendee at venue preference changed, repopulating nav drawer and menu.");
if (mAppNavigationViewAsDrawer != null) {
mAppNavigationViewAsDrawer.updateNavigationItems();
}
invalidateOptionsMenu();
}
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
mAppNavigationViewAsDrawer = new AppNavigationViewAsDrawerImpl(new ImageLoader(this), this);
mAppNavigationViewAsDrawer.activityReady(this, this, getSelfNavDrawerItem());
if (getSelfNavDrawerItem() != NavigationItemEnum.INVALID) {
setToolbarForNavigation();
}
trySetupSwipeRefresh();
View mainContent = findViewById(R.id.main_content);
if (mainContent != null) {
mainContent.setAlpha(0);
mainContent.animate().alpha(1).setDuration(MAIN_CONTENT_FADEIN_DURATION);
} else {
LOGW(TAG, "No view with ID main_content to fade in.");
}
}
@Override
public void onAccountChangeRequested() {
// override if you want to be notified when another account has been selected account has
// changed
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.menu_refresh:
requestDataRefresh();
break;
}
return super.onOptionsItemSelected(item);
}
protected void requestDataRefresh() {
android.accounts.Account activeAccount = AccountUtils.getActiveAccount(this);
ContentResolver contentResolver = getContentResolver();
if (contentResolver.isSyncActive(activeAccount, ScheduleContract.CONTENT_AUTHORITY)) {
LOGD(TAG, "Ignoring manual sync request because a sync is already in progress.");
return;
}
LOGD(TAG, "Requesting manual data refresh.");
SyncHelper.requestManualSync();
}
/**
* This utility method handles Up navigation intents by searching for a parent activity and
* navigating there if defined. When using this for an activity make sure to define both the
* native parentActivity as well as the AppCompat one when supporting API levels less than 16.
* when the activity has a single parent activity. If the activity doesn't have a single parent
* activity then don't define one and this method will use back button functionality. If "Up"
* functionality is still desired for activities without parents then use {@code
* syntheticParentActivity} to define one dynamically.
* <p/>
* Note: Up navigation intents are represented by a back arrow in the top left of the Toolbar in
* Material Design guidelines.
*
* @param currentActivity Activity in use when navigate Up action occurred.
* @param syntheticParentActivity Parent activity to use when one is not already configured.
*/
public static void navigateUpOrBack(Activity currentActivity,
Class<? extends Activity> syntheticParentActivity) {
// Retrieve parent activity from AndroidManifest.
Intent intent = NavUtils.getParentActivityIntent(currentActivity);
// Synthesize the parent activity when a natural one doesn't exist.
if (intent == null && syntheticParentActivity != null) {
try {
intent = NavUtils.getParentActivityIntent(currentActivity, syntheticParentActivity);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
if (intent == null) {
// No parent defined in manifest. This indicates the activity may be used by
// in multiple flows throughout the app and doesn't have a strict parent. In
// this case the navigation up button should act in the same manner as the
// back button. This will result in users being forwarded back to other
// applications if currentActivity was invoked from another application.
currentActivity.onBackPressed();
} else {
if (NavUtils.shouldUpRecreateTask(currentActivity, intent)) {
// Need to synthesize a backstack since currentActivity was probably invoked by a
// different app. The preserves the "Up" functionality within the app according to
// the activity hierarchy defined in AndroidManifest.xml via parentActivity
// attributes.
TaskStackBuilder builder = TaskStackBuilder.create(currentActivity);
builder.addNextIntentWithParentStack(intent);
builder.startActivities();
} else {
// Navigate normally to the manifest defined "Up" activity.
NavUtils.navigateUpTo(currentActivity, intent);
}
}
}
@Override
public void onSignInOrCreateAccount() {
//Get list of accounts on device.
android.accounts.AccountManager am = android.accounts.AccountManager.get(BaseActivity.this);
android.accounts.Account[] accountArray =
am.getAccountsByType(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
if (accountArray.length == 0) {
//Send the user to the "Add Account" page.
Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES,
new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE});
startActivity(intent);
} else {
//Try to log the user in with the first account on the device.
startLoginProcess();
mAppNavigationViewAsDrawer.closeNavDrawer();
}
}
@Override
protected void onResume() {
super.onResume();
// Perform one-time bootstrap setup, if needed
DataBootstrapService.startDataBootstrapIfNecessary(this);
// Check to ensure a Google Account is active for the app. Placing the check here ensures
// it is run again in the case where a Google Account wasn't present on the device and a
// picker had to be started.
if (!AccountUtils.enforceActiveGoogleAccount(this, SELECT_GOOGLE_ACCOUNT_RESULT)) {
LOGD(TAG, "EnforceActiveGoogleAccount returned false");
return;
}
// Watch for sync state changes
mSyncStatusObserver.onStatusChanged(0);
final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING |
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
mSyncObserverHandle = ContentResolver.addStatusChangeListener(mask, mSyncStatusObserver);
startLoginProcess();
}
@Override
protected void onPause() {
super.onPause();
if (mSyncObserverHandle != null) {
ContentResolver.removeStatusChangeListener(mSyncObserverHandle);
mSyncObserverHandle = null;
}
if (mLoginAndAuthProvider != null) {
mLoginAndAuthProvider.stop();
}
}
/**
* Converts an intent into a {@link Bundle} suitable for use as fragment arguments.
*/
public static Bundle intentToFragmentArguments(Intent intent) {
Bundle arguments = new Bundle();
if (intent == null) {
return arguments;
}
final Uri data = intent.getData();
if (data != null) {
arguments.putParcelable("_uri", data);
}
final Bundle extras = intent.getExtras();
if (extras != null) {
arguments.putAll(intent.getExtras());
}
return arguments;
}
@Override
public void onStartLoginProcessRequested() {
startLoginProcess();
}
private void startLoginProcess() {
LOGD(TAG, "Starting login process.");
if (!AccountUtils.hasActiveAccount(this)) {
LOGD(TAG, "No active account, attempting to pick a default.");
String defaultAccount = AccountUtils.getActiveAccountName(this);
if (defaultAccount == null) {
LOGE(TAG, "Failed to pick default account (no accounts). Failing.");
//complainMustHaveGoogleAccount();
return;
}
LOGD(TAG, "Default to: " + defaultAccount);
AccountUtils.setActiveAccount(this, defaultAccount);
}
if (!AccountUtils.hasActiveAccount(this)) {
LOGD(TAG, "Can't proceed with login -- no account chosen.");
return;
} else {
LOGD(TAG, "Chosen account: " + AccountUtils.getActiveAccountName(this));
}
String accountName = AccountUtils.getActiveAccountName(this);
LOGD(TAG, "Chosen account: " + AccountUtils.getActiveAccountName(this));
if (mLoginAndAuthProvider != null && mLoginAndAuthProvider.getAccountName()
.equals(accountName)) {
LOGD(TAG, "Helper already set up; simply starting it.");
mLoginAndAuthProvider.start();
return;
}
LOGD(TAG, "Starting login process with account " + accountName);
if (mLoginAndAuthProvider != null) {
LOGD(TAG, "Tearing down old Helper, was " + mLoginAndAuthProvider.getAccountName());
if (mLoginAndAuthProvider.isStarted()) {
LOGD(TAG, "Unregister device from GCM");
mMessagingRegistration.unregisterDevice(mLoginAndAuthProvider.getAccountName());
LOGD(TAG, "Stopping old Helper");
mLoginAndAuthProvider.stop();
}
mLoginAndAuthProvider = null;
}
LOGD(TAG, "Creating and starting new Helper with account: " + accountName);
mLoginAndAuthProvider =
LoginAndAuthProvider.provideLoginAndAuth(this, this, accountName);
mLoginAndAuthProvider.start();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SELECT_GOOGLE_ACCOUNT_RESULT) {
// Handle the select {@code startActivityForResult} from
// {@code enforceActiveGoogleAccount()} when a Google Account wasn't present on the
// device.
if (resultCode == RESULT_OK) {
// Set selected GoogleAccount as active.
String accountName =
data.getStringExtra(android.accounts.AccountManager.KEY_ACCOUNT_NAME);
AccountUtils.setActiveAccount(this, accountName);
onAuthSuccess(accountName, true);
} else {
LOGW(TAG, "A Google Account is required to use this application.");
// This application requires a Google Account to be selected.
finish();
}
return;
} else if (requestCode == SWITCH_USER_RESULT) {
// Handle account change notifications after {@link SwitchUserActivity} has been invoked
// (typically by {@link AppNavigationViewAsDrawerImpl}).
if (resultCode == RESULT_OK) {
onAccountChangeRequested();
onStartLoginProcessRequested();
}
}
if (mLoginAndAuthProvider == null || !mLoginAndAuthProvider.onActivityResult(requestCode,
resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onPlusInfoLoaded(String accountName) {
mAppNavigationViewAsDrawer.updateNavigationItems();
}
/**
* Called when authentication succeeds. This may either happen because the user just
* authenticated for the first time (and went through the sign in flow), or because it's a
* returning user.
*
* @param accountName name of the account that just authenticated successfully.
* @param newlyAuthenticated If true, this user just authenticated for the first time. If false,
* it's a returning user.
*/
@Override
public void onAuthSuccess(String accountName, boolean newlyAuthenticated) {
android.accounts.Account account =
new android.accounts.Account(accountName, GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
LOGD(TAG, "onAuthSuccess, account " + accountName + ", newlyAuthenticated="
+ newlyAuthenticated);
refreshAccountDependantData();
if (newlyAuthenticated) {
LOGD(TAG, "Enabling auto sync on content provider for account " + accountName);
SyncHelper.updateSyncInterval(this);
SyncHelper.requestManualSync();
}
mAppNavigationViewAsDrawer.updateNavigationItems();
mMessagingRegistration.registerDevice();
}
@Override
public void onAuthFailure(String accountName) {
LOGD(TAG, "Auth failed for account " + accountName);
refreshAccountDependantData();
mAppNavigationViewAsDrawer.updateNavigationItems();
}
protected void refreshAccountDependantData() {
// Force local data refresh for data that depends on the logged user:
LOGD(TAG, "Refreshing User Data");
getContentResolver().notifyChange(ScheduleContract.MySchedule.CONTENT_URI, null, false);
getContentResolver().notifyChange(ScheduleContract.MyViewedVideos.CONTENT_URI, null, false);
getContentResolver().notifyChange(
ScheduleContract.MyFeedbackSubmitted.CONTENT_URI, null, false);
}
protected void retryAuth() {
mLoginAndAuthProvider.retryAuthByUserRequest();
}
public Toolbar getToolbar() {
if (mToolbar == null) {
mToolbar = (Toolbar) findViewById(R.id.toolbar);
if (mToolbar != null) {
mToolbar.setNavigationContentDescription(getResources().getString(R.string
.navdrawer_description_a11y));
setSupportActionBar(mToolbar);
}
}
return mToolbar;
}
private void setToolbarForNavigation() {
if (mToolbar != null) {
mToolbar.setNavigationIcon(R.drawable.ic_hamburger);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mAppNavigationViewAsDrawer.showNavigation();
}
});
}
}
/**
* @param clickListener The {@link android.view.View.OnClickListener} for the navigation icon of
* the toolbar.
*/
protected void setToolbarAsUp(View.OnClickListener clickListener) {
// Initialise the toolbar
getToolbar();
mToolbar.setNavigationIcon(R.drawable.ic_up);
mToolbar.setNavigationContentDescription(R.string.close_and_go_back);
mToolbar.setNavigationOnClickListener(clickListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mMessagingRegistration != null) {
mMessagingRegistration.destroy();
}
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.unregisterOnSharedPreferenceChangeListener(this);
}
private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
@Override
public void onStatusChanged(int which) {
runOnUiThread(new Runnable() {
@Override
public void run() {
String accountName = AccountUtils.getActiveAccountName(BaseActivity.this);
if (TextUtils.isEmpty(accountName)) {
onRefreshingStateChanged(false);
return;
}
android.accounts.Account account = new android.accounts.Account(
accountName, GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
boolean syncActive = ContentResolver.isSyncActive(
account, ScheduleContract.CONTENT_AUTHORITY);
onRefreshingStateChanged(syncActive);
}
});
}
};
protected void onRefreshingStateChanged(boolean refreshing) {
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setRefreshing(refreshing);
}
}
public LUtils getLUtils() {
return mLUtils;
}
@Override
public boolean canSwipeRefreshChildScrollUp() {
return false;
}
/**
* Configure this Activity as a floating window, with the given {@code width}, {@code height}
* and {@code alpha}, and dimming the background with the given {@code dim} value.
*/
protected void setupFloatingWindow(int width, int height, int alpha, float dim) {
WindowManager.LayoutParams params = getWindow().getAttributes();
params.width = getResources().getDimensionPixelSize(width);
params.height = getResources().getDimensionPixelSize(height);
params.alpha = alpha;
params.dimAmount = dim;
params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
getWindow().setAttributes(params);
}
/**
* Returns true if the theme sets the {@code R.attr.isFloatingWindow} flag to true.
*/
protected boolean shouldBeFloatingWindow() {
Resources.Theme theme = getTheme();
TypedValue floatingWindowFlag = new TypedValue();
// Check isFloatingWindow flag is defined in theme.
if (theme == null || !theme
.resolveAttribute(R.attr.isFloatingWindow, floatingWindowFlag, true)) {
return false;
}
return (floatingWindowFlag.data != 0);
}
}