diff --git a/android/apps/interface/src/main/assets/avatars.json b/android/apps/interface/src/main/assets/avatars.json new file mode 100644 index 0000000000..b84d904587 --- /dev/null +++ b/android/apps/interface/src/main/assets/avatars.json @@ -0,0 +1,44 @@ +{ + "avatars": [ + { + "name": "Wooden Mannequin", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/7fe80a1e-f445-4800-9e89-40e677b03bee/large/hifi-mp-7fe80a1e-f445-4800-9e89-40e677b03bee.jpg", + "url": "qrc:////meshes/defaultAvatar_full.fst" + }, + { + "name": "Anime-Styled Boy", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/46e0fd52-3cff-462f-ba97-927338d88295/thumbnail/hifi-mp-46e0fd52-3cff-462f-ba97-927338d88295.jpg", + "url": "http://mpassets.highfidelity.com/46e0fd52-3cff-462f-ba97-927338d88295-v1/AnimeBoy2.fst" + }, + { + "name": "Anime-Styled Girl", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/1e7e43f6-1757-44d3-baa4-756827d96311/large/hifi-mp-1e7e43f6-1757-44d3-baa4-756827d96311.jpg", + "url": "http://mpassets.highfidelity.com/0dce3426-55c8-4641-8dd5-d76eb575b64a-v1/Anime_F_Outfit.fst" + }, + { + "name": "Last Legends: Male Avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/28569047-6f1a-4100-af67-8054ec397cc3/thumbnail/hifi-mp-28569047-6f1a-4100-af67-8054ec397cc3.jpg", + "url": "http://mpassets.highfidelity.com/28569047-6f1a-4100-af67-8054ec397cc3-v1/LLMale2.fst" + }, + { + "name": "Last Legends: Female Avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/8d823be5-6197-4418-b984-eb94160ed956/thumbnail/hifi-mp-8d823be5-6197-4418-b984-eb94160ed956.jpg", + "url": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/46e0fd52-3cff-462f-ba97-927338d88295/thumbnail/hifi-mp-46e0fd52-3cff-462f-ba97-927338d88295.jpg" + }, + { + "name": "Matthew: Photo-real avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/b652081b-a199-425e-ae5c-7815721bdc09/thumbnail/hifi-mp-b652081b-a199-425e-ae5c-7815721bdc09.jpg", + "url": "http://mpassets.highfidelity.com/b652081b-a199-425e-ae5c-7815721bdc09-v1/matthew.fst" + }, + { + "name": "Priscilla: Photo real avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/e7565f93-8bc5-47c2-b6eb-b3b31d4a1339/thumbnail/hifi-mp-e7565f93-8bc5-47c2-b6eb-b3b31d4a1339.jpg", + "url": "http://mpassets.highfidelity.com/e7565f93-8bc5-47c2-b6eb-b3b31d4a1339-v1/priscilla.fst" + }, + { + "name": "H1-F1 Optical Interpreter bot", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/469c8b66-e3c2-47fb-9820-e306b1dd15c4/large/hifi-mp-469c8b66-e3c2-47fb-9820-e306b1dd15c4.jpg", + "url": "http://mpassets.highfidelity.com/469c8b66-e3c2-47fb-9820-e306b1dd15c4-v1/optical_interpreter[1].fst" + } + ] +} \ No newline at end of file diff --git a/android/apps/interface/src/main/cpp/native.cpp b/android/apps/interface/src/main/cpp/native.cpp index 2bb851bb85..a466245eda 100644 --- a/android/apps/interface/src/main/cpp/native.cpp +++ b/android/apps/interface/src/main/cpp/native.cpp @@ -493,6 +493,34 @@ Java_io_highfidelity_hifiinterface_SplashActivity_registerLoadCompleteListener(J } +JNIEXPORT jstring JNICALL +Java_io_highfidelity_hifiinterface_fragment_ProfileFragment_getDisplayName(JNIEnv *env, + jobject instance) { + + QString displayName = AndroidHelper::instance().getDisplayName(); + return env->NewStringUTF(displayName.toLatin1().data()); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_ProfileFragment_setDisplayName(JNIEnv *env, + jobject instance, + jstring name_) { + const char *c_name = env->GetStringUTFChars(name_, 0); + const QString name = QString::fromUtf8(c_name); + env->ReleaseStringUTFChars(name_, c_name); + AndroidHelper::instance().setDisplayName(name); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_ProfileFragment_setAvatarUrl(JNIEnv *env, + jobject instance, + jstring url_) { + const char *url = env->GetStringUTFChars(url_, 0); + QString avatarUrl = QString::fromUtf8(url); + AndroidHelper::instance().setMyAvatarUrl(avatarUrl); + env->ReleaseStringUTFChars(url_, url); +} + JNIEXPORT void JNICALL Java_io_highfidelity_hifiinterface_MainActivity_logout(JNIEnv *env, jobject instance) { DependencyManager::get()->logout(); diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java index e17b530f1c..e5ea0f998d 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java @@ -33,13 +33,15 @@ import com.squareup.picasso.Picasso; import io.highfidelity.hifiinterface.fragment.FriendsFragment; import io.highfidelity.hifiinterface.fragment.HomeFragment; import io.highfidelity.hifiinterface.fragment.PolicyFragment; +import io.highfidelity.hifiinterface.fragment.ProfileFragment; import io.highfidelity.hifiinterface.fragment.SettingsFragment; import io.highfidelity.hifiinterface.fragment.SignupFragment; import io.highfidelity.hifiinterface.task.DownloadProfileImageTask; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, HomeFragment.OnHomeInteractionListener, - FriendsFragment.OnHomeInteractionListener { + FriendsFragment.OnHomeInteractionListener, + ProfileFragment.OnProfileInteractionListener { private static final int PROFILE_PICTURE_PLACEHOLDER = R.drawable.default_profile_avatar; public static final String DEFAULT_FRAGMENT = "Home"; @@ -61,6 +63,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private View mProfilePanel; private TextView mLogoutOption; private MenuItem mPeopleMenuItem; + private MenuItem mProfileMenuItem; private boolean backToScene; private String backToUrl; @@ -83,6 +86,8 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On mPeopleMenuItem = mNavigationView.getMenu().findItem(R.id.action_people); + mProfileMenuItem = mNavigationView.getMenu().findItem(R.id.action_profile); + updateDebugMenu(mNavigationView.getMenu()); Toolbar toolbar = findViewById(R.id.toolbar); @@ -162,6 +167,12 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true, true); } + private void loadProfileFragment() { + Fragment fragment = ProfileFragment.newInstance(); + + loadFragment(fragment, getString(R.string.profile), getString(R.string.tagFragmentProfile), true, true); + } + private void loadSettingsFragment() { SettingsFragment fragment = SettingsFragment.newInstance(); @@ -261,6 +272,9 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On case R.id.action_people: loadPeopleFragment(); return true; + case R.id.action_profile: + loadProfileFragment(); + break; case R.id.action_debug_settings: loadSettingsFragment(); return true; @@ -351,6 +365,21 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On goToUser(username); } + @Override + public void onCancelProfileEdit() { + loadHomeFragment(false); + } + + @Override + public void onCompleteProfileEdit() { + loadHomeFragment(false); + } + + @Override + public void onAvatarChosen() { + loadHomeFragment(false); + } + private class RoundProfilePictureCallback implements Callback { @Override public void onSuccess() { diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/ProfileFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/ProfileFragment.java new file mode 100644 index 0000000000..e5aa793341 --- /dev/null +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/ProfileFragment.java @@ -0,0 +1,126 @@ +package io.highfidelity.hifiinterface.fragment; + + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.TextView; + +import io.highfidelity.hifiinterface.R; +import io.highfidelity.hifiinterface.provider.AvatarProvider; +import io.highfidelity.hifiinterface.view.AvatarAdapter; + +public class ProfileFragment extends Fragment { + + private TextView mDisplayName; + + private Button mOkButton; + private OnProfileInteractionListener mListener; + private AvatarProvider mAvatarsProvider; + + private native String getDisplayName(); + private native void setDisplayName(String name); + private native void setAvatarUrl(String url); + + public ProfileFragment() { + // Required empty public constructor + } + + public static ProfileFragment newInstance() { + ProfileFragment fragment = new ProfileFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_profile, container, false); + + mDisplayName = rootView.findViewById(R.id.displayName); + mDisplayName.setText(getDisplayName()); + mDisplayName.setOnEditorActionListener((textView, actionId, keyEvent) -> onDisplayNameEditorAction(textView, actionId, keyEvent)); + + mOkButton = rootView.findViewById(R.id.okButton); + mOkButton.setOnClickListener(view -> onOkButtonClicked()); + + rootView.findViewById(R.id.cancel).setOnClickListener(view -> onCancelProfileEdit()); + + RecyclerView avatarsView = rootView.findViewById(R.id.gridview); + int numberOfColumns = 1; + mAvatarsProvider = new AvatarProvider(getContext()); + GridLayoutManager gridLayoutMgr = new GridLayoutManager(getContext(), numberOfColumns); + avatarsView.setLayoutManager(gridLayoutMgr); + AvatarAdapter avatarAdapter = new AvatarAdapter(getContext(), mAvatarsProvider); + avatarsView.setAdapter(avatarAdapter); + avatarAdapter.loadAvatars(); + + avatarAdapter.setClickListener((view, position, avatar) -> { + setAvatarUrl(avatar.avatarUrl); + if (mListener != null) { + mListener.onAvatarChosen(); + } + }); + return rootView; + } + + private void onOkButtonClicked() { + setDisplayName(mDisplayName.getText().toString()); + View view = getActivity().getCurrentFocus(); + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + if (mListener != null) { + mListener.onCompleteProfileEdit(); + } + } + + private boolean onDisplayNameEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + mOkButton.performClick(); + return true; + } + return false; + } + + private void onCancelProfileEdit() { + View view = getActivity().getCurrentFocus(); + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + if (mListener != null) { + mListener.onCancelProfileEdit(); + } + } + + /** + * Processes the back pressed event and returns true if it was managed by this Fragment + * @return + */ + public boolean onBackPressed() { + return false; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnProfileInteractionListener) { + mListener = (OnProfileInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnProfileInteractionListener"); + } + } + + public interface OnProfileInteractionListener { + void onCancelProfileEdit(); + void onCompleteProfileEdit(); + void onAvatarChosen(); + } +} diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/AvatarProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/AvatarProvider.java new file mode 100644 index 0000000000..5bbb8ee666 --- /dev/null +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/AvatarProvider.java @@ -0,0 +1,70 @@ +package io.highfidelity.hifiinterface.provider; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import io.highfidelity.hifiinterface.view.AvatarAdapter; + +/** + * Created by gcalero on 1/21/19 + */ +public class AvatarProvider { + + private static final String AVATARS_JSON = "avatars.json"; + private static final String JSON_FIELD_NAME = "name"; + private static final String JSON_FIELD_URL = "url"; + private static final String JSON_FIELD_IMAGE = "preview_image"; + private static final String JSON_FIELD_AVATARS_ARRAY = "avatars"; + private final Context mContext; + + public interface AvatarsCallback { + void retrieveOk(List avatars); + void retrieveError(Exception e, String message); + } + + public AvatarProvider(Context context) { + mContext = context; + } + + public void retrieve(AvatarsCallback avatarsCallback) + { + try { + JSONObject obj = new JSONObject(loadJSONFromAssets()); + JSONArray m_jArry = obj.getJSONArray(JSON_FIELD_AVATARS_ARRAY); + ArrayList avatars = new ArrayList<>(); + + for (int i = 0; i < m_jArry.length(); i++) { + JSONObject jo_inside = m_jArry.getJSONObject(i); + AvatarAdapter.Avatar anAvatar = new AvatarAdapter.Avatar(); + anAvatar.avatarName = jo_inside.getString(JSON_FIELD_NAME); + anAvatar.avatarPreviewUrl = jo_inside.getString(JSON_FIELD_IMAGE); + anAvatar.avatarUrl = jo_inside.getString(JSON_FIELD_URL); + avatars.add(anAvatar); + } + avatarsCallback.retrieveOk(avatars); + } catch (IOException e) { + avatarsCallback.retrieveError(e, "Failed retrieving avatar JSON"); + } catch (JSONException e) { + avatarsCallback.retrieveError(e, "Failed parsing avatar JSON"); + } + } + + private String loadJSONFromAssets() throws IOException { + String json = null; + InputStream is = mContext.getAssets().open(AVATARS_JSON); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + json = new String(buffer, "UTF-8"); + return json; + } +} diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/view/AvatarAdapter.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/view/AvatarAdapter.java new file mode 100644 index 0000000000..d88083ff2a --- /dev/null +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/view/AvatarAdapter.java @@ -0,0 +1,111 @@ +package io.highfidelity.hifiinterface.view; + +import android.content.Context; +import android.net.Uri; +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.Picasso; + +import java.util.ArrayList; +import java.util.List; + +import io.highfidelity.hifiinterface.R; +import io.highfidelity.hifiinterface.provider.AvatarProvider; + +/** + * Created by gcalero on 1/21/19 + */ +public class AvatarAdapter extends RecyclerView.Adapter { + + private static final String TAG = "Interface"; + private final Context mContext; + private final LayoutInflater mInflater; + private final AvatarProvider mProvider; + private List mAvatars = new ArrayList<>(); + private ItemClickListener mClickListener; + + public AvatarAdapter(Context context, AvatarProvider provider) { + mContext = context; + mInflater = LayoutInflater.from(mContext); + mProvider = provider; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = mInflater.inflate(R.layout.avatar_item, parent, false); + return new AvatarAdapter.ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AvatarAdapter.Avatar anAvatar = mAvatars.get(position); + assert(holder.mName != null); + holder.mName.setText(anAvatar.avatarName); + Uri uri = Uri.parse(anAvatar.avatarPreviewUrl); + Picasso.get().load(uri).into(holder.mPreviewImage); + } + + @Override + public int getItemCount() { + return mAvatars.size(); + } + + public void loadAvatars() { + mProvider.retrieve(new AvatarProvider.AvatarsCallback() { + @Override + public void retrieveOk(List avatars) { + mAvatars = new ArrayList<>(avatars); + notifyDataSetChanged(); + } + + @Override + public void retrieveError(Exception e, String message) { + Log.e(TAG, message, e); + } + }); + } + + public void setClickListener(ItemClickListener clickListener) { + mClickListener = clickListener; + } + + public interface ItemClickListener { + void onItemClick(View view, int position, Avatar avatar); + } + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + + TextView mName; + ImageView mPreviewImage; + + public ViewHolder(View itemView) { + super(itemView); + mName = itemView.findViewById(R.id.avatarName); + assert (mName != null); + mPreviewImage = itemView.findViewById(R.id.avatarPreview); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + int position= getAdapterPosition(); + if (mClickListener != null) { + mClickListener.onItemClick(view, position, mAvatars.get(position)); + } + } + } + + public static class Avatar { + public String avatarName; + public String avatarUrl; + public String avatarPreviewUrl; + + public Avatar() { } + } +} diff --git a/android/apps/interface/src/main/res/layout/avatar_item.xml b/android/apps/interface/src/main/res/layout/avatar_item.xml new file mode 100644 index 0000000000..6fba708030 --- /dev/null +++ b/android/apps/interface/src/main/res/layout/avatar_item.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/android/apps/interface/src/main/res/layout/fragment_profile.xml b/android/apps/interface/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000000..8a5f925ad2 --- /dev/null +++ b/android/apps/interface/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + +