package com.github.andlyticsproject.console.v2;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.util.Log;
import com.github.andlyticsproject.console.AuthenticationException;
import com.github.andlyticsproject.console.DevConsole;
import com.github.andlyticsproject.console.DevConsoleException;
import com.github.andlyticsproject.console.NetworkException;
import com.github.andlyticsproject.model.AppInfo;
import com.github.andlyticsproject.model.AppStats;
import com.github.andlyticsproject.model.Comment;
import com.github.andlyticsproject.model.DeveloperConsoleAccount;
import com.github.andlyticsproject.model.RevenueSummary;
import com.github.andlyticsproject.util.Utils;
import org.apache.http.HttpStatus;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* This is a WIP class representing the new v2 version of the developer console.
* The aim is to build it from scratch to make it a light weight and as well
* documented at the end as possible. Once it is done and available to all
* users, we will rip out the old code and replace it with this.
*
* Once v2 is available to all users, there is scope for better utilising the
* available statistics data, so keep that in mind when developing this class.
* For now though, keep it simple and get it working.
*
* See https://github.com/AndlyticsProject/andlytics/wiki/Developer-Console-v2
* for some more documentation
*
* This class fetches the data, which is then passed using {@link JsonParser}
*
*/
@SuppressLint("DefaultLocale")
public class DevConsoleV2 implements DevConsole {
// 30 seconds -- for both socket and connection
public static final int TIMEOUT = 30 * 1000;
private static final String TAG = DevConsoleV2.class.getSimpleName();
private static final boolean DEBUG = false;
private DefaultHttpClient httpClient;
private DevConsoleAuthenticator authenticator;
private String accountName;
private DevConsoleV2Protocol protocol;
private ResponseHandler<String> responseHandler = HttpClientFactory.createResponseHandler();
public static DevConsoleV2 createForAccount(String accountName, DefaultHttpClient httpClient) {
DevConsoleAuthenticator authenticator = new OauthAccountManagerAuthenticator(accountName,
httpClient);
return new DevConsoleV2(httpClient, authenticator, new DevConsoleV2Protocol());
}
public static DevConsoleV2 createForAccountAndPassword(String accountName, String password,
DefaultHttpClient httpClient) {
DevConsoleAuthenticator authenticator = new PasswordAuthenticator(accountName, password,
httpClient);
return new DevConsoleV2(httpClient, authenticator, new DevConsoleV2Protocol());
}
private DevConsoleV2(DefaultHttpClient httpClient, DevConsoleAuthenticator authenticator,
DevConsoleV2Protocol protocol) {
this.httpClient = httpClient;
this.authenticator = authenticator;
this.accountName = authenticator.getAccountName();
this.protocol = protocol;
}
/**
* Gets a list of available apps for the given account
*
* @param activity
* @return
* @throws DevConsoleException
*/
public synchronized List<AppInfo> getAppInfo(Activity activity) throws DevConsoleException {
try {
// the authenticator launched a sub-activity, bail out for now
if (!authenticateWithCachedCredentialas(activity)) {
return new ArrayList<AppInfo>();
}
return fetchAppInfosAndStatistics();
} catch (AuthenticationException ex) {
if (!authenticateFromScratch(activity)) {
return new ArrayList<AppInfo>();
}
return fetchAppInfosAndStatistics();
}
}
private List<AppInfo> fetchAppInfosAndStatistics() {
// Fetch a list of available apps
List<AppInfo> apps = fetchAppInfos();
for (AppInfo app : apps) {
// Fetch remaining app statistics
// Latest stats object, and active/total installs is fetched
// in fetchAppInfos
AppStats stats = app.getLatestStats();
fetchRatings(app, stats);
RevenueSummary revenue = fetchRevenueSummary(app);
app.setTotalRevenueSummary(revenue);
// this is currently the same as the last item of the historical
// data, so save some cycles and don't parse historical
// XXX the definition of 'last day' is unclear: GMT?
if (revenue != null) {
stats.setTotalRevenue(revenue.getLastDay());
}
}
return apps;
}
/**
* Gets a list of comments for the given app based on the startIndex and
* count
*
* @param accountName
* @param packageName
* @param startIndex
* @param count
* @return
* @throws DevConsoleException
*/
public synchronized List<Comment> getComments(Activity activity, String packageName,
String developerId, int startIndex, int count, String displayLocale)
throws DevConsoleException {
try {
if (!authenticateWithCachedCredentialas(activity)) {
return new ArrayList<Comment>();
}
return fetchComments(packageName, developerId, startIndex, count, displayLocale);
} catch (AuthenticationException ex) {
if (!authenticateFromScratch(activity)) {
return new ArrayList<Comment>();
}
return fetchComments(packageName, developerId, startIndex, count, displayLocale);
}
}
public synchronized Comment replyToComment(Activity activity, String packageName,
String developerId, String commentUniqueId, String reply) {
try {
if (!authenticateWithCachedCredentialas(activity)) {
return null;
}
return replyToComment(packageName, developerId, commentUniqueId, reply);
} catch (AuthenticationException ex) {
if (!authenticateFromScratch(activity)) {
return null;
}
return replyToComment(packageName, developerId, commentUniqueId, reply);
}
}
private Comment replyToComment(String packageName, String developerId, String commentUiqueId,
String reply) {
String response = post(protocol.createCommentsUrl(developerId),
protocol.createReplyToCommentRequest(packageName, commentUiqueId, reply),
developerId);
return protocol.parseCommentReplyResponse(response);
}
/**
* Fetches a combined list of apps for all available console accounts
*
* @return combined list of apps
* @throws DevConsoleException
*/
private List<AppInfo> fetchAppInfos() throws DevConsoleException {
List<AppInfo> result = new ArrayList<AppInfo>();
for (DeveloperConsoleAccount consoleAccount : protocol.getSessionCredentials()
.getDeveloperConsoleAccounts()) {
String developerId = consoleAccount.getDeveloperId();
if (!consoleAccount.getCanAccessApps()) {
Log.w(TAG, "Not allowed to fetch app info for " + developerId);
continue;
}
Log.d(TAG, "Getting apps for " + developerId);
String response = post(protocol.createFetchAppsUrl(developerId),
protocol.createFetchAppInfosRequest(), developerId);
// don't skip incomplete apps, so we can get the package list
List<AppInfo> apps = protocol.parseAppInfosResponse(response, accountName, false);
if (apps.isEmpty()) {
continue;
}
for (AppInfo appInfo : apps) {
appInfo.setDeveloperId(developerId);
appInfo.setDeveloperName(consoleAccount.getName());
}
result.addAll(apps);
List<String> incompletePackages = new ArrayList<String>();
for (AppInfo app : apps) {
if (app.isIncomplete()) {
result.remove(app);
incompletePackages.add(app.getPackageName());
}
}
Log.d(TAG, String.format("Found %d apps for %s", apps.size(), developerId));
Log.d(TAG, String.format("Incomplete packages: %d", incompletePackages.size()));
if (incompletePackages.isEmpty()) {
continue;
}
Log.d(TAG, String.format("Got %d incomplete apps, issuing details request",
incompletePackages.size()));
response = post(protocol.createFetchAppsUrl(developerId),
protocol.createFetchAppInfosRequest(incompletePackages), developerId);
// if info is not here, not much to do, skip
List<AppInfo> extraApps = protocol.parseAppInfosResponse(response, accountName, true);
Log.d(TAG, String.format("Got %d extra apps from details request", extraApps.size()));
for (AppInfo appInfo : extraApps) {
appInfo.setDeveloperId(developerId);
appInfo.setDeveloperName(consoleAccount.getName());
}
result.addAll(extraApps);
}
return result;
}
/**
* Fetches statistics for the given packageName of the given statsType and
* adds them to the given {@link AppStats} object
*
* This is not used as statistics can be fetched via fetchAppInfos Can use
* it later to get historical etc data
*
* @param packageName
* @param stats
* @param statsType
* @throws DevConsoleException
*/
@SuppressWarnings("unused")
private void fetchStatistics(AppInfo appInfo, AppStats stats, int statsType)
throws DevConsoleException {
String developerId = appInfo.getDeveloperId();
String response = post(protocol.createFetchStatisticsUrl(developerId),
protocol.createFetchStatisticsRequest(appInfo.getPackageName(), statsType),
developerId);
protocol.parseStatisticsResponse(response, stats, statsType);
}
/**
* Fetches ratings for the given packageName and adds them to the given {@link AppStats} object
*
* @param packageName
* The app to fetch ratings for
* @param stats
* The AppStats object to add them to
* @throws DevConsoleException
*/
private void fetchRatings(AppInfo appInfo, AppStats stats) throws DevConsoleException {
String developerId = appInfo.getDeveloperId();
String response = post(protocol.createCommentsUrl(developerId),
protocol.createFetchRatingsRequest(appInfo.getPackageName()), developerId);
protocol.parseRatingsResponse(response, stats);
}
private List<Comment> fetchComments(String packageName, String developerId, int startIndex,
int count, String displayLocale) throws DevConsoleException {
List<Comment> comments = new ArrayList<Comment>();
String response = post(protocol.createCommentsUrl(developerId),
protocol.createFetchCommentsRequest(packageName, startIndex, count, displayLocale),
developerId);
comments.addAll(protocol.parseCommentsResponse(response));
return comments;
}
private RevenueSummary fetchRevenueSummary(AppInfo appInfo) throws DevConsoleException {
try {
String developerId = appInfo.getDeveloperId();
String response = post(protocol.createRevenueUrl(developerId),
protocol.createFetchRevenueSummaryRequest(appInfo.getPackageName()),
developerId);
return protocol.parseRevenueResponse(response);
} catch (NetworkException e) {
// XXX not pretty, maybe use a dedicated exception?
// if we don't have 'view financial info' permission for an app
// getting revenue returns 403.
// same sems to apply for 500
if (e.getStatusCode() == HttpStatus.SC_FORBIDDEN
|| e.getStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
return null;
}
throw e;
}
}
private boolean authenticateWithCachedCredentialas(Activity activity) {
return authenticate(activity, false);
}
private boolean authenticateFromScratch(Activity activity) {
return authenticate(activity, true);
}
/**
* Logs into the Android Developer Console
*
* @param reuseAuthentication
* @throws DevConsoleException
*/
private boolean authenticate(Activity activity, boolean invalidateCredentials)
throws DevConsoleException {
if (invalidateCredentials) {
protocol.invalidateSessionCredentials();
}
if (protocol.hasSessionCredentials()) {
// nothing to do
return true;
}
SessionCredentials sessionCredentials = activity == null ? authenticator
.authenticateSilently(invalidateCredentials) : authenticator.authenticate(activity,
invalidateCredentials);
protocol.setSessionCredentials(sessionCredentials);
return protocol.hasSessionCredentials();
}
private String post(String url, String postData, String developerId) {
try {
HttpPost post = new HttpPost(url);
protocol.addHeaders(post, developerId);
post.setEntity(new StringEntity(postData, "UTF-8"));
if (DEBUG) {
CookieStore cookieStore = httpClient.getCookieStore();
List<Cookie> cookies = cookieStore.getCookies();
for (Cookie c : cookies) {
Log.d(TAG, String.format("****Cookie**** %s=%s", c.getName(), c.getValue()));
}
}
return httpClient.execute(post, responseHandler);
} catch (HttpResponseException e) {
if (e.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
throw new AuthenticationException(e);
}
throw new NetworkException(e, e.getStatusCode());
} catch (IOException e) {
throw new NetworkException(e);
}
}
public boolean canReplyToComments() {
return protocol.canReplyToComments();
}
public boolean hasSessionCredentials() {
return protocol.hasSessionCredentials();
}
}