/*
* Copyright 2015 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.examples.abelanav2.grpcclient;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.util.Log;
import android.widget.Toast;
import com.examples.abelanav2.AndroidConstants;
import com.examples.abelanav2.grpc.AbelanaGrpc;
import com.examples.abelanav2.grpc.DeletePhotoRequest;
import com.examples.abelanav2.grpc.EditPhotoRequest;
import com.examples.abelanav2.grpc.FlagRequest;
import com.examples.abelanav2.grpc.NewPhotoRequest;
import com.examples.abelanav2.grpc.Photo;
import com.examples.abelanav2.grpc.PhotoListRequest;
import com.examples.abelanav2.grpc.PhotoListResponse;
import com.examples.abelanav2.grpc.SignInRequest;
import com.examples.abelanav2.grpc.SignInResponse;
import com.examples.abelanav2.grpc.StatusResponse;
import com.examples.abelanav2.R;
import com.examples.abelanav2.grpc.UploadPhotoResponse;
import com.examples.abelanav2.grpc.VoteRequest;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.grpc.Channel;
import io.grpc.ChannelImpl;
import io.grpc.ClientInterceptors;
import io.grpc.transport.okhttp.OkHttpChannelBuilder;
/**
* The GRPC client to connect to the server.
*/
public class AbelanaClient {
/**
* The actual OkHttp channel implementation.
*/
private ChannelImpl mChannelImpl;
/**
* The gRPC stub used to make calls to the server.
*/
private AbelanaGrpc.AbelanaBlockingStub mBlockingStub;
/**
* The header client interceptor, to inject the Auth header.
*/
private AuthHeaderClientInterceptor mInterceptor;
/**
* The application context.
*/
private Context mContext;
/**
* Stores whether there is an active connection to the server or not.
*/
private boolean mConnected;
/**
* Stores the photo lists and next pages.
*/
private CacheStore mCacheStore;
/**
* The type of photo list to store.
*/
public enum PhotoListType {
/**
* The photo list for the personalized stream.
*/
PHOTO_LIST_STREAM,
/**
* The photo list for the my likes stream.
*/
PHOTO_LIST_LIKES,
/**
* The photo list for the my photos stream.
*/
PHOTO_LIST_MINE
}
/**
* The Abelana GRPC client constructor.
* @param context the application context.
*/
public AbelanaClient(final Context context) {
mContext = context;
mCacheStore = new CacheStore(context);
initServerConnection();
}
/**
* Initializes a connection to the gRPC server.
* @return a boolean indicating the success.
*/
private boolean initServerConnection() {
if(!mConnected) {
mInterceptor = new AuthHeaderClientInterceptor(
getUserIdToken());
try {
mChannelImpl = OkHttpChannelBuilder
.forAddress(AndroidConstants.HOST,
AndroidConstants.PORT)
.build();
Channel mOriginChannel = ClientInterceptors
.intercept(mChannelImpl, mInterceptor);
mBlockingStub = AbelanaGrpc.newBlockingStub(mOriginChannel);
mConnected = true;
} catch (RuntimeException e) {
mConnected = false;
}
}
return mConnected;
}
/**
* Checks if the client is connected to the server. If not, tries to connect
* and returns the result.
* @return a boolean indicating if the client is connected to the server.
*/
private boolean isConnectedToServerOrTryToConnect() {
return mConnected || initServerConnection();
}
/**
* Stops the communication between the client and the server.
*/
public void shutdown() {
mChannelImpl.shutdown();
mCacheStore.backup();
mConnected = false;
}
/**
* Signs the user in using a GitkitToken.
* @param gitkitToken the gitkitToken string.
* @return a boolean indicating that the user is successfully signed in.
* @throws AbelanaClientException if cannot connect to server or
* authenticate.
*/
public final boolean signIn(final String gitkitToken)
throws AbelanaClientException {
if (isConnectedToServerOrTryToConnect()) {
// Send request to the server
SignInRequest signInRequest = new SignInRequest();
signInRequest.gitkitToken = gitkitToken;
try {
SignInResponse signInResponse = mBlockingStub
.signIn(signInRequest);
// Save the user id token received
if (signInResponse.error == null) {
setUserIdToken(signInResponse.userToken);
return true;
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_authentication_error));
}
} catch (RuntimeException e) {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
}
/**
* Flags a photo as inappropriate.
* @param photoId the ID of the photo to flag.
* @return a boolean indicating that the photo was successfully flagged.
* @throws AbelanaClientException if cannot connect to server.
* @throws AbelanaClientException if cannot authenticate to server.
*/
public final boolean flagPhoto(final long photoId)
throws AbelanaClientException, AbelanaClientException {
if (isConnectedToServerOrTryToConnect()) {
FlagRequest flagRequest = new FlagRequest();
flagRequest.photoId = photoId;
try {
StatusResponse statusResponse = mBlockingStub
.flagPhoto(flagRequest);
if (statusResponse.error != null
&& statusResponse.error.code.equals("403")) {
throw new AbelanaClientException(mContext
.getString(R.string.abelana_auth_error_message)
);
}
return statusResponse.error == null;
} catch (RuntimeException e) {
Log.e("AbelanaClient", e.getMessage());
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
}
/**
* Deletes a photo.
* @param photoId the ID of the photo to delete.
* @return a boolean indicating that the photo was successfully deleted.
* @throws AbelanaClientException if cannot connect to server.
* @throws AbelanaClientException if cannot authenticate to server.
*/
public final boolean deletePhoto(final long photoId)
throws AbelanaClientException, AbelanaClientException {
if (isConnectedToServerOrTryToConnect()) {
DeletePhotoRequest deletePhotoRequest = new DeletePhotoRequest();
deletePhotoRequest.photoId = photoId;
try {
StatusResponse statusResponse = mBlockingStub
.deletePhoto(deletePhotoRequest);
if (statusResponse.error != null
&& statusResponse.error.code.equals("403")) {
throw new AbelanaClientException(mContext
.getString(R.string.abelana_auth_error_message)
);
}
return statusResponse.error == null;
} catch (RuntimeException e) {
Log.e("AbelanaClient", e.getMessage());
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
}
/**
* Edits a photo.
* @param photoId the ID of the photo to edit.
* @param description the new description of the photo.
* @return a boolean indicating that the photo was successfully edited.
* @throws AbelanaClientException if cannot connect to server.
* @throws AbelanaClientException if cannot authenticate to server.
*/
public final boolean editPhoto(final long photoId, String description)
throws AbelanaClientException, AbelanaClientException {
if (isConnectedToServerOrTryToConnect()) {
EditPhotoRequest editPhotoRequest = new EditPhotoRequest();
editPhotoRequest.photoId = photoId;
editPhotoRequest.description = description;
try {
StatusResponse statusResponse = mBlockingStub
.editPhoto(editPhotoRequest);
if (statusResponse.error != null
&& statusResponse.error.code.equals("403")) {
throw new AbelanaClientException(mContext
.getString(R.string.abelana_auth_error_message)
);
}
return statusResponse.error == null;
} catch (RuntimeException e) {
Log.e("AbelanaClient", e.getMessage());
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
}
/**
* Rates a photo.
* @param photoId the ID of the photo to rate.
* @param vote the vote for the photo: -1, 0 or 1.
* @return a boolean indicating that the photo was successfully rated.
* @throws AbelanaClientException if cannot connect to server.
* @throws AbelanaClientException if cannot authenticate to server.
*/
public final boolean votePhoto(final long photoId, final int vote)
throws AbelanaClientException, AbelanaClientException {
if (isConnectedToServerOrTryToConnect()) {
VoteRequest voteRequest = new VoteRequest();
voteRequest.photoId = photoId;
switch (vote) {
case -1:
voteRequest.vote = VoteRequest.THUMBS_DOWN;
break;
case 1:
voteRequest.vote = VoteRequest.THUMBS_UP;
break;
case 0:
default:
voteRequest.vote = VoteRequest.NEUTRAL;
break;
}
try {
StatusResponse statusResponse = mBlockingStub
.ratePhoto(voteRequest);
if (statusResponse.error != null
&& statusResponse.error.code.equals("403")) {
throw new AbelanaClientException(mContext
.getString(R.string.abelana_auth_error_message)
);
}
return statusResponse.error == null;
} catch (RuntimeException e) {
Log.e("AbelanaClient", e.getMessage());
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
}
/**
* Uploads a new photo.
* @param description the description of the photo.
* @param image the image bitmap
* @return a boolean indicating that the photo was successfully uploaded.
* @throws AbelanaClientException if cannot connect to server.
* @throws AbelanaClientException if cannot authenticate to server.
*/
public final boolean uploadPhoto(final String description,
final Bitmap image)
throws AbelanaClientException, AbelanaClientException {
if (isConnectedToServerOrTryToConnect()) {
NewPhotoRequest newPhotoRequest = new NewPhotoRequest();
newPhotoRequest.description = description;
try {
UploadPhotoResponse uploadPhotoResponse = mBlockingStub
.uploadPhoto(newPhotoRequest);
if (uploadPhotoResponse.error == null) {
CloudStorage.uploadImage(uploadPhotoResponse.uploadUrl,
image);
return true;
} else {
Log.e("AbelanaClient", uploadPhotoResponse.error.details);
if (uploadPhotoResponse.error.code.equals("403")) {
throw new AbelanaClientException(mContext
.getString(R.string.abelana_auth_error_message)
);
}
return false;
}
} catch (IOException e) {
Log.e("AbelanaClient", e.getMessage());
throw new AbelanaClientException(mContext
.getString(R.string.upload_photo_failed));
}
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
}
/**
* Gets the photo list.
* @param photoListType the kind of photo list.
* @param nextPage if we want the next page. Gets new results if false.
* @return The updated list of photos.
* @throws AbelanaClientException if cannot connect to server.
* @throws AbelanaClientException if cannot authenticate to server.
*/
public final List<PhotoInfo> getPhotoList(final PhotoListType photoListType,
final boolean nextPage)
throws AbelanaClientException, AbelanaClientException {
if (isConnectedToServerOrTryToConnect()) {
PhotoListRequest photoListRequest = new PhotoListRequest();
if (nextPage) {
photoListRequest.pageNumber = mCacheStore
.getNextPage(photoListType);
} else {
photoListRequest.pageNumber = 0;
}
try {
PhotoListResponse photoListResponse;
switch (photoListType) {
case PHOTO_LIST_LIKES:
photoListResponse = mBlockingStub
.listMyLikes(photoListRequest);
break;
case PHOTO_LIST_MINE:
photoListResponse = mBlockingStub
.listMyPhotos(photoListRequest);
break;
case PHOTO_LIST_STREAM:
default:
photoListResponse = mBlockingStub
.photoStream(photoListRequest);
break;
}
if (photoListResponse.error == null) {
mCacheStore.setNextPage(photoListType,
photoListResponse.nextPage);
Log.i("AbelanaClient", "Next page=" + photoListResponse
.nextPage);
if (!nextPage) {
mCacheStore.getPhotoList(photoListType).clear();
}
for(Photo p : photoListResponse.photo) {
mCacheStore.addPhotoToList(photoListType,
new PhotoInfo(p.url, p.photoId, p.userId,
p.date, p.description, p.rating));
}
return mCacheStore.getPhotoList(photoListType);
} else {
Log.e("AbelanaClient", photoListResponse.error.details);
if (photoListResponse.error.code.equals("403")) {
throw new AbelanaClientException(mContext
.getString(R.string.abelana_auth_error_message)
);
}
throw new AbelanaClientException(mContext.getString(R.string
.server_error));
}
} catch (RuntimeException e) {
Log.e("AbelanaClient", e.getMessage());
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
} else {
throw new AbelanaClientException(mContext.getString(R.string
.server_connection_error));
}
}
/**
* Returns a boolean indicating if there are more pages to load.
* @param photoListType the kind of photo list.
* @return true if there are more pages
*/
public final boolean hasMorePages(PhotoListType photoListType) {
return !(mCacheStore.getNextPage(photoListType) == 0);
}
/**
* Uploads a new photo.
* @param photoListType the kind of photo list.
* @return The updated list of photos.
*/
public final List<PhotoInfo> getPhotoListFromCache(
final PhotoListType photoListType) {
return mCacheStore.getPhotoList(photoListType);
}
/**
* @return a boolean indicating if the user is signed in or not.
*/
public boolean isSignedIn() {
return getUserIdToken() != null && !getUserIdToken().equals("");
}
/**
* Signs the user out.
* @throws AbelanaClientException if impossible to sign the user out.
*/
public void signOut() throws AbelanaClientException {
setUserIdToken(null);
}
/**
* Returns the user ID token from SharedPreferences.
* @return the user ID token.
*/
private final String getUserIdToken() {
SharedPreferences settings = mContext.getSharedPreferences(
AndroidConstants.SHARED_PREFS_USER_ID, 0);
return settings.getString(AndroidConstants.SHARED_PREFS_KEY_USER_ID,
null);
}
/**
* Sets the user ID token in SharedPreferences.
* @param userIdToken the user id token to save.
* @throws AbelanaClientException if cannot save authentication
*/
private final void setUserIdToken(final String userIdToken)
throws AbelanaClientException {
SharedPreferences settings = mContext.getSharedPreferences(
AndroidConstants.SHARED_PREFS_USER_ID, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putString(AndroidConstants.SHARED_PREFS_KEY_USER_ID,
userIdToken);
if (!editor.commit()) {
Log.e("AbelanaClient", "Could not save the user id token.");
throw new AbelanaClientException(mContext.getString(R.string
.server_authentication_error));
}
mInterceptor.setUserIdToken(userIdToken);
}
}