Merge pull request #13730 from Cristo86/android_friends_main

Android - Friends in home screen menu
This commit is contained in:
John Conklin II 2018-08-17 16:58:45 -07:00 committed by GitHub
commit c8addd0581
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1037 additions and 14 deletions

View file

@ -145,5 +145,7 @@ dependencies {
compile 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.picasso:picasso:2.71828'
compile 'com.sothree.slidinguppanel:library:3.4.0'
implementation fileTree(include: ['*.jar'], dir: 'libs')
}

View file

@ -209,6 +209,11 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeGotoUr
DependencyManager::get<AddressManager>()->loadSettings(jniUrl.toString());
}
JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeGoToUser(JNIEnv* env, jobject obj, jstring username) {
QAndroidJniObject jniUsername("java/lang/String", "(Ljava/lang/String;)V", username);
DependencyManager::get<AddressManager>()->goToUser(jniUsername.toString(), false);
}
JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnPause(JNIEnv* env, jobject obj) {
}
@ -285,6 +290,18 @@ Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *en
Q_ARG(const QString&, username), Q_ARG(const QString&, password));
}
JNIEXPORT jboolean JNICALL
Java_io_highfidelity_hifiinterface_fragment_FriendsFragment_nativeIsLoggedIn(JNIEnv *env, jobject instance) {
auto accountManager = DependencyManager::get<AccountManager>();
return accountManager->isLoggedIn();
}
JNIEXPORT jstring JNICALL
Java_io_highfidelity_hifiinterface_fragment_FriendsFragment_nativeGetAccessToken(JNIEnv *env, jobject instance) {
auto accountManager = DependencyManager::get<AccountManager>();
return env->NewStringUTF(accountManager->getAccountInfo().getAccessToken().token.toLatin1().data());
}
JNIEXPORT void JNICALL
Java_io_highfidelity_hifiinterface_SplashActivity_registerLoadCompleteListener(JNIEnv *env,
jobject instance) {

View file

@ -48,6 +48,7 @@ import com.google.vr.ndk.base.GvrApi;*/
public class InterfaceActivity extends QtActivity implements WebViewFragment.OnWebViewInteractionListener {
public static final String DOMAIN_URL = "url";
public static final String EXTRA_GOTO_USERNAME = "gotousername";
private static final String TAG = "Interface";
private static final int WEB_DRAWER_RIGHT_MARGIN = 262;
private static final int WEB_DRAWER_BOTTOM_MARGIN = 150;
@ -59,6 +60,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
private native long nativeOnCreate(InterfaceActivity instance, AssetManager assetManager);
private native void nativeOnDestroy();
private native void nativeGotoUrl(String url);
private native void nativeGoToUser(String username);
private native void nativeBeforeEnterBackground();
private native void nativeEnterBackground();
private native void nativeEnterForeground();
@ -280,6 +282,9 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
if (intent.hasExtra(DOMAIN_URL)) {
webSlidingDrawer.setVisibility(View.GONE);
nativeGotoUrl(intent.getStringExtra(DOMAIN_URL));
} else if (intent.hasExtra(EXTRA_GOTO_USERNAME)) {
webSlidingDrawer.setVisibility(View.GONE);
nativeGoToUser(intent.getStringExtra(EXTRA_GOTO_USERNAME));
}
}

View file

@ -29,6 +29,7 @@ import android.widget.TextView;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import io.highfidelity.hifiinterface.fragment.FriendsFragment;
import io.highfidelity.hifiinterface.fragment.HomeFragment;
import io.highfidelity.hifiinterface.fragment.LoginFragment;
import io.highfidelity.hifiinterface.fragment.PolicyFragment;
@ -36,7 +37,8 @@ import io.highfidelity.hifiinterface.task.DownloadProfileImageTask;
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener,
LoginFragment.OnLoginInteractionListener,
HomeFragment.OnHomeInteractionListener {
HomeFragment.OnHomeInteractionListener,
FriendsFragment.OnHomeInteractionListener {
private static final int PROFILE_PICTURE_PLACEHOLDER = R.drawable.default_profile_avatar;
public static final String DEFAULT_FRAGMENT = "Home";
@ -56,6 +58,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
private View mLoginPanel;
private View mProfilePanel;
private TextView mLogoutOption;
private MenuItem mPeopleMenuItem;
private boolean backToScene;
@ -75,6 +78,8 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
mDisplayName = mNavigationView.getHeaderView(0).findViewById(R.id.displayName);
mProfilePicture = mNavigationView.getHeaderView(0).findViewById(R.id.profilePicture);
mPeopleMenuItem = mNavigationView.getMenu().findItem(R.id.action_people);
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar.setTitleTextAppearance(this, R.style.HomeActionBarTitleStyle);
setSupportActionBar(toolbar);
@ -109,40 +114,69 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
loadLoginFragment();
break;
case "Home":
loadHomeFragment();
loadHomeFragment(true);
break;
case "Privacy Policy":
loadPrivacyPolicyFragment();
break;
case "People":
loadPeopleFragment();
break;
default:
Log.e(TAG, "Unknown fragment " + fragment);
}
}
private void loadHomeFragment() {
private void loadHomeFragment(boolean addToBackStack) {
Fragment fragment = HomeFragment.newInstance();
loadFragment(fragment, getString(R.string.home), false);
loadFragment(fragment, getString(R.string.home), getString(R.string.tagFragmentHome), addToBackStack);
}
private void loadLoginFragment() {
Fragment fragment = LoginFragment.newInstance();
loadFragment(fragment, getString(R.string.login), true);
loadFragment(fragment, getString(R.string.login), getString(R.string.tagFragmentLogin), true);
}
private void loadPrivacyPolicyFragment() {
Fragment fragment = PolicyFragment.newInstance();
loadFragment(fragment, getString(R.string.privacyPolicy), true);
loadFragment(fragment, getString(R.string.privacyPolicy), getString(R.string.tagFragmentPolicy), true);
}
private void loadFragment(Fragment fragment, String title, boolean addToBackStack) {
private void loadPeopleFragment() {
Fragment fragment = FriendsFragment.newInstance();
loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true);
}
private void loadFragment(Fragment fragment, String title, String tag, boolean addToBackStack) {
FragmentManager fragmentManager = getFragmentManager();
// check if it's the same fragment
String currentFragmentName = fragmentManager.getBackStackEntryCount() > 0
? fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount() - 1).getName()
: "";
if (currentFragmentName.equals(title)) {
mDrawerLayout.closeDrawer(mNavigationView);
return; // cancel as we are already in that fragment
}
// go back until first transaction
int backStackEntryCount = fragmentManager.getBackStackEntryCount();
for (int i = 0; i < backStackEntryCount - 1; i++) {
fragmentManager.popBackStackImmediate();
}
// this case is when we wanted to go home.. rollback already did that!
// But asking for a new Home fragment makes it easier to have an updated list so we let it to continue
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.replace(R.id.content_frame, fragment, tag);
if (addToBackStack) {
ft.addToBackStack(null);
ft.addToBackStack(title);
}
ft.commit();
setTitle(title);
@ -155,11 +189,13 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
mLoginPanel.setVisibility(View.GONE);
mProfilePanel.setVisibility(View.VISIBLE);
mLogoutOption.setVisibility(View.VISIBLE);
mPeopleMenuItem.setVisible(true);
updateProfileHeader();
} else {
mLoginPanel.setVisibility(View.VISIBLE);
mProfilePanel.setVisibility(View.GONE);
mLogoutOption.setVisibility(View.GONE);
mPeopleMenuItem.setVisible(false);
mDisplayName.setText("");
}
}
@ -200,7 +236,10 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch(item.getItemId()) {
case R.id.action_home:
loadHomeFragment();
loadHomeFragment(false);
return true;
case R.id.action_people:
loadPeopleFragment();
return true;
}
return false;
@ -219,6 +258,19 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
public void onLogoutClicked(View view) {
nativeLogout();
updateLoginMenu();
exitLoggedInFragment();
}
private void exitLoggedInFragment() {
// If we are in a "logged in" fragment (like People), go back to home. This could be expanded to multiple fragments
FragmentManager fragmentManager = getFragmentManager();
String currentFragmentName = fragmentManager.getBackStackEntryCount() > 0
? fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount() - 1).getName()
: "";
if (currentFragmentName.equals(getString(R.string.people))) {
loadHomeFragment(false);
}
}
public void onSelectedDomain(String domainUrl) {
@ -237,9 +289,17 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
startActivity(intent);
}
private void goToUser(String username) {
Intent intent = new Intent(this, InterfaceActivity.class);
intent.putExtra(InterfaceActivity.EXTRA_GOTO_USERNAME, username);
finish();
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
}
@Override
public void onLoginCompleted() {
loadHomeFragment();
loadHomeFragment(false);
updateLoginMenu();
if (backToScene) {
backToScene = false;
@ -266,6 +326,11 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
loadPrivacyPolicyFragment();
}
@Override
public void onVisitUserSelected(String username) {
goToUser(username);
}
private class RoundProfilePictureCallback implements Callback {
@Override
public void onSuccess() {
@ -284,15 +349,30 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
@Override
public void onBackPressed() {
int index = getFragmentManager().getBackStackEntryCount() - 1;
if (index > -1) {
// if a fragment needs to internally manage back presses..
FragmentManager fm = getFragmentManager();
Log.d("[BACK]", "getBackStackEntryCount " + fm.getBackStackEntryCount());
Fragment friendsFragment = fm.findFragmentByTag(getString(R.string.tagFragmentPeople));
if (friendsFragment != null && friendsFragment instanceof FriendsFragment) {
if (((FriendsFragment) friendsFragment).onBackPressed()) {
return;
}
}
int index = fm.getBackStackEntryCount() - 1;
if (index > 0) {
super.onBackPressed();
index--;
if (index > -1) {
setTitle(fm.getBackStackEntryAt(index).getName());
}
if (backToScene) {
backToScene = false;
goToLastLocation();
}
} else {
finishAffinity();
finishAffinity();
}
}

View file

@ -0,0 +1,193 @@
package io.highfidelity.hifiinterface.fragment;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import io.highfidelity.hifiinterface.R;
import io.highfidelity.hifiinterface.provider.EndpointUsersProvider;
import io.highfidelity.hifiinterface.provider.UsersProvider;
import io.highfidelity.hifiinterface.view.UserListAdapter;
public class FriendsFragment extends Fragment {
public native boolean nativeIsLoggedIn();
public native String nativeGetAccessToken();
private RecyclerView mUsersView;
private View mUserActions;
private UserListAdapter mUsersAdapter;
private SlidingUpPanelLayout mSlidingUpPanelLayout;
private EndpointUsersProvider mUsersProvider;
private String mSelectedUsername;
private OnHomeInteractionListener mListener;
private SwipeRefreshLayout mSwipeRefreshLayout;
public FriendsFragment() {
// Required empty public constructor
}
public static FriendsFragment newInstance() {
FriendsFragment fragment = new FriendsFragment();
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_friends, container, false);
String accessToken = nativeGetAccessToken();
mUsersProvider = new EndpointUsersProvider(accessToken);
Log.d("[USERS]", "token : [" + accessToken + "]");
mSwipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
mUsersView = rootView.findViewById(R.id.rvUsers);
int numberOfColumns = 1;
GridLayoutManager gridLayoutMgr = new GridLayoutManager(getContext(), numberOfColumns);
mUsersView.setLayoutManager(gridLayoutMgr);
mUsersAdapter = new UserListAdapter(getContext(), mUsersProvider);
mSwipeRefreshLayout.setRefreshing(true);
mUserActions = rootView.findViewById(R.id.userActionsLayout);
mSlidingUpPanelLayout = rootView.findViewById(R.id.sliding_layout);
mSlidingUpPanelLayout.setPanelHeight(0);
rootView.findViewById(R.id.userActionDelete).setOnClickListener(view -> onRemoveConnectionClick());
rootView.findViewById(R.id.userActionVisit).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mListener != null && mSelectedUsername != null) {
mListener.onVisitUserSelected(mSelectedUsername);
}
}
});
mUsersAdapter.setClickListener(new UserListAdapter.ItemClickListener() {
@Override
public void onItemClick(View view, int position, UserListAdapter.User user) {
// 1. 'select' user
mSelectedUsername = user.name;
// ..
// 2. adapt options
// ..
rootView.findViewById(R.id.userActionVisit).setVisibility(user.online ? View.VISIBLE : View.GONE);
// 3. show
mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED);
}
});
mUsersAdapter.setListener(new UserListAdapter.AdapterListener() {
@Override
public void onEmptyAdapter() {
mSwipeRefreshLayout.setRefreshing(false);
}
@Override
public void onNonEmptyAdapter() {
mSwipeRefreshLayout.setRefreshing(false);
}
@Override
public void onError(Exception e, String message) {
mSwipeRefreshLayout.setRefreshing(false);
}
});
mUsersView.setAdapter(mUsersAdapter);
mSlidingUpPanelLayout.setFadeOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
mSelectedUsername = null;
}
});
mSwipeRefreshLayout.setOnRefreshListener(() -> mUsersAdapter.loadUsers());
return rootView;
}
private void onRemoveConnectionClick() {
if (mSelectedUsername == null) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Remove '" + mSelectedUsername + "' from People?");
builder.setPositiveButton("Remove", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mUsersProvider.removeConnection(mSelectedUsername, new UsersProvider.UserActionCallback() {
@Override
public void requestOk() {
mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
mSelectedUsername = null;
mUsersAdapter.loadUsers();
}
@Override
public void requestError(Exception e, String message) {
// CLD: Show error message?
}
});
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// Cancelled, nothing to do
}
});
builder.show();
}
/**
* Processes the back pressed event and returns true if it was managed by this Fragment
* @return
*/
public boolean onBackPressed() {
if (mSlidingUpPanelLayout.getPanelState().equals(SlidingUpPanelLayout.PanelState.EXPANDED)) {
mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
mSelectedUsername = null;
return true;
} else {
return false;
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnHomeInteractionListener) {
mListener = (OnHomeInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnHomeInteractionListener");
}
}
public interface OnHomeInteractionListener {
void onVisitUserSelected(String username);
}
}

View file

@ -0,0 +1,225 @@
package io.highfidelity.hifiinterface.provider;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import io.highfidelity.hifiinterface.view.UserListAdapter;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
/**
* Created by cduarte on 6/13/18.
*/
public class EndpointUsersProvider implements UsersProvider {
public static final String BASE_URL = "https://metaverse.highfidelity.com/";
private final Retrofit mRetrofit;
private final EndpointUsersProviderService mEndpointUsersProviderService;
public EndpointUsersProvider(String accessToken) {
mRetrofit = createAuthorizedRetrofit(accessToken);
mEndpointUsersProviderService = mRetrofit.create(EndpointUsersProviderService.class);
}
private Retrofit createAuthorizedRetrofit(String accessToken) {
Retrofit mRetrofit;
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + accessToken)
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
});
OkHttpClient client = httpClient.build();
mRetrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
return mRetrofit;
}
@Override
public void retrieve(UsersCallback usersCallback) {
Call<UsersResponse> friendsCall = mEndpointUsersProviderService.getUsers(
CONNECTION_FILTER_CONNECTIONS,
400,
null);
friendsCall.enqueue(new Callback<UsersResponse>() {
@Override
public void onResponse(Call<UsersResponse> call, retrofit2.Response<UsersResponse> response) {
if (!response.isSuccessful()) {
usersCallback.retrieveError(new Exception("Error calling Users API"), "Error calling Users API");
return;
}
UsersResponse usersResponse = response.body();
List<UserListAdapter.User> adapterUsers = new ArrayList<>(usersResponse.total_entries);
for (User user : usersResponse.data.users) {
UserListAdapter.User adapterUser = new UserListAdapter.User();
adapterUser.connection = user.connection;
adapterUser.imageUrl = user.images.thumbnail;
adapterUser.name = user.username;
adapterUser.online = user.online;
adapterUser.locationName = (user.location != null ?
(user.location.root != null ? user.location.root.name :
(user.location.domain != null ? user.location.domain.name : ""))
: "");
adapterUsers.add(adapterUser);
}
usersCallback.retrieveOk(adapterUsers);
}
@Override
public void onFailure(Call<UsersResponse> call, Throwable t) {
usersCallback.retrieveError(new Exception(t), "Error calling Users API");
}
});
}
public class UserActionRetrofitCallback implements Callback<UsersResponse> {
UserActionCallback callback;
public UserActionRetrofitCallback(UserActionCallback callback) {
this.callback = callback;
}
@Override
public void onResponse(Call<UsersResponse> call, retrofit2.Response<UsersResponse> response) {
if (!response.isSuccessful()) {
callback.requestError(new Exception("Error with "
+ call.request().url().toString() + " "
+ call.request().method() + " call " + response.message()),
response.message());
return;
}
if (response.body() == null || !"success".equals(response.body().status)) {
callback.requestError(new Exception("Error with "
+ call.request().url().toString() + " "
+ call.request().method() + " call " + response.message()),
response.message());
return;
}
callback.requestOk();
}
@Override
public void onFailure(Call<UsersResponse> call, Throwable t) {
callback.requestError(new Exception(t), t.getMessage());
}
}
@Override
public void addFriend(String friendUserName, UserActionCallback callback) {
Call<UsersResponse> friendCall = mEndpointUsersProviderService.addFriend(new BodyAddFriend(friendUserName));
friendCall.enqueue(new UserActionRetrofitCallback(callback));
}
@Override
public void removeFriend(String friendUserName, UserActionCallback callback) {
Call<UsersResponse> friendCall = mEndpointUsersProviderService.removeFriend(friendUserName);
friendCall.enqueue(new UserActionRetrofitCallback(callback));
}
@Override
public void removeConnection(String connectionUserName, UserActionCallback callback) {
Call<UsersResponse> connectionCall = mEndpointUsersProviderService.removeConnection(connectionUserName);
connectionCall.enqueue(new UserActionRetrofitCallback(callback));
}
public interface EndpointUsersProviderService {
@GET("api/v1/users")
Call<UsersResponse> getUsers(@Query("filter") String filter,
@Query("per_page") int perPage,
@Query("online") Boolean online);
@DELETE("api/v1/user/connections/{connectionUserName}")
Call<UsersResponse> removeConnection(@Path("connectionUserName") String connectionUserName);
@DELETE("api/v1/user/friends/{friendUserName}")
Call<UsersResponse> removeFriend(@Path("friendUserName") String friendUserName);
@POST("api/v1/user/friends")
Call<UsersResponse> addFriend(@Body BodyAddFriend friendUserName);
/* response
{
"status": "success"
}
*/
}
class BodyAddFriend {
String username;
public BodyAddFriend(String username) {
this.username = username;
}
}
class UsersResponse {
public UsersResponse() {}
String status;
int current_page;
int total_pages;
int per_page;
int total_entries;
Data data;
}
class Data {
public Data() {}
List<User> users;
}
class User {
public User() {}
String username;
boolean online;
String connection;
Images images;
LocationData location;
}
class Images {
public Images() {}
String hero;
String thumbnail;
String tiny;
}
class LocationData {
public LocationData() {}
NameContainer root;
NameContainer domain;
}
class NameContainer {
String name;
}
}

View file

@ -0,0 +1,35 @@
package io.highfidelity.hifiinterface.provider;
import java.util.List;
import io.highfidelity.hifiinterface.view.UserListAdapter;
/**
* Created by cduarte on 6/13/18.
*/
public interface UsersProvider {
public static String CONNECTION_TYPE_FRIEND = "friend";
public static String CONNECTION_FILTER_CONNECTIONS = "connections";
void retrieve(UsersProvider.UsersCallback usersCallback);
interface UsersCallback {
void retrieveOk(List<UserListAdapter.User> users);
void retrieveError(Exception e, String message);
}
void addFriend(String friendUserName, UserActionCallback callback);
void removeFriend(String friendUserName, UserActionCallback callback);
void removeConnection(String connectionUserName, UserActionCallback callback);
interface UserActionCallback {
void requestOk();
void requestError(Exception e, String message);
}
}

View file

@ -0,0 +1,247 @@
package io.highfidelity.hifiinterface.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import java.util.ArrayList;
import java.util.List;
import io.highfidelity.hifiinterface.R;
import io.highfidelity.hifiinterface.provider.UsersProvider;
/**
* Created by cduarte on 6/13/18.
*/
public class UserListAdapter extends RecyclerView.Adapter<UserListAdapter.ViewHolder> {
private UsersProvider mProvider;
private LayoutInflater mInflater;
private Context mContext;
private List<User> mUsers = new ArrayList<>();
private ItemClickListener mClickListener;
private AdapterListener mAdapterListener;
public UserListAdapter(Context c, UsersProvider usersProvider) {
mContext = c;
mInflater = LayoutInflater.from(mContext);
mProvider = usersProvider;
loadUsers();
}
public void setListener(AdapterListener adapterListener) {
mAdapterListener = adapterListener;
}
public void loadUsers() {
mProvider.retrieve(new UsersProvider.UsersCallback() {
@Override
public void retrieveOk(List<User> users) {
mUsers = new ArrayList<>(users);
notifyDataSetChanged();
if (mAdapterListener != null) {
if (mUsers.isEmpty()) {
mAdapterListener.onEmptyAdapter();
} else {
mAdapterListener.onNonEmptyAdapter();
}
}
}
@Override
public void retrieveError(Exception e, String message) {
Log.e("[USERS]", message, e);
if (mAdapterListener != null) {
mAdapterListener.onError(e, message);
}
}
});
}
@Override
public UserListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mInflater.inflate(R.layout.user_item, parent, false);
return new UserListAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(UserListAdapter.ViewHolder holder, int position) {
User aUser = mUsers.get(position);
holder.mUsername.setText(aUser.name);
holder.mOnlineInfo.setVisibility(aUser.online? View.VISIBLE : View.GONE);
holder.mLocation.setText("- " + aUser.locationName); // Bring info from the API and use it here
holder.mFriendStar.onBindSet(aUser.name, aUser.connection.equals(UsersProvider.CONNECTION_TYPE_FRIEND));
Uri uri = Uri.parse(aUser.imageUrl);
Picasso.get().load(uri).into(holder.mImage, new RoundProfilePictureCallback(holder.mImage));
}
private class RoundProfilePictureCallback implements Callback {
private ImageView mProfilePicture;
public RoundProfilePictureCallback(ImageView imageView) {
mProfilePicture = imageView;
}
@Override
public void onSuccess() {
Bitmap imageBitmap = ((BitmapDrawable) mProfilePicture.getDrawable()).getBitmap();
RoundedBitmapDrawable imageDrawable = RoundedBitmapDrawableFactory.create(mProfilePicture.getContext().getResources(), imageBitmap);
imageDrawable.setCircular(true);
imageDrawable.setCornerRadius(Math.max(imageBitmap.getWidth(), imageBitmap.getHeight()) / 2.0f);
mProfilePicture.setImageDrawable(imageDrawable);
}
@Override
public void onError(Exception e) {
mProfilePicture.setImageResource(R.drawable.default_profile_avatar);
}
}
@Override
public int getItemCount() {
return mUsers.size();
}
public class ToggleWrapper {
private ViewGroup mFrame;
private ImageView mImage;
private boolean mChecked = false;
private String mUsername;
private boolean waitingChangeConfirm = false;
public ToggleWrapper(ViewGroup toggleFrame) {
mFrame = toggleFrame;
mImage = toggleFrame.findViewById(R.id.userFavImage);
mFrame.setOnClickListener(view -> toggle());
}
private void refreshUI() {
mImage.setColorFilter(ContextCompat.getColor(mImage.getContext(),
mChecked ? R.color.starSelectedTint : R.color.starUnselectedTint));
}
class RollbackUICallback implements UsersProvider.UserActionCallback {
boolean previousStatus;
RollbackUICallback(boolean previousStatus) {
this.previousStatus = previousStatus;
}
@Override
public void requestOk() {
if (!waitingChangeConfirm) {
return;
}
mFrame.setClickable(true);
// nothing to do, new status was set
}
@Override
public void requestError(Exception e, String message) {
if (!waitingChangeConfirm) {
return;
}
// new status was not set, rolling back
mChecked = previousStatus;
mFrame.setClickable(true);
refreshUI();
}
}
protected void toggle() {
// TODO API CALL TO CHANGE
final boolean previousStatus = mChecked;
mChecked = !mChecked;
mFrame.setClickable(false);
refreshUI();
waitingChangeConfirm = true;
if (mChecked) {
mProvider.addFriend(mUsername, new RollbackUICallback(previousStatus));
} else {
mProvider.removeFriend(mUsername, new RollbackUICallback(previousStatus));
}
}
protected void onBindSet(String username, boolean checked) {
mChecked = checked;
mUsername = username;
waitingChangeConfirm = false;
mFrame.setClickable(true);
refreshUI();
}
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView mUsername;
TextView mOnline;
View mOnlineInfo;
TextView mLocation;
ImageView mImage;
ToggleWrapper mFriendStar;
public ViewHolder(View itemView) {
super(itemView);
mUsername = itemView.findViewById(R.id.userName);
mOnline = itemView.findViewById(R.id.userOnline);
mImage = itemView.findViewById(R.id.userImage);
mOnlineInfo = itemView.findViewById(R.id.userOnlineInfo);
mLocation = itemView.findViewById(R.id.userLocation);
mFriendStar = new ToggleWrapper(itemView.findViewById(R.id.userFav));
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
int position = getAdapterPosition();
if (mClickListener != null) {
mClickListener.onItemClick(view, position, mUsers.get(position));
}
}
}
// allows clicks events to be caught
public void setClickListener(ItemClickListener itemClickListener) {
this.mClickListener = itemClickListener;
}
public interface ItemClickListener {
void onItemClick(View view, int position, User user);
}
public static class User {
public String name;
public String imageUrl;
public String connection;
public boolean online;
public String locationName;
public User() {}
}
public interface AdapterListener {
void onEmptyAdapter();
void onNonEmptyAdapter();
void onError(Exception e, String message);
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View file

@ -0,0 +1,4 @@
<vector android:height="31dp" android:viewportHeight="25.0"
android:viewportWidth="27.0" android:width="31dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FBD92A" android:pathData="M12.549,0.927C12.848,0.006 14.152,0.006 14.451,0.927L16.756,8.019C16.889,8.431 17.273,8.71 17.706,8.71H25.164C26.132,8.71 26.535,9.95 25.751,10.519L19.719,14.903C19.368,15.157 19.221,15.608 19.355,16.021L21.66,23.113C21.959,24.034 20.904,24.8 20.121,24.231L14.088,19.847C13.737,19.593 13.263,19.593 12.912,19.847L6.879,24.231C6.096,24.8 5.041,24.034 5.34,23.113L7.645,16.021C7.779,15.608 7.632,15.157 7.282,14.903L1.249,10.519C0.465,9.95 0.868,8.71 1.836,8.71H9.293C9.727,8.71 10.111,8.431 10.245,8.019L12.549,0.927Z"/>
</vector>

View file

@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M0.6,3h22.8v18.7h-22.8z"
android:fillAlpha="0"/>
<path
android:fillColor="#FF000000"
android:pathData="M0.6,3h16.3v18.7h-16.3z"
android:fillAlpha="0"/>
<path
android:fillColor="#FF000000"
android:pathData="M0.6,3h16.3v18.7h-16.3z"
android:fillAlpha="0"/>
<path
android:fillColor="#FF000000"
android:pathData="M13.8,9.9h9.6v7.8h-9.6z"
android:fillAlpha="0"/>
<path
android:pathData="M11.9,16.9c-0.2,-0.9 -0.3,-2.3 -0.4,-3.4c-0.1,-0.7 -0.1,-1.3 -0.2,-1.7c0,-0.1 -0.1,-0.3 0.3,-0.4c0.1,0 0.1,0 0.2,-0.1l4.4,-1.7c0.3,-0.1 0.5,-0.4 0.6,-0.7c0.1,-0.3 0.1,-0.7 -0.2,-0.9L16.6,8c-0.2,-0.2 -0.5,-0.3 -0.8,-0.3c-0.1,0 -4.8,0.7 -6.8,0.7c-0.1,0 -0.1,0 -0.1,0c-2,0 -6.9,-0.8 -7,-0.8c-0.4,-0.1 -0.8,0.1 -1,0.4L0.7,8.3C0.6,8.5 0.6,8.8 0.6,9.1c0.1,0.3 0.3,0.5 0.5,0.6C2,10 5,11.2 5.9,11.3c0.2,0 0.4,0.1 0.5,0.6c0.1,0.6 -0.2,3.6 -0.6,5c-0.4,1.4 -1,3.2 -1,3.2c-0.2,0.5 0.1,1 0.6,1.2l0.6,0.2c0.2,0.1 0.5,0.1 0.7,-0.1c0.2,-0.1 0.4,-0.3 0.5,-0.6l1.7,-5l1.6,5.1c0.1,0.3 0.3,0.5 0.5,0.6c0.1,0.1 0.3,0.1 0.4,0.1c0.1,0 0.2,0 0.3,-0.1l0.5,-0.2c0.4,-0.2 0.7,-0.6 0.6,-1.1C12.8,20.3 12.3,18.5 11.9,16.9z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M8.9,7.5c1.3,0 2.3,-1 2.3,-2.3S10.2,3 8.9,3S6.6,4 6.6,5.3S7.7,7.5 8.9,7.5z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M23,13.4L22.6,13c0,0 0,0 0,0l-2.9,-2.8c-0.2,-0.2 -0.5,-0.2 -0.7,0l-0.7,0.7c-0.2,0.2 -0.2,0.5 0,0.7l1.2,1.2h-5.2c-0.3,0 -0.5,0.2 -0.5,0.5v0.9c0,0.3 0.2,0.5 0.5,0.5h5.1l-1.2,1.1c-0.2,0.2 -0.2,0.5 0,0.7l0.7,0.7c0.2,0.2 0.5,0.2 0.7,0l3.3,-3.2C23.2,13.9 23.2,13.6 23,13.4z"
android:fillColor="#FFFFFF"/>
</vector>

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<com.sothree.slidinguppanel.SlidingUpPanelLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sliding_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
app:umanoFadeColor="@color/slidingUpPanelFadeColor"
app:umanoShadowHeight="4dp"
android:background="@color/backgroundLight">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rvUsers"
android:paddingTop="@dimen/list_vertical_padding"
android:paddingBottom="@dimen/list_vertical_padding"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
<LinearLayout
android:id="@+id/userActionsLayout"
android:layout_width="match_parent"
android:layout_height="270dp"
android:orientation="vertical"
android:background="@color/backgroundDark">
<android.support.constraint.ConstraintLayout
android:id="@+id/userActionVisit"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground">
<ImageView android:id="@+id/userActionVisitIcon"
android:layout_width="16dp"
android:layout_height="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:src="@drawable/ic_teleporticon"
android:tint="@color/white_opaque" />
<TextView android:id="@+id/userActionVisitText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Visit In-World"
android:fontFamily="@font/raleway"
android:textColor="@color/white_opaque"
app:layout_constraintStart_toEndOf="@id/userActionVisitIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="32dp" />
</android.support.constraint.ConstraintLayout>
<android.support.constraint.ConstraintLayout
android:id="@+id/userActionDelete"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground">
<ImageView android:id="@+id/userActionDeleteIcon"
android:layout_width="16dp"
android:layout_height="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:src="@drawable/ic_delete_black_24dp"
android:tint="@color/white_opaque" />
<TextView android:id="@+id/userActionDeleteText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Remove from People"
android:fontFamily="@font/raleway"
android:textColor="@color/white_opaque"
app:layout_constraintStart_toEndOf="@id/userActionDeleteIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="32dp" />
</android.support.constraint.ConstraintLayout>
</LinearLayout>
</com.sothree.slidinguppanel.SlidingUpPanelLayout>

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?attr/selectableItemBackground"
android:clickable="true">
<ImageView
android:id="@+id/userImage"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="@dimen/activity_horizontal_margin"
app:layout_constraintStart_toStartOf="parent"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="@dimen/activity_horizontal_margin"
app:layout_constraintStart_toEndOf="@id/userImage"
android:orientation="vertical">
<TextView
android:id="@+id/userName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/raleway"
android:textColor="@color/menuOption"/>
<LinearLayout android:id="@+id/userOnlineInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/userOnline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/online"
android:fontFamily="@font/raleway"
android:textColor="@color/hifiAquamarine" />
<TextView
android:id="@+id/userLocation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:fontFamily="@font/raleway_italic"
android:textColor="@color/menuOption"/>
</LinearLayout>
</LinearLayout>
<RelativeLayout android:id="@+id/userFav"
android:layout_width="48dp"
android:layout_height="48dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="5.5dp">
<ImageView android:id="@+id/userFavImage"
android:layout_width="27dp"
android:layout_height="27dp"
android:src="@drawable/ic_star"
android:tint="@color/starUnselectedTint"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_centerInParent="true"
android:layout_marginEnd="0dp" />
</RelativeLayout>
</android.support.constraint.ConstraintLayout>

View file

@ -5,4 +5,8 @@
android:id="@+id/action_home"
android:title="@string/home"
/>
<item
android:id="@+id/action_people"
android:title="@string/people"
/>
</menu>

View file

@ -18,4 +18,8 @@
<color name="black_060">#99000000</color>
<color name="statusbar_color">#292929</color>
<color name="hifiLogoColor">#23B2E7</color>
<color name="hifiAquamarine">#62D5C6</color>
<color name="starSelectedTint">#FBD92A</color>
<color name="starUnselectedTint">#8A8A8A</color>
<color name="slidingUpPanelFadeColor">#40000000</color>
</resources>

View file

@ -37,4 +37,6 @@
<dimen name="header_hifi_height">101dp</dimen>
<dimen name="header_hifi_width">425dp</dimen>
<dimen name="list_vertical_padding">8dp</dimen>
</resources>

View file

@ -1,6 +1,7 @@
<resources>
<string name="app_name" translatable="false">Interface</string>
<string name="home">Home</string>
<string name="people">People</string>
<string name="web_view_action_open_in_browser" translatable="false">Open in browser</string>
<string name="web_view_action_share" translatable="false">Share link</string>
<string name="web_view_action_share_subject" translatable="false">Shared a link</string>
@ -21,5 +22,11 @@
<string name="search_no_results">No places exist with that name</string>
<string name="privacyPolicy">Privacy Policy</string>
<string name="your_last_location">Your Last Location</string>
<string name="online">Online</string>
<!-- tags -->
<string name="tagFragmentHome">tagFragmentHome</string>
<string name="tagFragmentLogin">tagFragmentLogin</string>
<string name="tagFragmentPolicy">tagFragmentPolicy</string>
<string name="tagFragmentPeople">tagFragmentPeople</string>
</resources>

View file

@ -28,6 +28,7 @@ allprojects {
repositories {
jcenter()
google()
mavenCentral()
}
}