/*******************************************************************************
* This file is part of RedReader.
*
* RedReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* RedReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with RedReader. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.quantumbadger.redreader.reddit.api;
import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import org.quantumbadger.redreader.account.RedditAccount;
import org.quantumbadger.redreader.activities.BugReportActivity;
import org.quantumbadger.redreader.cache.CacheManager;
import org.quantumbadger.redreader.cache.CacheRequest;
import org.quantumbadger.redreader.common.AndroidApi;
import org.quantumbadger.redreader.common.General;
import org.quantumbadger.redreader.common.RRError;
import org.quantumbadger.redreader.common.TimestampBound;
import org.quantumbadger.redreader.common.UnexpectedInternalStateException;
import org.quantumbadger.redreader.common.collections.WeakReferenceListManager;
import org.quantumbadger.redreader.io.RawObjectDB;
import org.quantumbadger.redreader.io.RequestResponseHandler;
import org.quantumbadger.redreader.io.WritableHashSet;
import org.quantumbadger.redreader.reddit.APIResponseHandler;
import org.quantumbadger.redreader.reddit.RedditAPI;
import org.quantumbadger.redreader.reddit.RedditSubredditHistory;
import org.quantumbadger.redreader.reddit.RedditSubredditManager;
import org.quantumbadger.redreader.reddit.things.RedditSubreddit;
import java.util.ArrayList;
import java.util.HashSet;
public class RedditSubredditSubscriptionManager {
private static final String TAG = "SubscriptionManager";
public enum SubredditSubscriptionState { SUBSCRIBED, SUBSCRIBING, UNSUBSCRIBING, NOT_SUBSCRIBED }
private final SubredditSubscriptionStateChangeNotifier notifier = new SubredditSubscriptionStateChangeNotifier();
private final WeakReferenceListManager<SubredditSubscriptionStateChangeListener> listeners
= new WeakReferenceListManager<>();
private static RedditSubredditSubscriptionManager singleton;
private static RedditAccount singletonAccount;
private final RedditAccount user;
private final Context context;
private static RawObjectDB<String, WritableHashSet> db = null;
private WritableHashSet subscriptions;
private final HashSet<String> pendingSubscriptions = new HashSet<>(), pendingUnsubscriptions = new HashSet<>();
public static synchronized RedditSubredditSubscriptionManager getSingleton(final Context context, final RedditAccount account) {
if(db == null) {
db = new RawObjectDB<>(context, "rr_subscriptions.db", WritableHashSet.class);
}
if(singleton == null || !account.equals(RedditSubredditSubscriptionManager.singletonAccount)) {
singleton = new RedditSubredditSubscriptionManager(account, context);
RedditSubredditSubscriptionManager.singletonAccount = account;
}
return singleton;
}
private RedditSubredditSubscriptionManager(RedditAccount user, Context context) {
this.user = user;
this.context = context;
subscriptions = db.getById(user.getCanonicalUsername());
if(subscriptions != null) {
addToHistory(user, subscriptions.toHashset());
}
}
public void addListener(SubredditSubscriptionStateChangeListener listener) {
listeners.add(listener);
}
public synchronized boolean areSubscriptionsReady() {
return subscriptions != null;
}
public synchronized SubredditSubscriptionState getSubscriptionState(final String subredditCanonicalId) {
if(pendingSubscriptions.contains(subredditCanonicalId)) return SubredditSubscriptionState.SUBSCRIBING;
else if(pendingUnsubscriptions.contains(subredditCanonicalId)) return SubredditSubscriptionState.UNSUBSCRIBING;
else if(subscriptions.toHashset().contains(subredditCanonicalId)) return SubredditSubscriptionState.SUBSCRIBED;
else return SubredditSubscriptionState.NOT_SUBSCRIBED;
}
private synchronized void onSubscriptionAttempt(final String subredditCanonicalId) {
pendingSubscriptions.add(subredditCanonicalId);
listeners.map(notifier, SubredditSubscriptionChangeType.SUBSCRIPTION_ATTEMPTED);
}
private synchronized void onUnsubscriptionAttempt(final String subredditCanonicalId) {
pendingUnsubscriptions.add(subredditCanonicalId);
listeners.map(notifier, SubredditSubscriptionChangeType.UNSUBSCRIPTION_ATTEMPTED);
}
private synchronized void onSubscriptionChangeAttemptFailed(final String subredditCanonicalId) {
pendingUnsubscriptions.remove(subredditCanonicalId);
pendingSubscriptions.remove(subredditCanonicalId);
listeners.map(notifier, SubredditSubscriptionChangeType.LIST_UPDATED);
}
private synchronized void onSubscriptionAttemptSuccess(final String subredditCanonicalId) {
pendingSubscriptions.remove(subredditCanonicalId);
subscriptions.toHashset().add(subredditCanonicalId);
listeners.map(notifier, SubredditSubscriptionChangeType.LIST_UPDATED);
}
private synchronized void onUnsubscriptionAttemptSuccess(final String subredditCanonicalId) {
pendingUnsubscriptions.remove(subredditCanonicalId);
subscriptions.toHashset().remove(subredditCanonicalId);
listeners.map(notifier, SubredditSubscriptionChangeType.LIST_UPDATED);
}
private static void addToHistory(final RedditAccount account, final HashSet<String> newSubscriptions)
{
for(final String sub : newSubscriptions)
{
try
{
RedditSubredditHistory.addSubreddit(account, sub);
}
catch(RedditSubreddit.InvalidSubredditNameException e)
{
Log.e(TAG, "Invalid subreddit name " + sub, e);
}
}
}
private synchronized void onNewSubscriptionListReceived(final HashSet<String> newSubscriptions, final long timestamp) {
pendingSubscriptions.clear();
pendingUnsubscriptions.clear();
subscriptions = new WritableHashSet(newSubscriptions, timestamp, user.getCanonicalUsername());
// TODO threaded? or already threaded due to cache manager
db.put(subscriptions);
addToHistory(user, newSubscriptions);
listeners.map(notifier, SubredditSubscriptionChangeType.LIST_UPDATED);
}
public synchronized ArrayList<String> getSubscriptionList() {
return new ArrayList<>(subscriptions.toHashset());
}
public void triggerUpdate(final RequestResponseHandler<HashSet<String>, SubredditRequestFailure> handler, TimestampBound timestampBound) {
if(subscriptions != null && timestampBound.verifyTimestamp(subscriptions.getTimestamp())) {
return;
}
new RedditAPIIndividualSubredditListRequester(context, user).performRequest(
RedditSubredditManager.SubredditListType.SUBSCRIBED,
timestampBound,
new RequestResponseHandler<WritableHashSet, SubredditRequestFailure>() {
// TODO handle failed requests properly -- retry? then notify listeners
@Override
public void onRequestFailed(SubredditRequestFailure failureReason) {
if(handler != null) handler.onRequestFailed(failureReason);
}
@Override
public void onRequestSuccess(WritableHashSet result, long timeCached) {
final HashSet<String> newSubscriptions = result.toHashset();
onNewSubscriptionListReceived(newSubscriptions, timeCached);
if(handler != null) handler.onRequestSuccess(newSubscriptions, timeCached);
}
}
);
}
public void subscribe(final String subredditCanonicalId, final AppCompatActivity activity) {
RedditAPI.subscriptionAction(
CacheManager.getInstance(context),
new SubredditActionResponseHandler(activity, RedditAPI.SUBSCRIPTION_ACTION_SUBSCRIBE, subredditCanonicalId),
user,
subredditCanonicalId,
RedditAPI.SUBSCRIPTION_ACTION_SUBSCRIBE,
context
);
onSubscriptionAttempt(subredditCanonicalId);
}
public void unsubscribe(final String subredditCanonicalId, final AppCompatActivity activity) {
RedditAPI.subscriptionAction(
CacheManager.getInstance(context),
new SubredditActionResponseHandler(activity, RedditAPI.SUBSCRIPTION_ACTION_UNSUBSCRIBE, subredditCanonicalId),
user,
subredditCanonicalId,
RedditAPI.SUBSCRIPTION_ACTION_UNSUBSCRIBE,
context
);
onUnsubscriptionAttempt(subredditCanonicalId);
}
private class SubredditActionResponseHandler extends APIResponseHandler.ActionResponseHandler {
private final @RedditAPI.RedditSubredditAction int action;
private final AppCompatActivity activity;
private final String canonicalName;
protected SubredditActionResponseHandler(AppCompatActivity activity,
@RedditAPI.RedditSubredditAction int action,
String canonicalName) {
super(activity);
this.activity = activity;
this.action = action;
this.canonicalName = canonicalName;
}
@Override
protected void onSuccess() {
switch(action) {
case RedditAPI.SUBSCRIPTION_ACTION_SUBSCRIBE:
onSubscriptionAttemptSuccess(canonicalName);
break;
case RedditAPI.SUBSCRIPTION_ACTION_UNSUBSCRIBE:
onUnsubscriptionAttemptSuccess(canonicalName);
break;
}
triggerUpdate(null, TimestampBound.NONE);
}
@Override
protected void onCallbackException(Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(@CacheRequest.RequestFailureType int type, Throwable t, Integer status, String readableMessage) {
onSubscriptionChangeAttemptFailed(canonicalName);
if(t != null) t.printStackTrace();
final RRError error = General.getGeneralErrorForFailure(context, type, t, status, null);
AndroidApi.UI_THREAD_HANDLER.post(new Runnable() {
@Override
public void run() {
General.showResultDialog(activity, error);
}
});
}
@Override
protected void onFailure(APIFailureType type) {
onSubscriptionChangeAttemptFailed(canonicalName);
final RRError error = General.getGeneralErrorForFailure(context, type);
AndroidApi.UI_THREAD_HANDLER.post(new Runnable() {
@Override
public void run() {
General.showResultDialog(activity, error);
}
});
}
}
public Long getSubscriptionListTimestamp() {
return subscriptions != null ? subscriptions.getTimestamp() : null;
}
public interface SubredditSubscriptionStateChangeListener {
void onSubredditSubscriptionListUpdated(RedditSubredditSubscriptionManager subredditSubscriptionManager);
void onSubredditSubscriptionAttempted(RedditSubredditSubscriptionManager subredditSubscriptionManager);
void onSubredditUnsubscriptionAttempted(RedditSubredditSubscriptionManager subredditSubscriptionManager);
}
private enum SubredditSubscriptionChangeType {LIST_UPDATED, SUBSCRIPTION_ATTEMPTED, UNSUBSCRIPTION_ATTEMPTED}
private class SubredditSubscriptionStateChangeNotifier
implements WeakReferenceListManager.ArgOperator<SubredditSubscriptionStateChangeListener, SubredditSubscriptionChangeType> {
public void operate(SubredditSubscriptionStateChangeListener listener, SubredditSubscriptionChangeType changeType) {
switch(changeType) {
case LIST_UPDATED:
listener.onSubredditSubscriptionListUpdated(RedditSubredditSubscriptionManager.this);
break;
case SUBSCRIPTION_ATTEMPTED:
listener.onSubredditSubscriptionAttempted(RedditSubredditSubscriptionManager.this);
break;
case UNSUBSCRIPTION_ATTEMPTED:
listener.onSubredditUnsubscriptionAttempted(RedditSubredditSubscriptionManager.this);
break;
default:
throw new UnexpectedInternalStateException("Invalid SubredditSubscriptionChangeType " + changeType.toString());
}
}
}
}