/*
* Artcodes recognises a different marker scheme that allows the
* creation of aesthetically pleasing, even beautiful, codes.
* Copyright (C) 2013-2016 The University of Nottingham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.horizon.artcodes.account;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import okhttp3.CacheControl;
import okhttp3.Headers;
import okhttp3.Request;
import okhttp3.Response;
import uk.ac.horizon.artcodes.Artcodes;
import uk.ac.horizon.artcodes.GoogleAnalytics;
import uk.ac.horizon.artcodes.model.Experience;
import uk.ac.horizon.artcodes.server.HTTPException;
import uk.ac.horizon.artcodes.server.JsonCallback;
import uk.ac.horizon.artcodes.server.LoadCallback;
import uk.ac.horizon.artcodes.server.URILoaderCallback;
public class AppEngineAccount implements Account
{
private boolean numberOfExperiencesHasChangedHint = false;
private Map<String, Object> experienceUrlsThatHaveChangedHint = new ConcurrentHashMap<>();
static final String appSavePrefix = "appSaveID:";
private static final String httpRoot = "http://aestheticodes.appspot.com/";
private static final String httpsRoot = "https://aestheticodes.appspot.com/";
private final Context context;
private final Gson gson;
private final String name;
private final android.accounts.Account account;
private final Map<String, AppEngineUploadThread> uploadThreads = new HashMap<>();
public AppEngineAccount(Context context, String name, Gson gson)
{
this.context = context;
this.name = name;
this.gson = gson;
this.account = new android.accounts.Account(name, "com.google");
}
@Override
public boolean validates() throws UserRecoverableAuthException
{
try
{
return getToken() != null;
}
catch (UserRecoverableAuthException e)
{
throw e;
}
catch (GoogleAuthException | IOException e)
{
GoogleAnalytics.trackException(e);
return false;
}
}
@Override
public boolean isLocal()
{
return false;
}
@Override
public void loadLibrary(final LoadCallback<List<String>> callback)
{
load("https://aestheticodes.appspot.com/experiences", new JsonCallback<>(new TypeToken<List<String>>()
{
}.getType(), gson, context, new LoadCallback<List<String>>()
{
@Override
public void loaded(List<String> item)
{
SharedPreferences.Editor editor = context.getSharedPreferences(Account.class.getName(), Context.MODE_PRIVATE).edit();
for (String uri : item)
{
editor.putString(uri, getId());
}
editor.apply();
callback.loaded(item);
}
@Override
public void error(Throwable e)
{
callback.error(e);
}
}));
}
public void setDisplayName(String displayName)
{
context.getSharedPreferences(Account.class.getName(), Context.MODE_PRIVATE).edit()
.putString(getId(), displayName)
.apply();
}
@Override
public boolean load(final String uri, final URILoaderCallback callback)
{
AppEngineUploadThread uploadThread = uploadThreads.get(uri);
if (uploadThread != null)
{
Experience experience = uploadThread.getExperience();
callback.onLoaded(new StringReader(gson.toJson(experience)));
return true;
}
if (uri.startsWith(httpRoot) || uri.startsWith(httpsRoot))
{
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
CacheControl cc;
if (numberOfExperiencesHasChangedHint && uri.equals("https://aestheticodes.appspot.com/experiences"))
{
numberOfExperiencesHasChangedHint = false;
cc = CacheControl.FORCE_NETWORK;
}
else if (experienceUrlsThatHaveChangedHint.containsKey(uri))
{
experienceUrlsThatHaveChangedHint.remove(uri);
cc = CacheControl.FORCE_NETWORK;
}
else
{
cc = new CacheControl.Builder().maxAge(5, TimeUnit.MINUTES).build();
}
String url = uri.replace(httpRoot, httpsRoot);
final Request request = new Request.Builder()
.get()
.url(url)
.headers(getHeaders())
.cacheControl(cc)
.build();
final Response response = Artcodes.httpClient.newCall(request).execute();
validateResponse(request, response);
callback.onLoaded(response.body().charStream());
response.body().close();
}
catch (Exception e)
{
callback.onError(e);
}
}
}).start();
return true;
}
return false;
}
@Override
public void saveExperience(final Experience experience)
{
this.saveExperience(experience, null);
}
@Override
public void saveExperience(final Experience experience, final AccountProcessCallback saveCallback)
{
if (experience.getId() == null)
{
experience.setId(appSavePrefix + UUID.randomUUID().toString());
this.numberOfExperiencesHasChangedHint = true;
}
else
{
this.experienceUrlsThatHaveChangedHint.put(experience.getId(), new Object());
}
AppEngineUploadThread uploadThread = new AppEngineUploadThread(this, experience, saveCallback);
uploadThreads.put(experience.getId(), uploadThread);
uploadThread.start();
}
@Override
public void deleteExperience(final Experience experience)
{
this.deleteExperience(experience, null);
}
@Override
public void deleteExperience(final Experience experience, final Account.AccountProcessCallback accountProcessCallback)
{
if (!canEdit(experience.getId()))
{
if (accountProcessCallback != null)
{
accountProcessCallback.accountProcessCallback(false, null);
}
return;
}
this.numberOfExperiencesHasChangedHint = true;
new Thread(new Runnable()
{
@Override
public void run()
{
boolean success = true;
try
{
final Request request = new Request.Builder()
.delete()
.url(experience.getId())
.headers(getHeaders())
.build();
final Response response = Artcodes.httpClient.newCall(request).execute();
validateResponse(request, response);
}
catch (Exception e)
{
success = false;
GoogleAnalytics.trackException(e);
}
if (accountProcessCallback != null)
{
accountProcessCallback.accountProcessCallback(success, null);
}
}
}).start();
}
@Override
public boolean equals(Object o)
{
return o instanceof Account && ((Account) o).getId().equals(getId());
}
@Override
public String getId()
{
return "google:" + name;
}
@Override
public String toString()
{
return name;
}
@Override
public String getDisplayName()
{
return context.getSharedPreferences(Account.class.getName(), Context.MODE_PRIVATE).getString(getId(), name);
}
@Override
public boolean canEdit(String uri)
{
final SharedPreferences preferences = context.getSharedPreferences(Account.class.getName(), Context.MODE_PRIVATE);
final String account = preferences.getString(uri, null);
return account != null && account.equals(getId());
}
@Override
public boolean isSaving(String uri)
{
try
{
AppEngineUploadThread uploadThread = uploadThreads.get(uri);
return uploadThread != null && !uploadThread.isFinished();
}
catch (Exception e)
{
GoogleAnalytics.trackException(e);
}
return false;
}
Headers getHeaders()
{
final Headers.Builder headers = new Headers.Builder();
headers.set("User-Agent", Artcodes.userAgent);
try
{
String token = getToken();
if (token != null)
{
headers.set("Authorization", "Bearer " + token);
}
}
catch (Exception e)
{
GoogleAnalytics.trackException(e);
}
return headers.build();
}
Gson getGson()
{
return gson;
}
Context getContext()
{
return context;
}
void validateResponse(Request request, Response response) throws IOException
{
if (response.code() == 401)
{
Log.w("", "Response " + response.code());
String authHeader = request.header("Authorization");
if (authHeader != null)
{
String token = authHeader.split(" ")[1];
try
{
GoogleAuthUtil.clearToken(context, token);
}
catch (GoogleAuthException | IOException e)
{
GoogleAnalytics.trackException(e);
}
}
throw new HTTPException(response.code(), response.message());
}
else if (response.code() != 200)
{
throw new HTTPException(response.code(), response.message());
}
}
private String getToken() throws IOException, GoogleAuthException
{
return GoogleAuthUtil.getToken(context, account, "oauth2:email");
}
}