/*
* Copyright 2016 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.myschedule;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.samples.apps.iosched.BuildConfig;
import com.google.samples.apps.iosched.Config;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.archframework.Model;
import com.google.samples.apps.iosched.archframework.QueryEnum;
import com.google.samples.apps.iosched.archframework.UserActionEnum;
import com.google.samples.apps.iosched.model.ScheduleHelper;
import com.google.samples.apps.iosched.model.ScheduleItem;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.settings.SettingsUtils;
import com.google.samples.apps.iosched.util.AnalyticsHelper;
import com.google.samples.apps.iosched.util.ParserUtils;
import com.google.samples.apps.iosched.util.ThrottledContentObserver;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import static com.google.common.base.Preconditions.checkNotNull;
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.makeLogTag;
public class MyScheduleModel implements Model<MyScheduleModel.MyScheduleQueryEnum,
MyScheduleModel.MyScheduleUserActionEnum> {
private static final String TAG = makeLogTag(MyScheduleModel.class);
public static final int PRE_CONFERENCE_DAY_ID = 0;
/**
* Used for user action {@link MyScheduleUserActionEnum#SESSION_SLOT}
*/
public static final String SESSION_URL_KEY = "SESSION_URL_KEY";
/**
* Used for user action {@link MyScheduleUserActionEnum#FEEDBACK}
*/
public static final String SESSION_ID_KEY = "SESSION_ID_KEY";
/**
* Used for user action {@link MyScheduleUserActionEnum#FEEDBACK}
*/
public static final String SESSION_TITLE_KEY = "SESSION_TITLE_KEY";
/**
* The key of {@link #mScheduleData} is the index of the day in the conference, starting at 1
* for the first day of the conference, using {@link #PRE_CONFERENCE_DAY_ID} for the
* preconference day, if any.
*/
protected HashMap<Integer, ArrayList<ScheduleItem>> mScheduleData =
new HashMap<Integer, ArrayList<ScheduleItem>>();
// The ScheduleHelper is responsible for feeding data in a format suitable to the Adapter.
private ScheduleHelper mScheduleHelper;
private Context mContext;
protected DataQueryCallback mScheduleDataQueryCallback;
/**
* @param scheduleHelper
* @param context Should be an Activity context
*/
public MyScheduleModel(ScheduleHelper scheduleHelper, Context context) {
mContext = context;
mScheduleHelper = scheduleHelper;
}
/**
* Initialises the pre conference data and data observers. This is not called from the
* constructor, to allow for unit tests to bypass this (as this uses Android methods not
* available in unit tests).
*
* @return the Model it can be chained with the constructor
*/
public MyScheduleModel initStaticDataAndObservers() {
if (showPreConferenceData(mContext)) {
preparePreConferenceDayAdapter();
}
addDataObservers();
return this;
}
/**
* This method is an ad-hoc implementation of the pre conference day, which contains an item to
* pick up the badge at registration desk
*/
private void preparePreConferenceDayAdapter() {
ScheduleItem item = new ScheduleItem();
item.title = mContext.getString(R.string.my_schedule_badgepickup);
item.startTime = ParserUtils.parseTime(BuildConfig.PRECONFERENCE_DAY_START);
item.endTime = ParserUtils.parseTime(BuildConfig.PRECONFERENCE_DAY_END);
item.type = ScheduleItem.BREAK;
item.room =
item.subtitle = mContext.getString(R.string.my_schedule_badgepickup_description);
item.sessionType = ScheduleItem.SESSION_TYPE_MISC;
mScheduleData.put(PRE_CONFERENCE_DAY_ID, new ArrayList<ScheduleItem>(Arrays.asList(item)));
}
private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener =
new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
LOGD(TAG, "sharedpreferences key " + key + " changed, maybe reloading data.");
if (SettingsUtils.PREF_LOCAL_TIMES.equals(key)) {
// mPrefChangeListener is observing as soon as the model is created but
// mScheduleDataQueryCallback is only created when the view has requested
// some data. There is a tiny amount of time when mPrefChangeListener is
// active but mScheduleDataQueryCallback is null. This was observed when
// going to MySchedule screen straight after the welcome flow.
if (mScheduleDataQueryCallback != null) {
mScheduleDataQueryCallback.onModelUpdated(MyScheduleModel.this,
MyScheduleQueryEnum.SCHEDULE);
} else {
LOGE(TAG, "sharedpreferences key " + key +
" changed, but null schedule data query callback, cannot " +
"inform model is updated");
}
} else if (BuildConfig.PREF_ATTENDEE_AT_VENUE.equals(key)) {
updateData(mScheduleDataQueryCallback);
}
}
};
/**
* Observe changes on base uri and in shared preferences
*/
private void addDataObservers() {
mContext.getContentResolver().registerContentObserver(
ScheduleContract.BASE_CONTENT_URI, true, mObserver);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
sp.registerOnSharedPreferenceChangeListener(mPrefChangeListener);
}
private void removeDataObservers() {
mContext.getContentResolver().unregisterContentObserver(mObserver);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
sp.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener);
}
/**
* Visible for classes extending this model, so UI tests can be written to simulate the system
* firing this observer.
*/
@VisibleForTesting
protected final ThrottledContentObserver mObserver = new ThrottledContentObserver(
new ThrottledContentObserver.Callbacks() {
@Override
public void onThrottledContentObserverFired() {
LOGD(TAG, "content may be changed, reloading data");
updateData(mScheduleDataQueryCallback);
}
});
@Override
public MyScheduleQueryEnum[] getQueries() {
return MyScheduleQueryEnum.values();
}
@Override
public MyScheduleUserActionEnum[] getUserActions() {
return MyScheduleUserActionEnum.values();
}
/**
* @param day The day of the conference, starting at 1 for the first day
* @return the list of items, or an empty list if the day isn't found
*/
public ArrayList<ScheduleItem> getConferenceDataForDay(int day) {
if (mScheduleData.containsKey(day)) {
return mScheduleData.get(day);
} else {
return new ArrayList<>();
}
}
public static boolean showPreConferenceData(Context context) {
return SettingsUtils.isAttendeeAtVenue(context);
}
@Override
public void deliverUserAction(final MyScheduleUserActionEnum action, @Nullable Bundle args,
final UserActionCallback callback) {
switch (action) {
case RELOAD_DATA:
DataQueryCallback queryCallback = new DataQueryCallback() {
@Override
public void onModelUpdated(Model model, QueryEnum query) {
callback.onModelUpdated(MyScheduleModel.this, action);
}
@Override
public void onError(QueryEnum query) {
callback.onError(action);
}
};
if (mScheduleDataQueryCallback == null) {
mScheduleDataQueryCallback = queryCallback;
}
updateData(queryCallback);
break;
case SESSION_SLOT:
if (args == null || !args.containsKey(SESSION_URL_KEY)) {
callback.onError(action);
} else {
String uriStr = args.getString(SESSION_URL_KEY);
// ANALYTICS EVENT: Select a slot on My Agenda
// Contains: URI indicating session ID or time interval of slot
AnalyticsHelper.sendEvent("My Schedule", "selectslot", uriStr);
// No need to notify presenter, nothing to do
}
break;
case FEEDBACK:
if (args == null || !args.containsKey(SESSION_ID_KEY)
|| !args.containsKey(SESSION_TITLE_KEY)) {
callback.onError(action);
} else {
String title = args.getString(SESSION_TITLE_KEY);
String id = args.getString(SESSION_ID_KEY);
// ANALYTICS EVENT: Click on the "Send Feedback" action from Schedule page.
// Contains: The session title.
AnalyticsHelper.sendEvent("My Schedule", "Feedback", title);
// No need to notify presenter, nothing to do
}
break;
case REDRAW_UI:
// We use cached data
callback.onModelUpdated(this, action);
break;
default:
break;
}
}
@Override
public void requestData(@NonNull MyScheduleQueryEnum query,
@NonNull DataQueryCallback callback) {
checkNotNull(query);
checkNotNull(callback);
switch (query) {
case SCHEDULE:
mScheduleDataQueryCallback = callback;
updateData(mScheduleDataQueryCallback);
break;
default:
callback.onError(query);
break;
}
}
@Override
public void cleanUp() {
removeDataObservers();
}
/**
* This updates the data, by calling {@link ScheduleHelper#getScheduleDataAsync
* (LoadScheduleDataListener, long, long)} for each day. It is protected and not private, to
* allow us to extend this class and use mock data in UI tests (refer {@code
* StubMyScheduleModel} in {@code androidTest}).
*/
protected void updateData(final DataQueryCallback callback) {
for (int i = 0; i < Config.CONFERENCE_DAYS.length; i++) {
/**
* The key in {@link #mScheduleData} is 1 for the first day, 2 for the second etc
*/
final int dayId = i + 1;
// Immediately use cached data if available
if (mScheduleData.containsKey(dayId)) {
if (callback != null) {
callback.onModelUpdated(this, MyScheduleQueryEnum.SCHEDULE);
}
}
// Update cached data
mScheduleHelper.getScheduleDataAsync(
new LoadScheduleDataListener() {
@Override
public void onDataLoaded(ArrayList<ScheduleItem> scheduleItems) {
updateCache(dayId, scheduleItems, callback);
}
},
Config.CONFERENCE_DAYS[i][0], Config.CONFERENCE_DAYS[i][1]);
}
}
/**
* This updates the cached data for the day with id {@code dayId} with {@code scheduleItems}
* then notifies the {@code callback}.It is protected and not private, to allow us to extend
* this class and use mock data in UI tests (refer {@code StubMyScheduleModel} in {@code
* androidTest}).
*/
protected void updateCache(int dayId, ArrayList<ScheduleItem> scheduleItems,
DataQueryCallback callback) {
mScheduleData.put(dayId, scheduleItems);
if (callback != null) {
callback.onModelUpdated(MyScheduleModel.this,
MyScheduleQueryEnum.SCHEDULE);
}
}
public enum MyScheduleQueryEnum implements QueryEnum {
SCHEDULE(0, null);
private int id;
private String[] projection;
MyScheduleQueryEnum(int id, String[] projection) {
this.id = id;
this.projection = projection;
}
@Override
public int getId() {
return id;
}
@Override
public String[] getProjection() {
return projection;
}
}
public enum MyScheduleUserActionEnum implements UserActionEnum {
RELOAD_DATA(1),
// Click on a row in the schedule, it opens the session or a list of available sessions
SESSION_SLOT(2),
FEEDBACK(3),
REDRAW_UI(4);
private int id;
MyScheduleUserActionEnum(int id) {
this.id = id;
}
@Override
public int getId() {
return id;
}
}
public interface LoadScheduleDataListener {
void onDataLoaded(ArrayList<ScheduleItem> scheduleItems);
}
}