From ca40fee516951969695071f43cacad94d27c55b2 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sat, 30 Jan 2016 01:21:45 +0800 Subject: [PATCH 01/43] Refactor model data into seperate package and refactor previous model usages to reflect the change --- .../worldscope/FacebookLoginActivity.java | 1 + .../com/litmus/worldscope/MainActivity.java | 1 + .../worldscope/WorldScopeAPIService.java | 2 + .../litmus/worldscope/WorldScopeRestAPI.java | 1 + .../model/WorldScopeCreatedStream.java | 18 +++++++ .../worldscope/model/WorldScopeStream.java | 47 +++++++++++++++++++ .../{ => model}/WorldScopeUser.java | 2 +- .../model/WorldScopeViewStream.java | 16 +++++++ 8 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeCreatedStream.java create mode 100644 client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java rename client/android/WorldScope/app/src/main/java/com/litmus/worldscope/{ => model}/WorldScopeUser.java (99%) create mode 100644 client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java index d82622e..9ee3f12 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java @@ -8,6 +8,7 @@ import android.widget.Toast; import com.facebook.AccessToken; +import com.litmus.worldscope.model.WorldScopeUser; import layout.FacebookLoginFragment; diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java index b63c7c7..fd378c0 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java @@ -31,6 +31,7 @@ import com.facebook.GraphRequest; import com.facebook.GraphResponse; import com.facebook.HttpMethod; +import com.litmus.worldscope.model.WorldScopeUser; import com.squareup.picasso.Picasso; import com.squareup.picasso.Transformation; diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java index 9d7bd1e..574f213 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java @@ -1,5 +1,7 @@ package com.litmus.worldscope; +import com.litmus.worldscope.model.WorldScopeUser; + import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java index 305fc3b..2e6d474 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java @@ -9,6 +9,7 @@ import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; +import com.litmus.worldscope.model.WorldScopeUser; import java.lang.reflect.Type; diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeCreatedStream.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeCreatedStream.java new file mode 100644 index 0000000..7dd6a2f --- /dev/null +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeCreatedStream.java @@ -0,0 +1,18 @@ +package com.litmus.worldscope.model; + +import java.util.Date; + +/** + * Stream object returned from WorldScope App Server when creating a new stream + */ +public class WorldScopeCreatedStream extends WorldScopeStream { + + private Date endedAt; + private String streamLink; + + public Date getEndedAt() {return endedAt;} + public String getStreamLink() {return streamLink;}; + + public void setEndedAt(Date endedAt) {this.endedAt = endedAt;} + public void setStreamLink(String streamLink) {this.streamLink = streamLink;} +} diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java new file mode 100644 index 0000000..be3979c --- /dev/null +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java @@ -0,0 +1,47 @@ +package com.litmus.worldscope.model; + +import java.util.Date; + +/** + * Generic Stream object model returned by WorldScope API Service + */ +public class WorldScopeStream { + private String streamId; + private String appInstance; + private String title; + private String roomId; + private int totalStickers; + private int totalViewers; + private boolean live; + private String duration; + private String description; + private Date createdAt; + private Date deletedAt; + private WorldScopeUser owner; + + public String getStreamId() {return streamId;} + public String getAppInstance() {return appInstance;} + public String getTitle() {return title;} + public String getRoomId() {return roomId;} + public int getTotalStickers() {return totalStickers;} + public int getTotalViewers() {return totalViewers;} + public boolean getLive() {return live;} + public String getDuration() {return duration;} + public String getDescription() {return description;} + public Date getCreatedAt() {return createdAt;} + public Date getDeletedAt() {return deletedAt;} + public WorldScopeUser getOwner() {return owner;} + + public void setStreamId(String streamId) {this.streamId = streamId;} + public void setAppInstance(String appInstance) {this.appInstance = appInstance;} + public void setTitle(String title) {this.title = title;} + public void setRoomId(String roomId) {this.roomId = roomId;} + public void setTotalStickers(int totalStickers) {this.totalStickers = totalStickers;} + public void setTotalViewers(int totalViewers) {this.totalViewers = totalViewers;} + public void setLive(boolean live) {this.live = live;} + public void setDuration(String duration) {this.duration = duration;} + public void setDescription(String description) {this.description = description;} + public void setCreatedAt(Date createdAt) {this.createdAt = createdAt;} + public void setDeletedAt(Date deletedAt) {this.deletedAt = deletedAt;} + public void setOwner(WorldScopeUser owner) {this.owner = owner;} +} diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeUser.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeUser.java similarity index 99% rename from client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeUser.java rename to client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeUser.java index 34c8fa4..5f16569 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeUser.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeUser.java @@ -1,4 +1,4 @@ -package com.litmus.worldscope; +package com.litmus.worldscope.model; import android.os.Parcel; import android.os.Parcelable; diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java new file mode 100644 index 0000000..be38d7e --- /dev/null +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java @@ -0,0 +1,16 @@ +package com.litmus.worldscope.model; + +/** + * Stream object model returned from WorldScope App Server for viewing of streams + */ +public class WorldScopeViewStream extends WorldScopeStream{ + private String viewLink; + private String thumbnailLink; + + public String getViewLink() {return viewLink;} + public String getThumbnailLink() {return thumbnailLink;} + + public void setViewLink(String viewLink) {this.viewLink = viewLink;} + public void setThumbnailLink(String thumbnailLink) {this.thumbnailLink = thumbnailLink;} + +} From da8e6c42b7fd2d8d23fb4d7518284a57fa0a9749 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sat, 30 Jan 2016 15:51:34 +0800 Subject: [PATCH 02/43] Fix Logout and remove TitleBar in Welcome screen --- .../app/src/main/AndroidManifest.xml | 3 ++- .../com/litmus/worldscope/MainActivity.java | 22 +++++++++++++++---- .../java/layout/FacebookLoginFragment.java | 9 ++++++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/client/android/WorldScope/app/src/main/AndroidManifest.xml b/client/android/WorldScope/app/src/main/AndroidManifest.xml index 609b1ed..3ae4130 100644 --- a/client/android/WorldScope/app/src/main/AndroidManifest.xml +++ b/client/android/WorldScope/app/src/main/AndroidManifest.xml @@ -29,7 +29,8 @@ + android:label="@string/app_name" + android:theme="@style/Theme.AppCompat.NoActionBar"> diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java index fd378c0..d0d6978 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java @@ -185,6 +185,19 @@ private void setToolbarTitle() { } } + // Redirects to Facebook Login activity + + private void redirectToFacebookLoginActivity(boolean isAttemptLogout) { + Intent intent = new Intent(this, FacebookLoginActivity.class); + + if(isAttemptLogout) { + intent.putExtra("isAttemptLogout", true); + } + + startActivity(intent); + } + + // Redirects to view activity private void redirectToViewActivity() { @@ -253,19 +266,20 @@ public void onResponse(Response response) { Log.d(TAG, "Success!"); Log.d(TAG, "" + response.body().toString()); - // Redirect to FacebookLoginActivty - Intent intent = new Intent(context, FacebookLoginActivity.class); - startActivity(intent); + redirectToFacebookLoginActivity(true); } else { Log.d(TAG, "Failure!"); Log.d(TAG, "" + response.code()); Log.d(TAG, "" + response.body().toString()); + + redirectToFacebookLoginActivity(true); } } @Override public void onFailure(Throwable t) { - Log.d(TAG, "Failure: " + t.getMessage()); + + redirectToFacebookLoginActivity(true); } }); } diff --git a/client/android/WorldScope/app/src/main/java/layout/FacebookLoginFragment.java b/client/android/WorldScope/app/src/main/java/layout/FacebookLoginFragment.java index 063dd14..7e07726 100644 --- a/client/android/WorldScope/app/src/main/java/layout/FacebookLoginFragment.java +++ b/client/android/WorldScope/app/src/main/java/layout/FacebookLoginFragment.java @@ -35,6 +35,7 @@ public class FacebookLoginFragment extends Fragment { private final String PUBLIC_PROFILE_PERMISSION = "public_profile"; private final String USER_FRIENDS_PERMISSION = "user_friends"; private final String ERROR_IMPLEMENT_ON_FRAGMENT_INTERACTION_LISTENER = " must implement OnFragmentInteractionListener"; + private final String EXTRA_IS_ATTEMPT_LOGOUT = "isAttemptLogout"; private OnFragmentInteractionListener mListener; private CallbackManager callbackManager; @@ -113,8 +114,12 @@ public void onError(FacebookException exception) { loginManager = LoginManager.getInstance(); loginManager.registerCallback(callbackManager, facebookCallback); - // Attempt to check if user is logged in or not - loginManager.logInWithReadPermissions(this, Arrays.asList(PUBLIC_PROFILE_PERMISSION, USER_FRIENDS_PERMISSION)); + // If user did not previously logout, check if login. Else, logout of facebook + if(!getActivity().getIntent().getBooleanExtra(EXTRA_IS_ATTEMPT_LOGOUT , false)) { + loginManager.logInWithReadPermissions(this, Arrays.asList(PUBLIC_PROFILE_PERMISSION, USER_FRIENDS_PERMISSION)); + } else { + logoutFromFacebook(); + } return view; } From f7925cafbb4fc3182e3880a0a007b9a09c7b451d Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sat, 30 Jan 2016 15:51:59 +0800 Subject: [PATCH 03/43] Added default constructor to instantiate WorldScopeUser --- .../main/java/com/litmus/worldscope/model/WorldScopeUser.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeUser.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeUser.java index 5f16569..81407f8 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeUser.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeUser.java @@ -58,6 +58,8 @@ public String toString() { + this.getUpdatedAt() + " " + this.getDeletedAt() + " " + this.getuserName(); } + public WorldScopeUser() { + } protected WorldScopeUser(Parcel in) { userId = in.readString(); From c3788ec15888d782a44ac77358e29846c879110b Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sat, 30 Jan 2016 15:53:08 +0800 Subject: [PATCH 04/43] Updated XML layout and remove redundant fragments --- .../main/res/layout/fragment_stream_list.xml | 1 + .../res/layout/fragment_stream_list_item.xml | 146 ++++++++---------- 2 files changed, 67 insertions(+), 80 deletions(-) diff --git a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list.xml b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list.xml index 41ee8f9..7c870d7 100644 --- a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list.xml +++ b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list.xml @@ -5,6 +5,7 @@ android:layout_height="wrap_content"> diff --git a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml index 1dd4bc4..d8ae334 100644 --- a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml +++ b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml @@ -1,109 +1,95 @@ + + + - - + android:layout_marginLeft="16dp" + android:layout_weight="1"> - - - - - - - + android:gravity="center_vertical"> + android:layout_alignEnd="@+id/startTime" + android:layout_marginEnd="15dp" + android:layout_weight="1" + android:maxLines="2" /> - + + - + - + + + - - + + - \ No newline at end of file From ddae3fc92b9356ef82ac946ced3dda734b8b4e7d Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sat, 30 Jan 2016 15:53:47 +0800 Subject: [PATCH 05/43] Set up SwipeRefreshLayout, hook up to ListView and setup dummy data into the list --- .../worldscope/StreamRefreshListFragment.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java index 4b6157d..09e51f4 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java @@ -1,22 +1,43 @@ package com.litmus.worldscope; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import com.litmus.worldscope.model.WorldScopeUser; +import com.litmus.worldscope.model.WorldScopeViewStream; +import com.squareup.picasso.Picasso; + +import java.util.ArrayList; +import java.util.Date; /** * A placeholder fragment containing a simple view. */ public class StreamRefreshListFragment extends Fragment { + private final String TAG = "StreamRefreshListFragment"; + /** * The fragment argument representing the section number for this * fragment. */ private static final String ARG_SECTION_NUMBER = "section_number"; + private ListView listView; + private SwipeRefreshLayout swipeRefreshLayout; + private WorldScopeStreamAdapter worldScopeStreamAdapter; + private ArrayList streams; + /** * Returns a new instance of this fragment for the given section * number. @@ -35,8 +56,119 @@ public StreamRefreshListFragment() { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the fragment with the XML layout View rootView = inflater.inflate(R.layout.fragment_stream_list, container, false); + // Initiate the streams array + streams = new ArrayList<>(); + + // Set the WorldScopeStreamAdapter into ListView + swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipeRefreshLayout); + listView = (ListView) rootView.findViewById(R.id.streamListView); + worldScopeStreamAdapter = new WorldScopeStreamAdapter(getActivity(), R.layout.fragment_stream_list_item, streams); + listView.setAdapter(worldScopeStreamAdapter); + + // Set the onRefreshListener into swipeRefreshLayout + swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + // Adds one more dummy when refresh + putFakeData(); + worldScopeStreamAdapter = new WorldScopeStreamAdapter(getActivity(), R.layout.fragment_stream_list_item, streams); + listView.setAdapter(worldScopeStreamAdapter); + swipeRefreshLayout.setRefreshing(false); + } + }); + putFakeData(); + return rootView; } + + private void putFakeData() { + WorldScopeUser dummyUser = new WorldScopeUser(); + dummyUser.setAlias("Ash Ketchum"); + + WorldScopeViewStream dummy = new WorldScopeViewStream(); + dummy.setThumbnailLink("https://36.media.tumblr.com/0c71b9afd08d039c6c294578ba3c96e8/tumblr_mwhk5xPN8u1rgpyeqo1_500.png"); + dummy.setTitle("Pikachu eating Pocky"); + dummy.setOwner(dummyUser); + dummy.setCreatedAt(new Date()); + dummy.setTotalViewers(123); + + streams.add(dummy); + } + + private class WorldScopeStreamAdapter extends ArrayAdapter { + + private Context context; + private int layoutResourceId; + private ArrayList data; + + public WorldScopeStreamAdapter(Context context, int layoutResourceId, ArrayList data) { + super(context, layoutResourceId, data); + this.context = context; + this.layoutResourceId = layoutResourceId; + this.data = data; + } + + /** + * Override getView method in ArrayAdapter to configure how the view will look + */ + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // Get the item to display from data given position + WorldScopeViewStream stream = data.get(position); + + /** + * Implement viewHolder pattern to cache the view to reduce calls to findViewById(), + * improving loading and response time + * + * ViewHolder patterns involve creating a class containing each individual elements that + * can be found in the XML layout and using it to 'hold' the view to reduce calls to + * findViewById() + * + * Check if convertView is null, if null then inflate and set viewHolder + */ + ViewHolder viewHolder; + if(convertView == null) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + convertView = inflater.inflate(R.layout.fragment_stream_list_item, parent, false); + + viewHolder = new ViewHolder(); + viewHolder.thumbnailImageView = (ImageView) convertView.findViewById(R.id.streamThumbnailImageView); + viewHolder.titleTextView = (TextView) convertView.findViewById(R.id.streamTitle); + viewHolder.ownerTextView = (TextView) convertView.findViewById(R.id.streamOwner); + viewHolder.createdAtTextView = (TextView) convertView.findViewById(R.id.startTime); + viewHolder.totalViewerTextView = (TextView) convertView.findViewById(R.id.numOfViewers); + + // Access convertView's XML elements now using viewHolder + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + + // Set text data into the view + viewHolder.titleTextView.setText(stream.getTitle()); + viewHolder.ownerTextView.setText(stream.getOwner().getAlias()); + viewHolder.createdAtTextView.setText(stream.getCreatedAt().toString()); + viewHolder.totalViewerTextView.setText(String.valueOf(stream.getTotalViewers())); + + // Use Picasso to set thumbnail image + Picasso.with(viewHolder.thumbnailImageView.getContext()) + .load(stream.getThumbnailLink()) + .into(viewHolder.thumbnailImageView); + + return convertView; + } + + } + + private static class ViewHolder { + ImageView thumbnailImageView; + TextView titleTextView; + TextView ownerTextView; + TextView createdAtTextView; + TextView totalViewerTextView; + } } \ No newline at end of file From b81ab0de7746fdfa0b5af23143ecdf4a2a1dd042 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sun, 31 Jan 2016 03:09:25 +0800 Subject: [PATCH 06/43] Updated method to instantiate Retrofit, created new API call getStreams and test getting stream in putFakeData() --- .../worldscope/FacebookLoginActivity.java | 2 +- .../com/litmus/worldscope/MainActivity.java | 3 +- .../worldscope/StreamRefreshListFragment.java | 30 ++++++- .../worldscope/WorldScopeAPIService.java | 22 +++++ .../litmus/worldscope/WorldScopeRestAPI.java | 85 ++++++++++++++++++- 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java index 9ee3f12..8888996 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java @@ -38,7 +38,7 @@ public void onFacebookLoginSuccess(AccessToken accessToken) { Log.d(TAG, "AccessToken: " + accessToken.getToken()); // Instantiate and make a call to login user into WorldScope servers - Call call = WorldScopeRestAPI.buildWorldScopeAPIService().loginUser(new WorldScopeAPIService.LoginUserRequest(accessToken.getToken())); + Call call = new WorldScopeRestAPI(context).buildWorldScopeAPIService().loginUser(new WorldScopeAPIService.LoginUserRequest(accessToken.getToken())); call.enqueue(new Callback() { @Override public void onResponse(Response response) { diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java index d0d6978..d23a924 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java @@ -258,7 +258,8 @@ public void onCompleted(GraphResponse response) { // Log out from WorldScope App server // TODO: Check JSON formatting of API request's result private void logoutFromAppServer() { - Call call = WorldScopeRestAPI.buildWorldScopeAPIService().logoutUser(); + Call call = new WorldScopeRestAPI(context + ).buildWorldScopeAPIService().logoutUser(); call.enqueue(new Callback() { @Override public void onResponse(Response response) { diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java index 09e51f4..beedae3 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java @@ -19,13 +19,18 @@ import java.util.ArrayList; import java.util.Date; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; /** * A placeholder fragment containing a simple view. */ public class StreamRefreshListFragment extends Fragment { - private final String TAG = "StreamRefreshListFragment"; + private final String TAG = "StreamRefreshList"; /** * The fragment argument representing the section number for this @@ -85,6 +90,29 @@ public void onRefresh() { } private void putFakeData() { + + // Make a dummy call to backend + Call> call = new WorldScopeRestAPI(getActivity()).buildWorldScopeAPIService().getStreams(null, null, null); + call.enqueue(new Callback>() { + @Override + public void onResponse(Response> response) { + Log.d(TAG, "GOT RESPONSE"); + + if (response.isSuccess()) { + Log.d(TAG, "RESPONSE SUCCESS"); + Log.d(TAG, response.body().toString()); + } else { + Log.d(TAG, "RESPONSE FAIL"); + } + } + + @Override + public void onFailure(Throwable t) { + Log.d(TAG, "NO RESPONSE"); + } + }); + + WorldScopeUser dummyUser = new WorldScopeUser(); dummyUser.setAlias("Ash Ketchum"); diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java index 574f213..5a7aef9 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java @@ -1,11 +1,23 @@ package com.litmus.worldscope; +import android.preference.Preference; +import android.preference.PreferenceManager; + import com.litmus.worldscope.model.WorldScopeUser; +import com.litmus.worldscope.model.WorldScopeViewStream; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; import retrofit2.http.POST; +import retrofit2.http.Query; public class WorldScopeAPIService { @@ -23,6 +35,16 @@ public interface WorldScopeAPIInterface { @GET("/api/users/logout") Call logoutUser(); + /** + * Method to get streams + * @param status - Possible values: live, done, all + * @param sort - Possible values: time, viewers, title + * @param order - Possible values: desc, asc + * @return + */ + @GET("/api/streams") + Call> getStreams(@Query("status") String status, @Query("sort") String sort, @Query("order") String order); + } // Class to set body of login request diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java index 2e6d474..5ab7413 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java @@ -1,5 +1,8 @@ package com.litmus.worldscope; +import android.content.Context; +import android.preference.Preference; +import android.preference.PreferenceManager; import android.util.Log; import com.google.gson.FieldNamingPolicy; @@ -11,8 +14,15 @@ import com.google.gson.JsonParseException; import com.litmus.worldscope.model.WorldScopeUser; +import java.io.IOException; import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import retrofit2.GsonConverterFactory; import retrofit2.Retrofit; @@ -20,9 +30,28 @@ public class WorldScopeRestAPI { private static final String TAG = "WorldScopeRestAPI"; + private static final String cookiesSetTag = "PREF_COOKIES"; + + private static final String cookiesHeaderTag = "cookie"; + + private static final String setCookiesHeaderTag = "set-cookie"; + + private static Context context; + + private OkHttpClient okHttpClient; static Retrofit.Builder retrofitBuilder; - public static WorldScopeAPIService.WorldScopeAPIInterface buildWorldScopeAPIService() { + public WorldScopeRestAPI(Context context) { + this.context = context; + + // Set up interceptors for cookies with Context given + okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new AddCookiesInterceptor()) + .addInterceptor(new SaveCookiesInterceptor()) + .build(); + } + + public WorldScopeAPIService.WorldScopeAPIInterface buildWorldScopeAPIService() { // Create an instance of the GsonBuilder to pass JSON results GsonBuilder gsonBuilder = new GsonBuilder(); @@ -35,6 +64,7 @@ public static WorldScopeAPIService.WorldScopeAPIInterface buildWorldScopeAPIServ Retrofit retrofit = getRetrofitBuilderInstance() .baseUrl(WorldScopeAPIService.WorldScopeURL) .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) .build(); Log.d(TAG, "Returning created service"); @@ -61,4 +91,57 @@ public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) return new Gson().fromJson(content, type); } } + + // AddCookiesInterceptor appends cookies to request + public class AddCookiesInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + + Log.d(TAG, "Request: " + chain.request().toString()); + Log.d(TAG, "Request URL: " + chain.request().url()); + Log.d(TAG, "Request Method: " + chain.request().method()); + + HashSet preferences = (HashSet) PreferenceManager.getDefaultSharedPreferences(context) + .getStringSet(cookiesSetTag, new HashSet()); + + if(chain.request().method().equals("POST") && chain.request().url().toString().equals("http://54.179.170.132:3000/api/users/login")) { + preferences.clear(); + } + + Request.Builder builder = chain.request().newBuilder(); + for(String cookie: preferences) { + builder.addHeader(cookiesHeaderTag, cookie); + Log.d(TAG, "Header: " + cookiesHeaderTag + "=" + cookie); + } + + return chain.proceed(builder.build()); + + } + } + + // SaveCookiesInterceptor saves cookies received + public class SaveCookiesInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Response originalResponse = chain.proceed(chain.request()); + + Log.d(TAG, "Response: " + originalResponse.request().headers().toString()); + + + if(!originalResponse.headers(setCookiesHeaderTag).isEmpty()) { + HashSet cookies = new HashSet<>(); + + for(String header: originalResponse.headers(setCookiesHeaderTag)) { + cookies.add(header); + Log.d(TAG, "Cookies saved: " + header); + } + + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putStringSet(cookiesSetTag, cookies) + .apply(); + } + return originalResponse; + } + + } } From 69d8e6b88d7ddcab3f6562fde765fd27ec224ad7 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sun, 31 Jan 2016 03:09:55 +0800 Subject: [PATCH 07/43] Included toString() methods for WorldScopeStream and WorldScopeViewStream --- .../worldscope/model/WorldScopeStream.java | 16 ++++++++++++++++ .../worldscope/model/WorldScopeViewStream.java | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java index be3979c..258055d 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java @@ -44,4 +44,20 @@ public class WorldScopeStream { public void setCreatedAt(Date createdAt) {this.createdAt = createdAt;} public void setDeletedAt(Date deletedAt) {this.deletedAt = deletedAt;} public void setOwner(WorldScopeUser owner) {this.owner = owner;} + + @Override + public String toString() { + return "streamId: " + getStreamId() + "\n" + + "appInstance: " + getAppInstance() + "\n" + + "title: " + getTitle() + "\n" + + "roomId: " + getRoomId() + "\n" + + "totalStickers: " + getTotalStickers() + "\n" + + "totalViewers: " + getTotalViewers() + "\n" + + "live: " + getLive() + "\n" + + "duration: " + getDuration() + "\n" + + "description: " + getDescription() + "\n" + + "createdAt: " + getCreatedAt() + "\n" + + "deletedAt: " + getDeletedAt() + "\n" + + "owner: " + getOwner(); + } } diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java index be38d7e..5b630fb 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java @@ -13,4 +13,11 @@ public class WorldScopeViewStream extends WorldScopeStream{ public void setViewLink(String viewLink) {this.viewLink = viewLink;} public void setThumbnailLink(String thumbnailLink) {this.thumbnailLink = thumbnailLink;} + @Override + public String toString() { + return this.toString() + "\n" + + "viewLink: " + this.getViewLink() + + "thumbnailLink: " + this.getThumbnailLink(); + } + } From 9edf5777f530d31dadea88eaf4b01a392f107c57 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Tue, 2 Feb 2016 15:24:49 +0800 Subject: [PATCH 08/43] Increase cache limit of tabs from 2 to 3 --- .../src/main/java/com/litmus/worldscope/MainActivity.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java index d23a924..b8621c3 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java @@ -49,6 +49,8 @@ public class MainActivity extends AppCompatActivity // Welcome message shown in Toast private final String WELCOME_MSG = "Welcome to WorldScope, %s"; + private final int NUMBER_OF_TABS = 3; + /** * The {@link android.support.v4.view.PagerAdapter} that will provide * fragments for each of the sections. We use a @@ -153,6 +155,9 @@ private void setUpTabsFragment() { // Set up the ViewPager with the sections adapter. mViewPager = (ViewPager) findViewById(R.id.container); + + // Keep all three tabs in memory + mViewPager.setOffscreenPageLimit(NUMBER_OF_TABS); mViewPager.setAdapter(mSectionsPagerAdapter); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); From 85d66dda93fcdfe616514e37ca320fcb7466cb18 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Tue, 2 Feb 2016 15:38:50 +0800 Subject: [PATCH 09/43] Remove putFakeData() and add real data to streams, modified model to reflect changes --- .../worldscope/StreamRefreshListFragment.java | 65 +++++++++++------- .../worldscope/WorldScopeAPIService.java | 25 +++---- .../litmus/worldscope/WorldScopeRestAPI.java | 13 +++- .../worldscope/model/WorldScopeStream.java | 38 +++++++--- .../model/WorldScopeViewStream.java | 2 +- .../drawable-hdpi/ic_play_circle_outline.png | Bin 0 -> 1079 bytes .../drawable-ldpi/ic_play_circle_outline.png | Bin 0 -> 464 bytes .../drawable-mdpi/ic_play_circle_outline.png | Bin 0 -> 734 bytes .../drawable-tvdpi/ic_play_circle_outline.png | Bin 0 -> 1744 bytes .../drawable-xhdpi/ic_play_circle_outline.png | Bin 0 -> 1458 bytes .../ic_play_circle_outline.png | Bin 0 -> 2236 bytes .../ic_play_circle_outline.png | Bin 0 -> 3057 bytes .../res/layout/fragment_stream_list_item.xml | 25 +++++-- 13 files changed, 109 insertions(+), 59 deletions(-) create mode 100644 client/android/WorldScope/app/src/main/res/drawable-hdpi/ic_play_circle_outline.png create mode 100644 client/android/WorldScope/app/src/main/res/drawable-ldpi/ic_play_circle_outline.png create mode 100644 client/android/WorldScope/app/src/main/res/drawable-mdpi/ic_play_circle_outline.png create mode 100644 client/android/WorldScope/app/src/main/res/drawable-tvdpi/ic_play_circle_outline.png create mode 100644 client/android/WorldScope/app/src/main/res/drawable-xhdpi/ic_play_circle_outline.png create mode 100644 client/android/WorldScope/app/src/main/res/drawable-xxhdpi/ic_play_circle_outline.png create mode 100644 client/android/WorldScope/app/src/main/res/drawable-xxxhdpi/ic_play_circle_outline.png diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java index beedae3..d7d876c 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamRefreshListFragment.java @@ -12,13 +12,13 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.litmus.worldscope.model.WorldScopeUser; import com.litmus.worldscope.model.WorldScopeViewStream; import com.squareup.picasso.Picasso; import java.util.ArrayList; -import java.util.Date; import java.util.List; import retrofit2.Call; @@ -32,12 +32,17 @@ public class StreamRefreshListFragment extends Fragment { private final String TAG = "StreamRefreshList"; + private final String APP_SERVER_GET_STREAMS_CHECK_INTERNET_FAILED_MSG = "Failed to get streams, please check your internet connection"; + + private final String APP_SERVER_GET_STREAMS_SERVER_DOWN_FAILED_MSG = "Failed to get streams, please try again later"; + /** * The fragment argument representing the section number for this * fragment. */ private static final String ARG_SECTION_NUMBER = "section_number"; + private int sectionNumber; private ListView listView; private SwipeRefreshLayout swipeRefreshLayout; private WorldScopeStreamAdapter worldScopeStreamAdapter; @@ -48,10 +53,13 @@ public class StreamRefreshListFragment extends Fragment { * number. */ public static StreamRefreshListFragment newInstance(int sectionNumber) { + StreamRefreshListFragment fragment = new StreamRefreshListFragment(); Bundle args = new Bundle(); args.putInt(ARG_SECTION_NUMBER, sectionNumber); fragment.setArguments(args); + fragment.sectionNumber = sectionNumber; + fragment.sectionNumber = sectionNumber; return fragment; } @@ -64,8 +72,13 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, // Inflate the fragment with the XML layout View rootView = inflater.inflate(R.layout.fragment_stream_list, container, false); - // Initiate the streams array - streams = new ArrayList<>(); + // Initiate the streams array if not already initialized + if(streams == null) { + streams = new ArrayList<>(); + } + + // Load streams data + getStreamsData(); // Set the WorldScopeStreamAdapter into ListView swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipeRefreshLayout); @@ -78,52 +91,52 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, @Override public void onRefresh() { // Adds one more dummy when refresh - putFakeData(); - worldScopeStreamAdapter = new WorldScopeStreamAdapter(getActivity(), R.layout.fragment_stream_list_item, streams); - listView.setAdapter(worldScopeStreamAdapter); - swipeRefreshLayout.setRefreshing(false); + getStreamsData(); } }); - putFakeData(); return rootView; } - private void putFakeData() { + private void getStreamsData() { // Make a dummy call to backend - Call> call = new WorldScopeRestAPI(getActivity()).buildWorldScopeAPIService().getStreams(null, null, null); + Log.d(TAG, "Getting Streams"); + Call> call = new WorldScopeRestAPI(getActivity()).buildWorldScopeAPIService().getStreams("live", "time", "desc"); call.enqueue(new Callback>() { @Override public void onResponse(Response> response) { Log.d(TAG, "GOT RESPONSE"); + Log.d(TAG, response.message()); if (response.isSuccess()) { - Log.d(TAG, "RESPONSE SUCCESS"); - Log.d(TAG, response.body().toString()); + Log.d(TAG, "RESPONSE SUCCESS FOR TAB: " + sectionNumber + " with " + response.body().size() + " streams"); + + streams.clear(); + + for (WorldScopeViewStream stream : response.body()) { + streams.add(stream); + } + worldScopeStreamAdapter.notifyDataSetChanged(); + } else { Log.d(TAG, "RESPONSE FAIL"); + Toast toast = Toast.makeText(getContext(), APP_SERVER_GET_STREAMS_SERVER_DOWN_FAILED_MSG, Toast.LENGTH_SHORT); + toast.show(); } + + swipeRefreshLayout.setRefreshing(false); } @Override public void onFailure(Throwable t) { Log.d(TAG, "NO RESPONSE"); + t.printStackTrace(); + swipeRefreshLayout.setRefreshing(false); + Toast toast = Toast.makeText(getContext(), APP_SERVER_GET_STREAMS_CHECK_INTERNET_FAILED_MSG, Toast.LENGTH_SHORT); + toast.show(); } }); - - - WorldScopeUser dummyUser = new WorldScopeUser(); - dummyUser.setAlias("Ash Ketchum"); - - WorldScopeViewStream dummy = new WorldScopeViewStream(); - dummy.setThumbnailLink("https://36.media.tumblr.com/0c71b9afd08d039c6c294578ba3c96e8/tumblr_mwhk5xPN8u1rgpyeqo1_500.png"); - dummy.setTitle("Pikachu eating Pocky"); - dummy.setOwner(dummyUser); - dummy.setCreatedAt(new Date()); - dummy.setTotalViewers(123); - - streams.add(dummy); } private class WorldScopeStreamAdapter extends ArrayAdapter { @@ -178,7 +191,7 @@ public View getView(int position, View convertView, ViewGroup parent) { // Set text data into the view viewHolder.titleTextView.setText(stream.getTitle()); - viewHolder.ownerTextView.setText(stream.getOwner().getAlias()); + //viewHolder.ownerTextView.setText(stream.getOwner().getAlias()); viewHolder.createdAtTextView.setText(stream.getCreatedAt().toString()); viewHolder.totalViewerTextView.setText(String.valueOf(stream.getTotalViewers())); diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java index 5a7aef9..5cca738 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeAPIService.java @@ -1,18 +1,8 @@ package com.litmus.worldscope; -import android.preference.Preference; -import android.preference.PreferenceManager; - import com.litmus.worldscope.model.WorldScopeUser; import com.litmus.worldscope.model.WorldScopeViewStream; - -import java.io.IOException; -import java.util.HashSet; import java.util.List; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; @@ -24,26 +14,31 @@ public class WorldScopeAPIService { // Server address public static final String WorldScopeURL = "http://54.179.170.132:3000"; + // REST API Routes + private static final String loginRoute = "/api/users/login"; + private static final String logoutRoute = "/api/users/logout"; + private static final String streamsRoute = "/api/streams"; + // WorldScope App Id private static final String appId = "123456789"; // API interface requird by Retrofit to make the calls public interface WorldScopeAPIInterface { - @POST("/api/users/login") + @POST(loginRoute) Call loginUser(@Body LoginUserRequest body); - @GET("/api/users/logout") + @GET(logoutRoute) Call logoutUser(); /** * Method to get streams - * @param status - Possible values: live, done, all + * @param state - Possible values: live, done, all * @param sort - Possible values: time, viewers, title * @param order - Possible values: desc, asc * @return */ - @GET("/api/streams") - Call> getStreams(@Query("status") String status, @Query("sort") String sort, @Query("order") String order); + @GET(streamsRoute) + Call> getStreams(@Query("state") String state, @Query("sort") String sort, @Query("order") String order); } diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java index 5ab7413..5ee109c 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/WorldScopeRestAPI.java @@ -104,7 +104,8 @@ public Response intercept(Chain chain) throws IOException { HashSet preferences = (HashSet) PreferenceManager.getDefaultSharedPreferences(context) .getStringSet(cookiesSetTag, new HashSet()); - if(chain.request().method().equals("POST") && chain.request().url().toString().equals("http://54.179.170.132:3000/api/users/login")) { + if(chain.request().method().equals("POST") && chain.request().url().toString().equals(WorldScopeAPIService.WorldScopeURL + "/api/users/login")) { + Log.d(TAG, "Login detected, clearing cookies"); preferences.clear(); } @@ -125,13 +126,19 @@ public class SaveCookiesInterceptor implements Interceptor { public Response intercept(Chain chain) throws IOException { Response originalResponse = chain.proceed(chain.request()); - Log.d(TAG, "Response: " + originalResponse.request().headers().toString()); - + Log.d(TAG, "Response: " + originalResponse.toString()); if(!originalResponse.headers(setCookiesHeaderTag).isEmpty()) { HashSet cookies = new HashSet<>(); for(String header: originalResponse.headers(setCookiesHeaderTag)) { + + // Remove the extra "; HttpOnly; Path=/" behind cookie + int cookieEndIndex = header.indexOf(";"); + if(cookieEndIndex > 0) { + header = header.substring(0, cookieEndIndex); + } + cookies.add(header); Log.d(TAG, "Cookies saved: " + header); } diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java index 258055d..fe92108 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeStream.java @@ -15,9 +15,15 @@ public class WorldScopeStream { private boolean live; private String duration; private String description; - private Date createdAt; - private Date deletedAt; - private WorldScopeUser owner; + private String createdAt; + private String deletedAt; + private String owner; + + /* + private Date createdAt; + private Date deletedAt; + private WorldScopeUser owner; + */ public String getStreamId() {return streamId;} public String getAppInstance() {return appInstance;} @@ -28,9 +34,14 @@ public class WorldScopeStream { public boolean getLive() {return live;} public String getDuration() {return duration;} public String getDescription() {return description;} - public Date getCreatedAt() {return createdAt;} - public Date getDeletedAt() {return deletedAt;} - public WorldScopeUser getOwner() {return owner;} + /* + public Date getCreatedAt() {return createdAt;} + public Date getDeletedAt() {return deletedAt;} + public WorldScopeUser getOwner() {return owner;} + */ + public String getCreatedAt() {return createdAt;} + public String getDeletedAt() {return deletedAt;} + public String getOwner() {return owner;} public void setStreamId(String streamId) {this.streamId = streamId;} public void setAppInstance(String appInstance) {this.appInstance = appInstance;} @@ -41,9 +52,18 @@ public class WorldScopeStream { public void setLive(boolean live) {this.live = live;} public void setDuration(String duration) {this.duration = duration;} public void setDescription(String description) {this.description = description;} - public void setCreatedAt(Date createdAt) {this.createdAt = createdAt;} - public void setDeletedAt(Date deletedAt) {this.deletedAt = deletedAt;} - public void setOwner(WorldScopeUser owner) {this.owner = owner;} + + public void setCreatedAt(String createdAt) {this.createdAt = createdAt;} + public void setDeletedAt(String deletedAt) {this.deletedAt = deletedAt;} + + /* + public void setCreatedAt(Date createdAt) {this.createdAt = createdAt;} + public void setDeletedAt(Date deletedAt) {this.deletedAt = deletedAt;} + public void setOwner(WorldScopeUser owner) {this.owner = owner;} + */ + + public void setOwner(String owner) {this.owner = owner;} + @Override public String toString() { diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java index 5b630fb..8467f3b 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/model/WorldScopeViewStream.java @@ -15,7 +15,7 @@ public class WorldScopeViewStream extends WorldScopeStream{ @Override public String toString() { - return this.toString() + "\n" + return super.toString() + "\n" + "viewLink: " + this.getViewLink() + "thumbnailLink: " + this.getThumbnailLink(); } diff --git a/client/android/WorldScope/app/src/main/res/drawable-hdpi/ic_play_circle_outline.png b/client/android/WorldScope/app/src/main/res/drawable-hdpi/ic_play_circle_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..65df3eebe4b9fc1de1c44e162f761b2c5d4623e0 GIT binary patch literal 1079 zcmV-71jze|P)fRLX@9y%@t9 zwh_Y#ofuuM(J#GdM$UCYDM2T~1R!p6jilU@p!$N=?Ffjzd>)&dvc^&49_m8;e`~!UQ@f*2++XOgcq$<)dFFv1=hQ zNL(%{AE6|QW2CyuR<0UD*iRxkk32VUy!e`+@+XCJu4Ja_!cKzx25HrPJ0hGDypjZS zL3!w-;1?lz^n6D&)_2_3daw#?3Yr#ld}&xvGOhPq|EYk|DX(onJz*>C zO0HK}l4~&7XLx1H`x`HmA@y_m>zxH=xvXrT2G4ljDRze*s)4M3I(CEXD-AespL2av zG+j5>!Z{R@POgOJN&$*1nl6tkyY2SOVXj$#Bp_AOu~lHvzCN?9dzBI(71KRnq>hkn z&FJIWFDL?%H(eT{j8u~~V=u!tp#ezVbW@DPOKY|t(jrl)he!j$Piy# zpI9^IaDiW~2qEo7)l}zOr#_br2$TVt&B*Ro0}?(j?y*U z+!vA)YUG|_69vYW4Tbo@Xg)=qtl&5YX~xhxCIerDo>H1hxxkEL!~mQ$0>H19mA0|1 zLKYrzz0w;KG(J^TF(fFMap6mag0bT0S67mWc|pl0Dz)I$aISoI=5R|#kp~+I-@p15 z7kv4Ycf{{&>x#>FhP(-o&Sw5@$?p{&8j|-CHx19b9P&XL0ka(@0WbcEP)a2Te?>XuckI*%jm52Ki5j#R8)LH+LwtAc+=#!6h3JWfD8|k@ z&sc*<7x)afMN+^x#Nk?$ocZGoekE)dMB*piNuJA^5ppj<{7Rf|StP)Biq=|od1OSy zxi(?~CN-Z$mwr-f;v1pmxszqmZpGyl6Be);^eE27QOw5p^wAihxpuj8LTn{G9=pJo z_z+LxGPV)p(?;V3%{74#ky=9d?gEBS@hWae4%=$jh4G`ze1Xi5DDz8BD@`Yx($qe* zDIo@FKeQar+vm9-S}MA`N`-9|3YIN)0ikayc1@?)nX@14Q?~0YvUJLIpA50(y3N?c zDc6I2%C#Gb&v~Z);kb8VzswQ2ZNE&@zyHmb`6r)#D(xGOFN*<6=2%Yv00001KK)caAg6{Y~8no(nSZeD|Dtzx$mtcYqta4t5KZsG$K5E&Qd?qNfJt zQ6SHM+kGtHJAB1B#4C*T9z2I7TCZ;y9n_KSCHzzTFa{4s3)61FlXxEq@C(Z*B99DG zG_v$m!U|3!m^u=+;csIzH1R9ukXGP|*a;yw=s#AgzYPBe&kXBIqJr~K3X5I5WB3ln zt#qYa>+Nkz6cj4hrwr5EN;UVY9<7Q?)Y6Ow|`mYDl-XIVc&Tg-l|uG*li zo=dzRjT)(P>D`qB7Fd`vE%^9?2L?s$iU=>|EiLFdbJFP-p5K_&rYMOB2k47CC%J0V z$^;ifN7W$C1ZpDgtO?h$V)A9h>Zn>3m}=bB5U!#zfsU%OK&;1Io^a(Y5~!&v2=8{> z)e^3ZWdb=>IpI5w2MB5)Wt~7q)d1JR6(yLlPY_U)wNBu4(F*j`bnOneioC4Z9cpV?Pfe7deOa>uUh~^5DNW!R+I0%w zM1&V|z!=wpgh4Ge(RSI!s0vaSkaM~zxY}cWWxi2^vRrOWdX2l-2=>IxBMj+))2x;) zhZG}JSi!s}CUBjhO}Z)Ak+a8h7R~SkT>yPPi>g#nVdd1Admo?BZ+H3K2-WQ_HIG3> zS(RCs-SBB!P7@)by4Af(!3xwX2WaHzsZ8DGEP`3ITYdPrPpm&cj?70ZvkYAVJI3H` z(*02H-)Z;gmUf^FzE4eI^eR4MCrC}}r1fLkQHTC+)2|A3%)Xm%{Oe%<0MT?VJ0eEu Q-~a#s07*qoM6N<$g6hyvVgLXD literal 0 HcmV?d00001 diff --git a/client/android/WorldScope/app/src/main/res/drawable-tvdpi/ic_play_circle_outline.png b/client/android/WorldScope/app/src/main/res/drawable-tvdpi/ic_play_circle_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc700ddc458770727953125ef68ac6bbb2516a9 GIT binary patch literal 1744 zcmV;>1~2)EP)3G;wPJd0pX?ftUpdTcIEXeYc)=q7wc=qDTp`thtA zbK3EnMyxTbMhGX_LfAz3hL9v&BU~qB2$O`{ggd}(Jj-AX&rJ{x5#GXD5v(2Pjk6dp zC45Hsm2d+a%M%I&jc~UFG|bK8x4gzLgdV~ZglNzh!NklZEGFzGj1eX*Zq_O^ta+8N zi|{yMP7oLoHi-I9gj*#xqMz(EpsRrXw=OBg*7%2Tj_@X-88lR6u(e)6*iT3oA)Ggr zBV-{RtlK9E!-N4~7|#-zlf`fHMHrbP>?14(Exr!c!}Wyka3V4yr?HVF4B-yK1~9Fa z&_s9$Xu`7W>D(loAiPg_7A}pgcwGsR4Py?^ zT|iiaHAb+eOv;#spW!WHew;jS{<7j!Cc&6@;SHi?1~s)uWgR;$MAhu6~h#D8)U4u1O5snbX6)Km2Iy87;?Vp0q=OivF1wXyWW2+p7t=I#@ zt4Y*h2!_!$Izn)D`y~dC3HX!n5#eDUjWECKW4v3F7{)K)B3&7Cp<5}5*rl-f`4w=( z^TC9)g043ZER}uwixK1$Bs$;Y^U0e>9O^JGi9OCDVk|3Kj3K~WspM=zE^g##7NMzK!i@~j8OcSDcU&~0Ow$L3cj&kQk6^2^)QALT z(qvhai0y03WPp)4E`>efLLHEQzZSM2Z!*{iu_s&ZC23kH2d2aw5T|0mU8n?$?(;6F z#6m)!DO$}VS&G}?WIIH(U=sfw4h>2%2I$GmZ-lMTSEpWrY3nq-r+~dYV^zu;P?^1M zFn|TP!wn9tcPR$w@A#YWB@CLAgogEeqv`N8>}8A9H6sWhGA1!fpkNX9hylg`W&oMz zJO?qsN{RaNUUpiQ-WUwiq{;oAMARibW`J&Q*ad$Gy|xp)wGmF4+#&C!*J{Cptz|!I z^+pB^qjcp519X*0!|*<8L-m?K>$HiXeO5gIi`{V;CZmxWK>f}y2I$~C4{x;{=~X2| zTn1aGW%1v|y^EbtIeN8A%)AriONb*jBSWt)MmX|2D&!Xjh!JpCR}Kw;tNJ@NRnNSrV)5l>T>iGp}rh_O}Nx25P}umC)C&D zojyxK9Gm}3%)B0FPqSCJcs;Hg8g4rHR>&J9(olz1;eCTd8^m7KATbDo>pC4QPU{d> z-jRsiI1J+|;Z2K74A%arkhjP%P3H+O2@l)mqKFVjg}hH_3ZdwBl(yY!l3E5oKPuEs zQkTTNE3k;y`WFKe5<=Z3#{mi7_V8WZS|lkEgri}FWe(wg^_aU5v3&<3yo^w{$(_gM zV}2Q-DAZv{lx~!~hzzY0x%x`zvAI#Q4gcGSxqbL;u|~-(iYG7naq=>oupHhYCDd)L z##Y&D$S6;tZZ`-FGPK;O8saEN#@? zIfxj!mUu;Y*DlYo{Z2%94I*G81lz!w-fpDQZhbIu8Cl&D5x@^7WQ@>1nPJs92#;a@ zWF}`&ft>jNCo^M+Hx8hf(JW@cCo`(blK#;Y-|TFG2=~Iu9|QXEtP9oK)nXQZG*v99 mvHICuSX4)KR7Z9AF#ZFPzTVU2bS3Qo0000mY5T*9g=0&*Y&YSx=kHwt3neh0Xd(O;n&OPVcJ3u6n zL=s6Pk=T=^sKp_8xP?B9U=q`8OtPy!+(ZZar50uL)9@nHqa6eIATjPCh3$yV7t{0D zUG2gfDbr_<33Or;L+4bA(TE4irq3ROXh1wS#>dct7e>Zs#&a~IFl_UGz#}WhXT}hA zg=hQ<+^}kVW^`kD80P89)3G=%3+K3FBO?7Gq)vnNAkieG(mC^Giz^-k5K6ZDn;^f^s27e1 z;(j=FY_XuWZc*o73Cf1e6GpCk{X z(mBun9fuzc9autg)%z+Pj9TJ;Dc3Wv$x{m5o=?UkWOn3=un9d=m&x! zp7s(J?0`Czj&kT!Fys~osk7{W8kP8vemxk{$3fOwcR+AV;M_^6Ua-nuFv!b3lel`5fZ?d~+aa-+}Nbl$M}SOr1huy~LiYmzWQu z>zstqq`NSBlY?xt?m*gLA-fg~>EIxTEj!>Tr4q+D^x0e|b4f*QS2zug>gn`DYYwC} z(&;Daory9saitXpJdJ{;N~fR!?s1^~7D~z_bmV|eNlDv!J%$dXwTt?@dhN~aq^(kE z;DD#yR#{DSZI;~`nBah?v>RJV=*t<-igEe0rjwi^Db{cxZPZ%)gzV2 z^-@UB!WPp8V*ZHw{#-Lr(jU?R$;E`gQaKPUr-z7|%b_^XMdp2=To?W)y9BFhlXLTr z2xk3GS6R2BR&Mx5`Jy>W%eW}lf4gHc!Tkx_q=<}gOiLE=7 zz_8D0-IQFBmnG-%d`i$qCMUH-#l^7if$igr0(uGa36ipXY$dF+{gNecKg+4})A&O1 zhMe&=eug|1F2Y=y&+Gj0lxL$v9$Ljp^}AX1^w{G*jYu7Vowvsh=i*UF}k<08IR>om9})K(S%|B^Dj53Sgo zE7AlfejC|BmBMxO;wdKZo{b51)r)I5%eY=+m^(!hNhFa(63HC-7rS%39X?G2lmGw# M07*qoM6N<$f)5GFvj6}9 literal 0 HcmV?d00001 diff --git a/client/android/WorldScope/app/src/main/res/drawable-xxhdpi/ic_play_circle_outline.png b/client/android/WorldScope/app/src/main/res/drawable-xxhdpi/ic_play_circle_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..db87c21ab498a41c7732bb970b31506f5a0b93ff GIT binary patch literal 2236 zcmV;t2t)UYP)AXH3YUrH1%{libz25UO=$78&p_L#o9LEkxw%~(#qkx#$*KSC>vL^*a7 z1&t2cb^42PjG(`6!RU%I?7>4v#58RWh^SI@X=&87BWg~N^E3*z7vz+4(Y9rfGYnPu zTa235+apwBXmh3}4TY%DLe1;#ET)jAW`_u-|xD2z_@k{q!_tW^QDEi?@P8PYy0QH2w}HsI$@6 z=Fl8LE~Tv7aMz)h^XL`?dIsT^1Ec@MN=iA?Tn+)Fu*#9Po5=H6wmaw# zB7fchB-Y;ZR^lX)YXirJi z2JEIc%ui%dII6a=SZ_f?J!!L*;#|Lxf$bKVzP4gWnnk^5Yd}g>Q45OB=(6)03+GBl zP>_MWN?X6yQ)_;t?3yzXBxEVO>jye&*SnMhdnW`9U2sid@pfr-W0hfH9)gI$%FuC? zq^gK=o@)+*iuuZ*QE;kpx-#bqGWIAepDZ`LUr}W2Zd1}q>PYpYGw!gq*Gj!Zg^EwX zeL)am+pBq-7JyV@s;+UF4O z;;UHU>pjVRB}c!jRkF{p(W~fo?CP)vkL9N~*NW+NwH|oN)*0J)6J5q~zt*GCP37rz zwT6N6fju50$PWwtzop)6M3#Ml6`;?nhDb5YWb`# zm&?)1+2v1Z1_+XOwJ7c}nijhg$W7;N5i!@^W&>GQq7L!;T1sS(2j$+=nt_vMD2IXY`74hfQTwX=-$8O}wmU_`C< zi69wQTg%8^=3LYkM${tT2$FEMC5+@v&P5$#L{0ILAn&d=O)jUeWke13nILbj#ubn+ z$wfgueJ99^tMy_eUUe>t+d|SV00g;rH7>fl>s-`hMpR}%2y*9YS?oLM7+fte8W@7y zxLTC0E4EOAO8g@z_%0Ur*TuL_x_v+hiuv712YZ+=aj8{sn6K40o9&UHcrXIRqXA7d zVp>v-L(WBQmaX3kC95SKwJw!QR+k5^+z}TjcdU~ucW?!>(>@Ut6DgS0$Q8`;*=aUE zO0Fd$I@8$GA%;5_)`2xQ*DyhGv7-Gz$)Y`QUF*7Nv#ymyndGz&KXJIt7(r65l|r@b z#9=ubmPd^c6c?U)PDh<==rTg^s9ey1DP3*8df zJZH|elE`3>AEX5%Ug*1QU2(q_g7mmn5(n5?qNmM($Jk}S zeZ&chYg`80U(0?I!dzDH24R9?S{IU6>Ts|G+Ok`DpCCt!Af2w2Ku`9png?jDkNXNC zg5ugYvs7t%hJP_su^noF06{VB+oAFl+o1+YC^*ZCKjM)f-L93yN!I?8G76?Dg1RCP z1jUVP$y6Q3GEqiF8a)c9;IfszxhRHOPzw;uywm#B^DOc2|u{PTXb(j-IZ{t4+4bE9|a3FZ#TZF4sO< zD?5dHysty)LS>O_uiIT$8qkW}fSGLb^r%h48Kv6q#THk2LsZ!vB*H^&S4{L^_d28~ z$_T*QQ*ChydX7%@#TKRZy*~Fvtu36Z5ufWep~CW@bpJ-QuF9@i;y45wcpDcijJPJY z8sBN$V|8cQIWzZ@b;C^yLmDvOI6VuMVPWzTxA#mLlV{@^wLM-=G;o}f?zeH*#>XZ< zuL+i)S7LW`u@PHl+PF~S3C5XzrKj0CPyV>(&n-_;4tv#}$sOqC1Vy$edVv@|)S|y9 zs%&06@P%K)YM~8BOP(lSA+%RIh5o3tePzXD_=xegZ@qj;A>0&!NIQd^`+$RMkjBAh z?wsef4K<~B#ZP(3?Tzp(~!GY08D&^?4z-*! z7x!34*A)(pPdUI@dMB2U(+oo|bfi2kaeY=6ebtM;GYw0z4mB?CEn6G3TYZZyGJTV+ zPJc@3t{8GA5cHhXThQ=UQd{8jMSk^|7xWufyWk3c?HBop9G!?z z{90>_oXe84q$Wu!z$pV=^2FwK&vR1@!A4ED_dcZPYYt$1#9*7=amBhE3#tmx!>t z)K@*H_K3c^O5Zs|KUqeT=5SV6s4w?f&Zug0000< KMNUMnLSTaN5j2qi literal 0 HcmV?d00001 diff --git a/client/android/WorldScope/app/src/main/res/drawable-xxxhdpi/ic_play_circle_outline.png b/client/android/WorldScope/app/src/main/res/drawable-xxxhdpi/ic_play_circle_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d41b9810c3a47f29eb9af2a37dc1b18a8dc6f9 GIT binary patch literal 3057 zcmVG0000ZPNklBAK@*IRy0Dg3(A}5~DK111 zAJNYh*+o&o00l*Pme=AYi~E&C0=)r;>O7e9GJd}6WqWWM+fkI2ZyEqD@d766*9IF&4{lG{p1H8ErU%3Spc)u87fCA)fh58b*yUOryNOyDqTv zQLRPvUB?03;}>dRI9B63?ZWRI4R{vAdam#%;w@p9FBu-K_!}nlJmKGnD&wN>I<{ea z&k=q;wpu0pPG|KUjOhs;{yGcHPWR?$O`OBFbS%bsYlYuAzQF@Xi>Ki^V!iM?M>X=| zCH!kdkk#yr_*0`<#LT_oBK}c0?v(I5$5D)kgZLNXzfOujHGaSX*OLqzB)W;U*pBB= ziek(_0S2J2`1=`RWQnf^i0{l0KPkmp>_DBQJZfsZicD9+7Oy!_!Df6RfrF3TOjpc|=yEpW13Ze+k@?8OW7v(Bh_2~$*TjEM1h1zRJB97-CzYQZ z+=pEeLcohCaY_77hMs60mSLbWz6J^B^0Uxu&=YP5KMkuxPVpda6VbBzc>H!83dv!m z3+aZJLrtv;leF?V1@DLAbG_YZiu6!!=N?Qp!tFF12*K@Z_NpK)#M6HozcI>lu~>D2 z^8A9W3Rn}^v#7u?jB}ldrviIytK#7KR9m$Dk&nXxxwp!i|dDNff5u}bIEcOfIVk4eWW%H_7No_HNf#80%Cv zcO2@OGy7hwfrfAmd$0GLguK3PC7^#CQO0LC}Mg<2X(vGicr|%@TUpG+>)6jERHY(>cv_Dy?0&FlvMGPt2t%fw+(WFWN)ho4| zF>8u;lXBRvg6tTE<|k=UfN4`Sl_7~=n#c(rR5IP}tCY=|qQfTgz;rjO+q5dcq$!$1 z%Hk8*B*!ui7o}@ffH_l?j?YPXNN#qpoFuqR(PZfZ6DCZNc6l5nXuA1It?kdrrhP0?Omu4pb<8S|(m3eancmXPhk`^q773pwf07AZiVDWYSe z*>VU`=O520oo1Nx?7B=*y`EU$ZGy~O)+s=jDcVBtut^S?Qv{iZtyF*xQ&dWDa8d@5 zEHVh2VyObuo1$rCFxERdkpi+9=~`i zc18gzOwpa{>qMxtz%v|DfP7Oli=cKaI*D3>M1f-pkZXzx30h~==Q{>CrvRCzXdpr9 z%jhI75G49Kr~rAUD2Kc#D>9l05?PKaK#nQOCf~KlpfW)j&MH8LDas(TL0$n@Rlt1v z6%bz&lyt)c#dQ%*xUvYx(+!o?Xl#&rH#B~MSkkRPEN-cYglnmYcqQXx_c)}IaXX@u zcq2CH=%kzJXlg@dts@FZxVj;;Sp7?;Tmg;B+N)9qTEdnjwJBd`y#kU7H|77WzBn|4%qvFoa)5(Kdy=9R3lpynnr3y$YFHL-$On6JdW}}_b zY&1Qcpt|QtD$b?K)4#Az0galP%2U0i%jA%`S@)sjhzdw*I-pTVaw6$~Mh3l>6VNy41sp}frFvyq!k_MV}3?&~)$?dD? z&dC*q6p%2|u7>U(RwkEV0ZH&PBMqC(+~So^9!UD z>HC5YX;nbNVAH;1x(!eYql~m|g;l19mxNA@5h@7NO?%(hqJX5~#;touIqX$Km~(6P zH026N7;arWlOct9YDi}>?v>lGQ~_p9PX`V$?hfp&iukh(?axuBfTRWO>q{6Cs8mOO z7~=wmTFK4ZZ81G5WHbE#77SL&!Y0NO@RW1~*kF26c!n|i4Qg4Kz+j;n`BD{-wB!<> zLe^bk$}h6wT>+`zfgvt7@{?e8#HX~o1?iGK>~ z{>)8!m>I*eg{O61@2VKNsAJuqIZ_uxYgqeK+hTH^Z8}*4t=#|rIdbp~i>2kUP{1Ep zbFEi?Ui30Nz+$RhM2@cGUS?6iLcMIIF>)du7kf^3TKqFbRKuC)KKe~Q=}KL0Ya><7bpydn+U3Sr=dy zO-ibuLZgJWOYl>&x?E8uX;@?kamDsT0~MFn zmycKNcBIonyagQxU8id0vxre*d*zdl4(#%+3fbM^Fy?6RjnYxfsPA{o_W!>})E;t* zpJO?ODC29Wa5m3| zDV#QWeLv4)C)SI%;!eyG&-Fm$h#)FMjBN4MAn~19;wMYQCw7so^n#2R)g8CcgZcPg z=^X$n#|87-Eiy2IY;<7fs5ZOVC=FUzF`Dp%xvh3J$P-VLE8^d8rp`xwnqm>z z=Fa@6$HJHt6?fwvu8!!Fs~jz;)LUX}24k=}bgDw#XvZdf_5J2C4)0kd{&)2pF0u>b z@mCAXQ42PxKiOz6hGG@Y8x#I{Ve<#0$0HMW3&&5l#ovp07VJn{gpndx`9^=@bK{P@bK{P@bK{P@bK{P@bK{Ph!6e;Ekf?3&Jn#*00000NkvXXu0mjf{Fdqw literal 0 HcmV?d00001 diff --git a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml index d8ae334..a770ec9 100644 --- a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml +++ b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_list_item.xml @@ -8,12 +8,27 @@ android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.litmus.worldscope.MainActivity$PlaceholderFragment"> + + - + + + + Date: Mon, 8 Feb 2016 12:57:51 +0800 Subject: [PATCH 10/43] Remove GIF player dependency because it cause regression with JavaCV --- client/android/WorldScope/app/build.gradle | 5 ++--- .../app/src/main/res/layout/activity_facebook_login.xml | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/client/android/WorldScope/app/build.gradle b/client/android/WorldScope/app/build.gradle index e11ea6f..be14a07 100644 --- a/client/android/WorldScope/app/build.gradle +++ b/client/android/WorldScope/app/build.gradle @@ -22,6 +22,7 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' + //JavaCV compile group: 'org.bytedeco', name: 'javacv', version: '1.0' compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '2.4.9-0.9', classifier: 'android-arm' @@ -30,7 +31,7 @@ dependencies { compile 'com.android.support:design:23.1.1' compile 'com.android.support:support-v4:23.1.1' compile 'com.google.android.exoplayer:exoplayer:r1.5.1' - compile 'com.facebook.android:facebook-android-sdk:4.+' + compile 'com.facebook.android:facebook-android-sdk:4.9.0' //Facebook compile 'com.android.support:appcompat-v7:23.1.1' //Retrofit - RESTful client @@ -40,6 +41,4 @@ dependencies { compile 'com.google.code.gson:gson:2.4' //Picasso - Android Image Loader compile 'com.squareup.picasso:picasso:2.5.2' - //Android GIF Drawable - compile 'pl.droidsonroids.gif:android-gif-drawable:1.1.+' } diff --git a/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml b/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml index 44568de..20b583e 100644 --- a/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml +++ b/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml @@ -5,11 +5,6 @@ android:layout_height="fill_parent" tools:context="com.litmus.worldscope.FacebookLoginActivity"> - - Date: Mon, 8 Feb 2016 23:13:54 +0800 Subject: [PATCH 11/43] Refactor and reorder Gradle dependencies --- client/android/WorldScope/app/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/android/WorldScope/app/build.gradle b/client/android/WorldScope/app/build.gradle index be14a07..2d9b0c0 100644 --- a/client/android/WorldScope/app/build.gradle +++ b/client/android/WorldScope/app/build.gradle @@ -28,12 +28,12 @@ dependencies { compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '2.4.9-0.9', classifier: 'android-arm' compile group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '2.3-0.9', classifier: 'android-arm' //Exoplayer - compile 'com.android.support:design:23.1.1' - compile 'com.android.support:support-v4:23.1.1' compile 'com.google.android.exoplayer:exoplayer:r1.5.1' - compile 'com.facebook.android:facebook-android-sdk:4.9.0' - //Facebook compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-v4:23.1.1' + compile 'com.android.support:design:23.1.1' + //Facebook + compile 'com.facebook.android:facebook-android-sdk:4.9.0' //Retrofit - RESTful client compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3' compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta3' From 392dcec74497e14ada8c1667b228c96a3af2e17e Mon Sep 17 00:00:00 2001 From: kylelwm Date: Mon, 8 Feb 2016 23:14:46 +0800 Subject: [PATCH 12/43] Reimplemented playing of GIF to use webview instead of regression library --- .../welcomeGifAssets}/welcome.gif | Bin .../src/main/assets/welcomeGifAssets/welcome.html | 5 +++++ .../litmus/worldscope/FacebookLoginActivity.java | 8 ++++++++ .../src/main/res/layout/activity_facebook_login.xml | 8 ++++++++ 4 files changed, 21 insertions(+) rename client/android/WorldScope/app/src/main/{res/drawable => assets/welcomeGifAssets}/welcome.gif (100%) create mode 100644 client/android/WorldScope/app/src/main/assets/welcomeGifAssets/welcome.html diff --git a/client/android/WorldScope/app/src/main/res/drawable/welcome.gif b/client/android/WorldScope/app/src/main/assets/welcomeGifAssets/welcome.gif similarity index 100% rename from client/android/WorldScope/app/src/main/res/drawable/welcome.gif rename to client/android/WorldScope/app/src/main/assets/welcomeGifAssets/welcome.gif diff --git a/client/android/WorldScope/app/src/main/assets/welcomeGifAssets/welcome.html b/client/android/WorldScope/app/src/main/assets/welcomeGifAssets/welcome.html new file mode 100644 index 0000000..45ae169 --- /dev/null +++ b/client/android/WorldScope/app/src/main/assets/welcomeGifAssets/welcome.html @@ -0,0 +1,5 @@ + + + + Welcome GIF + \ No newline at end of file diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java index 8888996..9560ace 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; +import android.webkit.WebView; import android.widget.Toast; import com.facebook.AccessToken; @@ -19,6 +20,7 @@ public class FacebookLoginActivity extends AppCompatActivity implements FacebookLoginFragment.OnFragmentInteractionListener { private static final String TAG = "FacebookLoginActivity"; + private static final String WELCOME_GIF_LINK = "file:///android_asset/welcomeGifAssets/welcome.html"; private static final String APP_SERVER_AUTH_FAILED_MSG = "Authentication with WorldScope's server has failed, please check that you have internet connections and try again."; private static Context context; private FacebookLoginFragment facebookLoginFragment; @@ -29,6 +31,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_facebook_login); context = this; + loadGifIntoWebView(); } @Override @@ -80,4 +83,9 @@ private void logoutOfFacebook() { facebookLoginFragment.logoutFromFacebook(); } + + private void loadGifIntoWebView() { + WebView welcomeGifWebView = (WebView) findViewById(R.id.welcomeGifWebView); + welcomeGifWebView.loadUrl(WELCOME_GIF_LINK); + } } diff --git a/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml b/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml index 20b583e..1e56639 100644 --- a/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml +++ b/client/android/WorldScope/app/src/main/res/layout/activity_facebook_login.xml @@ -5,6 +5,14 @@ android:layout_height="fill_parent" tools:context="com.litmus.worldscope.FacebookLoginActivity"> + + Date: Mon, 8 Feb 2016 23:15:05 +0800 Subject: [PATCH 13/43] Added minimizing of app when back button is pressed in MainActivity --- .../src/main/java/com/litmus/worldscope/MainActivity.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java index b8621c3..5ee749b 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java @@ -19,6 +19,7 @@ import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -112,7 +113,10 @@ public void onBackPressed() { if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { - super.onBackPressed(); + Intent startMain = new Intent(Intent.ACTION_MAIN); + startMain.addCategory(Intent.CATEGORY_HOME); + startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(startMain); } } From 05f125cd7a6bf475ced5b100f20f50fdfd439bcf Mon Sep 17 00:00:00 2001 From: kylelwm Date: Tue, 9 Feb 2016 18:19:09 +0800 Subject: [PATCH 14/43] Refactor FacebookLoginActivity's redirection to MainActivity to a method --- .../litmus/worldscope/FacebookLoginActivity.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java index 9560ace..b6e7c27 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/FacebookLoginActivity.java @@ -50,9 +50,8 @@ public void onResponse(Response response) { Log.d(TAG, "" + response.body().toString()); // Redirect to MainActivty if successful - Intent intent = new Intent(context, MainActivity.class); - intent.putExtra("loginUser", response.body()); - startActivity(intent); + redirectToMainActivity(response.body()); + } else { Log.d(TAG, "Failure" + response.code() + ": " + response.body().toString()); // Logout of Facebook @@ -70,6 +69,13 @@ public void onFailure(Throwable t) { } + //Redirects to MainActivity + protected void redirectToMainActivity(WorldScopeUser user) { + Intent intent = new Intent(context, MainActivity.class); + intent.putExtra("loginUser", user); + startActivity(intent); + } + // Called to logout of Facebook when attempt to authenticate with App server fails private void logoutOfFacebook() { if(facebookLoginFragment == null) { @@ -84,6 +90,7 @@ private void logoutOfFacebook() { } + // Method to load Gif's html data into WebView private void loadGifIntoWebView() { WebView welcomeGifWebView = (WebView) findViewById(R.id.welcomeGifWebView); welcomeGifWebView.loadUrl(WELCOME_GIF_LINK); From f3f0c7053f5ebd7a0dbbcf300cfeb96eaba30183 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Tue, 9 Feb 2016 18:19:35 +0800 Subject: [PATCH 15/43] Account for case where no user is loaded into MainActivity --- .../com/litmus/worldscope/MainActivity.java | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java index 5ee749b..2cfe52a 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/MainActivity.java @@ -52,6 +52,8 @@ public class MainActivity extends AppCompatActivity private final int NUMBER_OF_TABS = 3; + private static final Boolean IS_LOGOUT_ATTEMPT = true; + /** * The {@link android.support.v4.view.PagerAdapter} that will provide * fragments for each of the sections. We use a @@ -73,6 +75,8 @@ public class MainActivity extends AppCompatActivity private Toolbar toolbar; + private Boolean userIsLoaded; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -84,11 +88,19 @@ protected void onCreate(Bundle savedInstanceState) { // Get user information from intent coming from FacebookLoginActivity loginUser = getIntent().getParcelableExtra("loginUser"); - Log.d(TAG, "" + loginUser.toString()); - // Show welcome message - Toast toast = Toast.makeText(context, String.format(WELCOME_MSG, loginUser.getAlias()), Toast.LENGTH_LONG); - toast.show(); + // If user is in Intent + if(loginUser != null) { + Log.d(TAG, "" + loginUser.toString()); + // Set boolean to load user + userIsLoaded = true; + // Show welcome message + Toast toast = Toast.makeText(context, String.format(WELCOME_MSG, loginUser.getAlias()), Toast.LENGTH_LONG); + toast.show(); + } else { + userIsLoaded = false; + } + // Set FAB to redirect to StreamActivity FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); @@ -196,7 +208,7 @@ private void setToolbarTitle() { // Redirects to Facebook Login activity - private void redirectToFacebookLoginActivity(boolean isAttemptLogout) { + protected void redirectToFacebookLoginActivity(boolean isAttemptLogout) { Intent intent = new Intent(this, FacebookLoginActivity.class); if(isAttemptLogout) { @@ -209,7 +221,7 @@ private void redirectToFacebookLoginActivity(boolean isAttemptLogout) { // Redirects to view activity - private void redirectToViewActivity() { + protected void redirectToViewActivity() { Intent intent = new Intent(this, ViewActivity.class); startActivity(intent); } @@ -217,7 +229,7 @@ private void redirectToViewActivity() { // Redirects to stream activity - private void redirectToStreamActivity() { + protected void redirectToStreamActivity() { Intent intent = new Intent(this, StreamActivity.class); startActivity(intent); } @@ -226,6 +238,10 @@ private void redirectToStreamActivity() { // Query Facebook graph API for profile picture and loads Alias and picture into Drawer private void loadUserInfoIntoDrawer() { + if(!userIsLoaded) { + return; + } + TextView alias = (TextView) findViewById(R.id.drawerAlias); final ImageView facebookProfilePicture = (ImageView) findViewById(R.id.drawerFacebookProfilePicture); @@ -276,20 +292,20 @@ public void onResponse(Response response) { Log.d(TAG, "Success!"); Log.d(TAG, "" + response.body().toString()); - redirectToFacebookLoginActivity(true); + redirectToFacebookLoginActivity(IS_LOGOUT_ATTEMPT); } else { Log.d(TAG, "Failure!"); Log.d(TAG, "" + response.code()); Log.d(TAG, "" + response.body().toString()); - redirectToFacebookLoginActivity(true); + redirectToFacebookLoginActivity(IS_LOGOUT_ATTEMPT); } } @Override public void onFailure(Throwable t) { - redirectToFacebookLoginActivity(true); + redirectToFacebookLoginActivity(IS_LOGOUT_ATTEMPT); } }); } From 72dcf88262484f4f8701a33a15b645efed27a7c1 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Tue, 9 Feb 2016 18:20:21 +0800 Subject: [PATCH 16/43] Set up Android Instrumentation test environment for project --- client/android/WorldScope/app/build.gradle | 10 +++++++++- .../WorldScope/app/src/main/AndroidManifest.xml | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/client/android/WorldScope/app/build.gradle b/client/android/WorldScope/app/build.gradle index 2d9b0c0..b4735ca 100644 --- a/client/android/WorldScope/app/build.gradle +++ b/client/android/WorldScope/app/build.gradle @@ -10,6 +10,7 @@ android { targetSdkVersion 23 versionCode 1 versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -23,6 +24,13 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' + //Instrumented Tests + androidTestCompile 'junit:junit:4.12' + androidTestCompile 'com.android.support:support-annotations:23.1.1' + androidTestCompile 'com.android.support.test:runner:0.4.1' + androidTestCompile 'com.android.support.test:rules:0.4.1' + androidTestCompile 'org.mockito:mockito-core:1.+' + androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' //JavaCV compile group: 'org.bytedeco', name: 'javacv', version: '1.0' compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '2.4.9-0.9', classifier: 'android-arm' @@ -41,4 +49,4 @@ dependencies { compile 'com.google.code.gson:gson:2.4' //Picasso - Android Image Loader compile 'com.squareup.picasso:picasso:2.5.2' -} +} \ No newline at end of file diff --git a/client/android/WorldScope/app/src/main/AndroidManifest.xml b/client/android/WorldScope/app/src/main/AndroidManifest.xml index 3ae4130..27a7431 100644 --- a/client/android/WorldScope/app/src/main/AndroidManifest.xml +++ b/client/android/WorldScope/app/src/main/AndroidManifest.xml @@ -50,8 +50,11 @@ android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + + From a7b580c6ed51b24b60ae16d355c21bce6b779976 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Tue, 9 Feb 2016 18:20:39 +0800 Subject: [PATCH 17/43] Remove stub test and add tests for redirections --- .../litmus/worldscope/ApplicationTest.java | 13 ---- .../worldscope/FacebookLoginActivityTest.java | 45 ++++++++++++++ .../litmus/worldscope/MainActivityTest.java | 59 +++++++++++++++++++ 3 files changed, 104 insertions(+), 13 deletions(-) delete mode 100644 client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/ApplicationTest.java create mode 100644 client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/FacebookLoginActivityTest.java create mode 100644 client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/MainActivityTest.java diff --git a/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/ApplicationTest.java b/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/ApplicationTest.java deleted file mode 100644 index 402a725..0000000 --- a/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.litmus.worldscope; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/FacebookLoginActivityTest.java b/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/FacebookLoginActivityTest.java new file mode 100644 index 0000000..1d828e8 --- /dev/null +++ b/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/FacebookLoginActivityTest.java @@ -0,0 +1,45 @@ +package com.litmus.worldscope; + +import android.app.Instrumentation; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import com.litmus.worldscope.model.WorldScopeUser; + +import junit.framework.TestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class FacebookLoginActivityTest extends TestCase{ + + private FacebookLoginActivity facebookLoginActivity; + + + @Rule + public ActivityTestRule activityRule = new ActivityTestRule<>( + FacebookLoginActivity.class); + + @Before + public void setUp() { + facebookLoginActivity = activityRule.getActivity(); + } + + @Test + public void testTrue() { + assertEquals(true, true); + } + + @Test + public void testRedirectToMainActivity() { + String testUserName = "testUserName"; + WorldScopeUser mockUser = new WorldScopeUser(); + Instrumentation.ActivityMonitor am = InstrumentationRegistry.getInstrumentation().addMonitor(MainActivity.class.getName(), null, true); + facebookLoginActivity.redirectToMainActivity(mockUser); + assertEquals(1, am.getHits()); + } +} \ No newline at end of file diff --git a/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/MainActivityTest.java b/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/MainActivityTest.java new file mode 100644 index 0000000..2d238d5 --- /dev/null +++ b/client/android/WorldScope/app/src/androidTest/java/com/litmus/worldscope/MainActivityTest.java @@ -0,0 +1,59 @@ +package com.litmus.worldscope; + +import android.app.Instrumentation; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import junit.framework.TestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MainActivityTest extends TestCase{ + + private static final Boolean IS_LOGOUT_ATTEMPT = true; + private MainActivity mainActivity; + + + @Rule + public ActivityTestRule activityRule = new ActivityTestRule<>( + MainActivity.class); + + @Before + public void setUp() { + mainActivity = activityRule.getActivity(); + } + + @Test + public void testTrue() { + assertEquals(true, true); + } + + @Test + public void testRedirectToStreamActivity() { + Instrumentation.ActivityMonitor am = InstrumentationRegistry.getInstrumentation() + .addMonitor(StreamActivity.class.getName(), null, true); + mainActivity.redirectToStreamActivity(); + assertEquals(1, am.getHits()); + } + + @Test + public void testRedirectToViewActivity() { + Instrumentation.ActivityMonitor am = InstrumentationRegistry.getInstrumentation() + .addMonitor(ViewActivity.class.getName(), null, true); + mainActivity.redirectToViewActivity(); + assertEquals(1, am.getHits()); + } + + @Test + public void testRedirectToFacebookLoginActivity() { + Instrumentation.ActivityMonitor am = InstrumentationRegistry.getInstrumentation() + .addMonitor(FacebookLoginActivity.class.getName(), null, true); + mainActivity.redirectToFacebookLoginActivity(IS_LOGOUT_ATTEMPT); + assertEquals(1, am.getHits()); + } +} \ No newline at end of file From f9c054908ac465b2b6c26a330203d59513e31925 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Thu, 11 Feb 2016 15:16:21 +0800 Subject: [PATCH 18/43] Updated JavaCV version --- client/android/WorldScope/app/build.gradle | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client/android/WorldScope/app/build.gradle b/client/android/WorldScope/app/build.gradle index b4735ca..fae72d0 100644 --- a/client/android/WorldScope/app/build.gradle +++ b/client/android/WorldScope/app/build.gradle @@ -20,6 +20,7 @@ android { } } + dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' @@ -32,21 +33,21 @@ dependencies { androidTestCompile 'org.mockito:mockito-core:1.+' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' //JavaCV - compile group: 'org.bytedeco', name: 'javacv', version: '1.0' - compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '2.4.9-0.9', classifier: 'android-arm' - compile group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '2.3-0.9', classifier: 'android-arm' + compile group: 'org.bytedeco', name: 'javacv', version: '1.1' + compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '3.0.0-1.1', classifier: 'android-arm' + compile group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '2.8.1-1.1', classifier: 'android-arm' //Exoplayer + //Facebook + //Retrofit - RESTful client + //Gson + //Picasso - Android Image Loader compile 'com.google.android.exoplayer:exoplayer:r1.5.1' compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:support-v4:23.1.1' compile 'com.android.support:design:23.1.1' - //Facebook compile 'com.facebook.android:facebook-android-sdk:4.9.0' - //Retrofit - RESTful client compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3' compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta3' - //Gson compile 'com.google.code.gson:gson:2.4' - //Picasso - Android Image Loader compile 'com.squareup.picasso:picasso:2.5.2' } \ No newline at end of file From 837f2801b47b3cb4409a3e78a352dd98355fb648 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Thu, 11 Feb 2016 15:17:10 +0800 Subject: [PATCH 19/43] Refactored streaming functionality of StreamActivity into a fragment and fix camera issues such as rotation and release --- .../com/litmus/worldscope/StreamActivity.java | 464 +------------- .../main/java/layout/StreamVideoFragment.java | 570 ++++++++++++++++++ .../src/main/res/layout/activity_stream.xml | 27 +- .../main/res/layout/fragment_stream_video.xml | 7 + 4 files changed, 608 insertions(+), 460 deletions(-) create mode 100644 client/android/WorldScope/app/src/main/java/layout/StreamVideoFragment.java create mode 100644 client/android/WorldScope/app/src/main/res/layout/fragment_stream_video.xml diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java index 58aa32d..8931e17 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java @@ -1,470 +1,28 @@ package com.litmus.worldscope; -import android.content.Context; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.Camera; -import android.media.AudioFormat; -import android.media.AudioRecord; -import android.media.MediaRecorder; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AppCompatActivity; import android.os.Bundle; -import android.util.DisplayMetrics; +import android.support.v7.app.AppCompatActivity; import android.util.Log; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.FrameLayout; -import android.widget.RelativeLayout; - -import org.bytedeco.javacv.FFmpegFrameRecorder; -import org.bytedeco.javacv.Frame; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ShortBuffer; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public class StreamActivity extends AppCompatActivity { - - final static String CLASS_LABEL = "StreamActivity"; - final static String LOG_TAG = CLASS_LABEL; - final Context mContext = this; - //Change this to stream RMTP - String ffmpeg_link = "rtmp://multimedia.worldscope.tk:1935/live/streamkey"; - - long startTime = 0; - boolean recording = false; - FFmpegFrameRecorder recorder; - - //Set the rotation - final int ROTATION_90 = 90; - - boolean isPreviewOn = false; - - int sampleAudioRateInHz = 44100; - int imageWidth = 320; - int imageHeight = 240; - int frameRate = 30; - /* audio data getting thread */ - AudioRecord audioRecord; - AudioRecordRunnable audioRecordRunnable; - Thread audioThread; - volatile boolean runAudioThread = true; +import layout.StreamVideoFragment; - /* video data getting thread */ - Camera cameraDevice; - CameraView cameraView; +public class StreamActivity extends AppCompatActivity implements StreamVideoFragment.OnStreamVideoFragmentListener{ - Frame yuvImage = null; - - /* layout setting */ - // Set the x border and y border - final int bg_screen_bx = 0; - final int bg_screen_by = 0; - - // Dimensions of the screen - Display display; - Point displayDimensions; - int actualHeight; - - // Set the width and height for the video preview - int bg_screen_width; - int bg_screen_height; - - // Set the width and height for the backdrop - int bg_width; - int bg_height; - - int live_width; - int live_height; - int screenWidth, screenHeight; - - /* The number of seconds in the continuous record loop (or 0 to disable loop). */ - //final int RECORD_LENGTH = 10; - final int RECORD_LENGTH = 0; - Frame[] images; - long[] timestamps; - int imagesIndex; + private static final String TAG = "StreamActivity"; + private StreamVideoFragment.StreamVideoControls control; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_stream); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - initializeLayout(); - } - - // Compute the layout size and set it to displayDimensions - private void initializeLayout() { - - // Get the layout id - FrameLayout root = (FrameLayout) findViewById(R.id.streamFrameLayout); - root.post(new Runnable() { - public void run() { - // Get actual height of the activity - getActualHeight(); - // Start camera preview - initializeCameraPreviewLayout(); - // Add callback for record button - addRecordButtonCallback(); - } - }); - } - private void getActualHeight() { - Rect rect = new Rect(); - Window win = getWindow(); // Get the Window - win.getDecorView().getWindowVisibleDisplayFrame(rect); - // Get the height of Status Bar - int statusBarHeight = rect.top; - // Get the height occupied by the decoration contents - int contentViewTop = win.findViewById(Window.ID_ANDROID_CONTENT).getTop(); - // Calculate titleBarHeight by deducting statusBarHeight from contentViewTop - int titleBarHeight = contentViewTop - statusBarHeight; - Log.i("MY", "titleHeight = " + titleBarHeight + " statusHeight = " + statusBarHeight + " contentViewTop = " + contentViewTop); - - // By now we got the height of titleBar & statusBar - // Now lets get the screen size - DisplayMetrics metrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(metrics); - int screenHeight = metrics.heightPixels; - int screenWidth = metrics.widthPixels; - Log.i("MY", "Actual Screen Height = " + screenHeight + " Width = " + screenWidth); - - // Now calculate the height that our layout can be set - // If you know that your application doesn't have statusBar added, then don't add here also. Same applies to application bar also - int layoutHeight = screenHeight - (titleBarHeight + statusBarHeight); - Log.i("MY", "Layout Height = " + layoutHeight); - - actualHeight = layoutHeight; - } - - // Create the layout - private void initializeCameraPreviewLayout() { - /* get size of screen */ - display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - displayDimensions = new Point(); - - //Set the size of the screen, preview and its background - display.getSize(displayDimensions); - displayDimensions.set(displayDimensions.x, actualHeight); - - Log.i(LOG_TAG, "x: " + displayDimensions.x + " y: " + displayDimensions.y); - - screenWidth = displayDimensions.x; - screenHeight = displayDimensions.y; - bg_screen_width = displayDimensions.x; - bg_screen_height = displayDimensions.y; - bg_width = displayDimensions.x; - bg_height = displayDimensions.y; - live_width = displayDimensions.x; - live_height = displayDimensions.y; - - //Initialize the frame layout - FrameLayout.LayoutParams frameLayoutParam; - RelativeLayout.LayoutParams relativeLayoutParam; - LayoutInflater myInflate; - - myInflate = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - FrameLayout topLayout = new FrameLayout(this); - setContentView(topLayout); - - /* add camera view */ - int display_width_d = (int) (1.0 * bg_screen_width * screenWidth / bg_width); - int display_height_d = (int) (1.0 * bg_screen_height * screenHeight / bg_height); - int prev_rw, prev_rh; - if (1.0 * display_width_d / display_height_d > 1.0 * live_width / live_height) { - prev_rh = display_height_d; - prev_rw = (int) (1.0 * display_height_d * live_width / live_height); - } else { - prev_rw = display_width_d; - prev_rh = (int) (1.0 * display_width_d * live_height / live_width); - } - frameLayoutParam = new FrameLayout.LayoutParams(prev_rw, prev_rh); - frameLayoutParam.topMargin = (int) (1.0 * bg_screen_by * screenHeight / bg_height); - frameLayoutParam.leftMargin = (int) (1.0 * bg_screen_bx * screenWidth / bg_width); - - //Set the camera to portrait mode - cameraDevice = Camera.open(); - cameraDevice.setDisplayOrientation(ROTATION_90); - - Log.i(LOG_TAG, "Camera open"); - cameraView = new CameraView(this, cameraDevice); - topLayout.addView(cameraView, frameLayoutParam); - Log.i(LOG_TAG, "Camera preview start: OK"); - - // TODO: Tweak start buttons - //Add in the button bar into the FrameLayout - relativeLayoutParam = new RelativeLayout.LayoutParams(screenWidth, screenHeight); - RelativeLayout streamButtonBar = (RelativeLayout) myInflate.inflate(R.layout.template_stream_button_bar, null); - topLayout.addView(streamButtonBar, relativeLayoutParam); - } - - // Start recording - private void startRecorder() { - initializeRecorder(); - try { - recording = true; - recorder.start(); - startTime = System.currentTimeMillis(); - audioThread.start(); - - } catch (FFmpegFrameRecorder.Exception e) { - e.printStackTrace(); - } + Log.d(TAG, "Streamer activity created!"); } - // Stop recording - public void stopRecording() { - - runAudioThread = false; - try { - audioThread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - audioRecordRunnable = null; - audioThread = null; - - if (recorder != null && recording) { - - recording = false; - Log.v(LOG_TAG,"Finishing recording, calling stop and release on recorder"); - try { - recorder.stop(); - recorder.release(); - } catch (FFmpegFrameRecorder.Exception e) { - e.printStackTrace(); - } - recorder = null; - - } - } - - // Create recorder - private void initializeRecorder() { - Log.w(LOG_TAG,"init recorder"); - - if (yuvImage == null) { - yuvImage = new Frame(imageWidth, imageHeight, Frame.DEPTH_UBYTE, 2); - Log.i(LOG_TAG, "create yuvImage"); - } - - Log.i(LOG_TAG, "ffmpeg_url: " + ffmpeg_link); - recorder = new FFmpegFrameRecorder(ffmpeg_link, imageWidth, imageHeight, 1); - - // Custom format - recorder.setFormat("flv"); - recorder.setVideoCodec(28); - recorder.setAudioCodec(86018); - recorder.setSampleRate(22050); - recorder.setFrameRate(30.0D); - - // Default format - //recorder.setSampleRate(sampleAudioRateInHz); - //recorder.setFrameRate(frameRate); - // Set in the surface changed method - - Log.i(LOG_TAG, "recorder initialize success"); - - audioRecordRunnable = new AudioRecordRunnable(); - audioThread = new Thread(audioRecordRunnable); - runAudioThread = true; - recorder.setVideoOption("preset", "ultrafast"); - } - - // CameraView class that contains thread to get and encode video data - class CameraView extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback { - - private SurfaceHolder mHolder; - private Camera mCamera; - - public CameraView(Context context, Camera camera) { - super(context); - Log.w("camera","camera view"); - mCamera = camera; - mHolder = getHolder(); - mHolder.addCallback(CameraView.this); - mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); - mCamera.setPreviewCallback(CameraView.this); - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - try { - stopPreview(); - mCamera.setPreviewDisplay(holder); - } catch (IOException exception) { - mCamera.release(); - mCamera = null; - } - } - - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - Camera.Parameters camParams = mCamera.getParameters(); - List sizes = camParams.getSupportedPreviewSizes(); - // Sort the list in ascending order - Collections.sort(sizes, new Comparator() { - - public int compare(final Camera.Size a, final Camera.Size b) { - return a.width * a.height - b.width * b.height; - } - }); - - // Pick the first preview size that is equal or bigger, or pick the last (biggest) option if we cannot - // reach the initial settings of imageWidth/imageHeight. - for (int i = 0; i < sizes.size(); i++) { - if ((sizes.get(i).width >= imageWidth && sizes.get(i).height >= imageHeight) || i == sizes.size() - 1) { - imageWidth = sizes.get(i).width; - imageHeight = sizes.get(i).height; - Log.v(LOG_TAG, "Changed to supported resolution: " + imageWidth + "x" + imageHeight); - break; - } - } - camParams.setPreviewSize(imageWidth, imageHeight); - - Log.v(LOG_TAG,"Setting imageWidth: " + imageWidth + " imageHeight: " + imageHeight + " frameRate: " + frameRate); - - camParams.setPreviewFrameRate(frameRate); - Log.v(LOG_TAG,"Preview Framerate: " + camParams.getPreviewFrameRate()); - - mCamera.setParameters(camParams); - startPreview(); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - try { - mHolder.addCallback(null); - mCamera.setPreviewCallback(null); - } catch (RuntimeException e) { - // The camera has probably just been released, ignore. - } - } - - public void startPreview() { - if (!isPreviewOn && mCamera != null) { - isPreviewOn = true; - mCamera.startPreview(); - } - } - - public void stopPreview() { - if (isPreviewOn && mCamera != null) { - isPreviewOn = false; - mCamera.stopPreview(); - } - } - - @Override - public void onPreviewFrame(byte[] data, Camera camera) { - if (audioRecord == null || audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { - startTime = System.currentTimeMillis(); - return; - } - if (RECORD_LENGTH > 0) { - int i = imagesIndex++ % images.length; - yuvImage = images[i]; - timestamps[i] = 1000 * (System.currentTimeMillis() - startTime); - } - /* get video data */ - if (yuvImage != null && recording) { - ((ByteBuffer)yuvImage.image[0].position(0)).put(data); - - if (RECORD_LENGTH <= 0) try { - Log.v(LOG_TAG,"Writing Frame"); - long t = 1000 * (System.currentTimeMillis() - startTime); - if (t > recorder.getTimestamp()) { - recorder.setTimestamp(t); - } - recorder.record(yuvImage); - } catch (FFmpegFrameRecorder.Exception e) { - Log.v(LOG_TAG,e.getMessage()); - e.printStackTrace(); - } - } - } - } - - // AudioRecordRunnable class that contains thread to get and encode audio data - class AudioRecordRunnable implements Runnable { - - @Override - public void run() { - android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); - - // Audio - int bufferSize; - ShortBuffer audioData; - int bufferReadResult; - - bufferSize = AudioRecord.getMinBufferSize(sampleAudioRateInHz, - AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); - audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleAudioRateInHz, - AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); - - audioData = ShortBuffer.allocate(bufferSize); - - Log.d(LOG_TAG, "audioRecord.startRecording()"); - audioRecord.startRecording(); - - /* ffmpeg_audio encoding loop */ - while (runAudioThread) { - - //Log.v(LOG_TAG,"recording? " + recording); - bufferReadResult = audioRecord.read(audioData.array(), 0, audioData.capacity()); - audioData.limit(bufferReadResult); - if (bufferReadResult > 0) { - Log.v(LOG_TAG,"bufferReadResult: " + bufferReadResult); - // If "recording" isn't true when start this thread, it never get's set according to this if statement...!!! - // Why? Good question... - if (recording) { - try { - recorder.recordSamples(audioData); - //Log.v(LOG_TAG,"recording " + 1024*i + " to " + 1024*i+1024); - } catch (FFmpegFrameRecorder.Exception e) { - Log.v(LOG_TAG,e.getMessage()); - e.printStackTrace(); - } - } - } - } - Log.v(LOG_TAG,"AudioThread Finished, release audioRecord"); - - /* encoding finish, release recorder */ - if (audioRecord != null) { - audioRecord.stop(); - audioRecord.release(); - audioRecord = null; - Log.v(LOG_TAG,"audioRecord released"); - } - } - } - - private void addRecordButtonCallback() { - final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.streamFab); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (!recording) { - fab.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_stop)); - startRecorder(); - } else { - fab.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_videocam)); - stopRecording(); - } - } - }); + @Override + public void streamVideoFragmentReady(StreamVideoFragment.StreamVideoControls control) { + this.control = control; + control.startStreaming(); + Log.d(TAG, "Streamer ready!"); } - } diff --git a/client/android/WorldScope/app/src/main/java/layout/StreamVideoFragment.java b/client/android/WorldScope/app/src/main/java/layout/StreamVideoFragment.java new file mode 100644 index 0000000..3fadc88 --- /dev/null +++ b/client/android/WorldScope/app/src/main/java/layout/StreamVideoFragment.java @@ -0,0 +1,570 @@ +package layout; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.Camera; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import com.litmus.worldscope.R; + +import org.bytedeco.javacv.FFmpegFrameRecorder; +import org.bytedeco.javacv.FFmpegFrameFilter; +import org.bytedeco.javacv.Frame; + +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Fragment containing video streaming functionality from JavaCV + * Contains an interface to be implemented by the Activity containing it + */ +public class StreamVideoFragment extends Fragment { + + private static final String TAG = "StreamVideoFragment"; + private final String ERROR_IMPLEMENT_ON_FRAGMENT_INTERACTION_LISTENER = " must implement OnStreamVideoFragmentListener"; + private final int MAX_SUPPORTED_IMAGE_WIDTH = 1024; + private final int MAX_SUPPORTED_IMAGE_HEIGHT = 768; + private StreamVideoControls controls; + //Change this to stream RMTP + private String ffmpeg_link = "rtmp://multimedia.worldscope.tk:1935/live/streamkey"; + private OnStreamVideoFragmentListener listener; + private Context context; + + private FrameLayout root; + + long startTime = 0; + boolean recording = false; + private FFmpegFrameRecorder recorder; + private FFmpegFrameFilter filter; + + /* From javacpp-presets -> avutil.java */ + private int AV_PIX_FMT_NV21 = 26; + private Frame yuvImage = null; + + // Rotation constant + final int ROTATION_90 = 90; + + // Audio and video initial setting + + boolean isPreviewOn = false; + int sampleAudioRateInHz = 44100; + int imageWidth = 320; + int imageHeight = 240; + int frameRate = 30; + + private Camera cameraDevice; + + /* audio data getting thread */ + AudioRecord audioRecord; + AudioRecordRunnable audioRecordRunnable; + Thread audioThread; + volatile boolean runAudioThread = true; + + private int actualHeight; + + public StreamVideoFragment() { + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @return A new instance of fragment StreamVideoFragment. + */ + // TODO: Rename and change types and number of parameters + public static StreamVideoFragment newInstance() { + StreamVideoFragment fragment = new StreamVideoFragment(); + return fragment; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + this.context = context; + if (context instanceof OnStreamVideoFragmentListener) { + listener = (OnStreamVideoFragmentListener) context; + } else { + throw new RuntimeException(context.toString() + + ERROR_IMPLEMENT_ON_FRAGMENT_INTERACTION_LISTENER ); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "Stream Video Fragment created!"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + + // Get the layout id + View view = inflater.inflate(R.layout.fragment_stream_video, container, false); + root = (FrameLayout) view.findViewById(R.id.streamFrameLayout); + return view; + } + + @Override + public void onStart() { + super.onStart(); + controls = new StreamVideoControls(); + listener.streamVideoFragmentReady(controls); + // Prevent the window from turning dark + getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + initializeLayout(); + } + + @Override + public void onPause() { + super.onPause(); + destroyRecorder(); + } + + // Compute the layout size and set it to displayDimensions + private void initializeLayout() { + root.post(new Runnable() { + public void run() { + // Get actual height of the activity + getActualHeight(); + // Start camera preview + initializeCameraPreviewLayout(); + } + }); + } + + private void getActualHeight() { + Rect rect = new Rect(); + Window win = getActivity().getWindow(); // Get the Window + win.getDecorView().getWindowVisibleDisplayFrame(rect); + // Get the height of Status Bar + int statusBarHeight = rect.top; + // Get the height occupied by the decoration contents + int contentViewTop = win.findViewById(Window.ID_ANDROID_CONTENT).getTop(); + // Calculate titleBarHeight by deducting statusBarHeight from contentViewTop + int titleBarHeight = contentViewTop - statusBarHeight; + Log.i("MY", "titleHeight = " + titleBarHeight + " statusHeight = " + statusBarHeight + " contentViewTop = " + contentViewTop); + + // By now we got the height of titleBar & statusBar + // Now lets get the screen size + DisplayMetrics metrics = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); + int screenHeight = metrics.heightPixels; + int screenWidth = metrics.widthPixels; + Log.i("MY", "Actual Screen Height = " + screenHeight + " Width = " + screenWidth); + + // Now calculate the height that our layout can be set + // If you know that your application doesn't have statusBar added, then don't add here also. Same applies to application bar also + int layoutHeight = screenHeight - (titleBarHeight + statusBarHeight); + Log.i("MY", "Layout Height = " + layoutHeight); + + actualHeight = layoutHeight; + } + + // Create the layout + private void initializeCameraPreviewLayout() { + // Dimensions of the screen + Display display; + Point displayDimensions; + + // Set the width and height for the video preview + int bg_screen_width; + int bg_screen_height; + + // Set the width and height for the backdrop + int bg_width; + int bg_height; + + int live_width; + int live_height; + int screenWidth, screenHeight; + + // x border and y border + final int bg_screen_bx = 0; + final int bg_screen_by = 0; + + /* get size of screen */ + display = ((WindowManager) getActivity().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + displayDimensions = new Point(); + + //Set the size of the screen, preview and its background + display.getSize(displayDimensions); + displayDimensions.set(displayDimensions.x, actualHeight); + + Log.i(TAG, "x: " + displayDimensions.x + " y: " + displayDimensions.y); + + screenWidth = displayDimensions.x; + screenHeight = displayDimensions.y; + bg_screen_width = displayDimensions.x; + bg_screen_height = displayDimensions.y; + bg_width = displayDimensions.x; + bg_height = displayDimensions.y; + live_width = displayDimensions.x; + live_height = displayDimensions.y; + + //Initialize the frame layout + FrameLayout.LayoutParams frameLayoutParam; + RelativeLayout.LayoutParams relativeLayoutParam; + LayoutInflater myInflate; + + myInflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + FrameLayout topLayout = new FrameLayout(context); + getActivity().setContentView(topLayout); + + /* add camera view */ + int display_width_d = (int) (1.0 * bg_screen_width * screenWidth / bg_width); + int display_height_d = (int) (1.0 * bg_screen_height * screenHeight / bg_height); + int prev_rw, prev_rh; + if (1.0 * display_width_d / display_height_d > 1.0 * live_width / live_height) { + prev_rh = display_height_d; + prev_rw = (int) (1.0 * display_height_d * live_width / live_height); + } else { + prev_rw = display_width_d; + prev_rh = (int) (1.0 * display_width_d * live_height / live_width); + } + frameLayoutParam = new FrameLayout.LayoutParams(prev_rw, prev_rh); + frameLayoutParam.topMargin = (int) (1.0 * bg_screen_by * screenHeight / bg_height); + frameLayoutParam.leftMargin = (int) (1.0 * bg_screen_bx * screenWidth / bg_width); + + //Set the camera to portrait mode + cameraDevice = Camera.open(); + + cameraDevice.setDisplayOrientation(ROTATION_90); + + Log.i(TAG, "Camera open"); + CameraView cameraView = new CameraView(context, cameraDevice); + topLayout.addView(cameraView, frameLayoutParam); + Log.i(TAG, "Camera preview start: OK"); + } + + // Create the filter required to rotate the camera output to portrait + private void initializeFilter() { + filter = new FFmpegFrameFilter("transpose=1:portrait", imageWidth, imageHeight); + filter.setPixelFormat(AV_PIX_FMT_NV21); // default camera format on Android + try { + filter.start(); + } catch(FFmpegFrameFilter.Exception e) { + e.printStackTrace(); + } + } + + // CameraView class that contains thread to get and encode video data + class CameraView extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback { + + private SurfaceHolder holder; + private Camera camera; + + public CameraView(Context context, Camera camera) { + super(context); + this.camera = camera; + Log.d(TAG,"CameraView surface created"); + holder = getHolder(); + holder.addCallback(CameraView.this); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + camera.setPreviewCallback(CameraView.this); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + try { + stopPreview(); + camera.setPreviewDisplay(holder); + + } catch (IOException e) { + e.printStackTrace(); + camera.release(); + camera = null; + } + } + + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + Camera.Parameters camParams = camera.getParameters(); + List sizes = camParams.getSupportedPreviewSizes(); + // Sort the list in ascending order + Collections.sort(sizes, new Comparator() { + + public int compare(final Camera.Size a, final Camera.Size b) { + return a.width * a.height - b.width * b.height; + } + }); + + // Pick the first preview size that is equal or bigger, or pick the last (biggest) option if we cannot + // reach the initial settings of imageWidth/imageHeight. + Log.d(TAG, "Sizes: " + (sizes.size())); + for (int i = sizes.size() - 1; i >= 0; i--) { + Log.d(TAG, "Looking at " + sizes.get(i).width + " x " + sizes.get(i).height); + if ((sizes.get(i).width <= MAX_SUPPORTED_IMAGE_WIDTH && sizes.get(i).height <= MAX_SUPPORTED_IMAGE_HEIGHT) || i == 0) { + imageWidth = sizes.get(i).width; + imageHeight = sizes.get(i).height; + Log.d(TAG, "Changed to supported resolution: " + imageWidth + "x" + imageHeight); + break; + } + } + + camParams.setPreviewSize(imageWidth, imageHeight); + + Log.v(TAG,"Setting imageWidth: " + imageWidth + " imageHeight: " + imageHeight + " frameRate: " + frameRate); + + camParams.setPreviewFrameRate(frameRate); + Log.v(TAG,"Preview Framerate: " + camParams.getPreviewFrameRate()); + + camera.setParameters(camParams); + + initializeFilter(); + + startPreview(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + try { + holder.addCallback(null); + camera.setPreviewCallback(null); + } catch (RuntimeException e) { + // The camera has probably just been released, ignore. + } + } + + public void startPreview() { + if (!isPreviewOn && camera != null) { + isPreviewOn = true; + camera.startPreview(); + } + } + + public void stopPreview() { + if (isPreviewOn && camera != null) { + isPreviewOn = false; + camera.stopPreview(); + } + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + if (audioRecord == null || audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { + startTime = System.currentTimeMillis(); + return; + } + + /* get video data */ + if (yuvImage != null && recording) { + /* Try to set data into Frame */ + try { + ((ByteBuffer)yuvImage.image[0].position(0)).put(data); + } catch (BufferOverflowException e) { + /* Reinitialize yuvImage to correct size */ + e.printStackTrace(); + Log.e(TAG, "Incorrect buffer size in yuvImage, resetting to " + imageWidth + " x " + imageHeight); + yuvImage = new Frame(imageWidth, imageHeight, Frame.DEPTH_UBYTE, 2); + ((ByteBuffer)yuvImage.image[0].position(0)).put(data); + } + + try { + Log.v(TAG, "Writing Frame"); + long t = 1000 * (System.currentTimeMillis() - startTime); + if (t > recorder.getTimestamp()) { + recorder.setTimestamp(t); + } + + try { + filter.push(yuvImage); + Frame frame; + while ((frame = filter.pull()) != null) { + recorder.record(frame); + } + } catch(FFmpegFrameFilter.Exception e) { + e.printStackTrace(); + } + + } catch (FFmpegFrameRecorder.Exception e) { + Log.v(TAG,e.getMessage()); + e.printStackTrace(); + } + } + } + } + + // AudioRecordRunnable class that contains thread to get and encode audio data + class AudioRecordRunnable implements Runnable { + + @Override + public void run() { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); + + // Audio + int bufferSize; + ShortBuffer audioData; + int bufferReadResult; + + bufferSize = AudioRecord.getMinBufferSize(sampleAudioRateInHz, + AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); + audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleAudioRateInHz, + AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); + + audioData = ShortBuffer.allocate(bufferSize); + + Log.d(TAG, "audioRecord.startRecording()"); + audioRecord.startRecording(); + + /* ffmpeg_audio encoding loop */ + while (runAudioThread) { + + //Log.v(TAG,"recording? " + recording); + bufferReadResult = audioRecord.read(audioData.array(), 0, audioData.capacity()); + audioData.limit(bufferReadResult); + if (bufferReadResult > 0) { + Log.v(TAG,"bufferReadResult: " + bufferReadResult); + // If "recording" isn't true when start this thread, it never get's set according to this if statement...!!! + // Why? Good question... + if (recording) { + try { + recorder.recordSamples(audioData); + //Log.v(TAG,"recording " + 1024*i + " to " + 1024*i+1024); + } catch (FFmpegFrameRecorder.Exception e) { + Log.v(TAG,e.getMessage()); + e.printStackTrace(); + } + } + } + } + Log.v(TAG,"AudioThread Finished, release audioRecord"); + + /* encoding finish, release recorder */ + if (audioRecord != null) { + audioRecord.stop(); + audioRecord.release(); + audioRecord = null; + Log.v(TAG,"audioRecord released"); + } + } + } + + // Create recorder + private void initializeRecorder() { + Log.w(TAG,"init recorder"); + + if (yuvImage == null) { + yuvImage = new Frame(imageWidth, imageHeight, Frame.DEPTH_UBYTE, 2); + Log.i(TAG, "create yuvImage"); + } + + Log.i(TAG, "ffmpeg_url: " + ffmpeg_link); + recorder = new FFmpegFrameRecorder(ffmpeg_link, imageWidth, imageHeight, 1); + + // Custom format + recorder.setFormat("flv"); + recorder.setVideoCodec(28); + recorder.setAudioCodec(86018); + recorder.setSampleRate(22050); + recorder.setFrameRate(30.0D); + + // Default format + //recorder.setSampleRate(sampleAudioRateInHz); + //recorder.setFrameRate(frameRate); + // Set in the surface changed method + + Log.i(TAG, "recorder initialize success"); + + audioRecordRunnable = new AudioRecordRunnable(); + audioThread = new Thread(audioRecordRunnable); + runAudioThread = true; + recorder.setVideoOption("preset", "ultrafast"); + } + + + // Start recording + private void startRecorder() { + initializeRecorder(); + try { + recording = true; + recorder.start(); + startTime = System.currentTimeMillis(); + audioThread.start(); + + } catch (FFmpegFrameRecorder.Exception e) { + e.printStackTrace(); + } + } + + // Stop recording + private void stopRecorder() { + + runAudioThread = false; + try { + audioThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + audioRecordRunnable = null; + audioThread = null; + + if (recorder != null && recording) { + + recording = false; + Log.v(TAG,"Finishing recording, calling stop and release on recorder"); + try { + recorder.stop(); + recorder.release(); + } catch (FFmpegFrameRecorder.Exception e) { + e.printStackTrace(); + } + recorder = null; + + } + } + + private void destroyRecorder() { + if(cameraDevice != null) { + Log.d(TAG, "Camera released!"); + cameraDevice.release(); + } + runAudioThread = false; + } + + /** + * This set of public functions control the video streamer within the fragment + */ + public class StreamVideoControls { + + public void startStreaming() { + startRecorder(); + } + + public void stopStreaming() { + stopRecorder(); + } + + public void destroyStreamer() { + destroyRecorder(); + } + + } + + public interface OnStreamVideoFragmentListener { + void streamVideoFragmentReady(StreamVideoControls control); + } +} \ No newline at end of file diff --git a/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml b/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml index f672634..e9339da 100644 --- a/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml +++ b/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml @@ -1,11 +1,24 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_height="fill_parent" + android:layout_width="fill_parent" + tools:context="com.litmus.worldscope.StreamActivity" + android:background="#ff0000"> + + + + diff --git a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_video.xml b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_video.xml new file mode 100644 index 0000000..3861935 --- /dev/null +++ b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_video.xml @@ -0,0 +1,7 @@ + + From f8555f17676bc0e20b9be891edc2c9676e0c47bb Mon Sep 17 00:00:00 2001 From: kylelwm Date: Thu, 11 Feb 2016 17:45:18 +0800 Subject: [PATCH 20/43] Changed theme of StreamActivity to fullscreen and allow opening of soft keyboard to resize Layout --- .../android/WorldScope/app/src/main/AndroidManifest.xml | 9 +++++---- .../WorldScope/app/src/main/res/values/colors.xml | 1 + .../WorldScope/app/src/main/res/values/styles.xml | 7 +++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/client/android/WorldScope/app/src/main/AndroidManifest.xml b/client/android/WorldScope/app/src/main/AndroidManifest.xml index 27a7431..014178b 100644 --- a/client/android/WorldScope/app/src/main/AndroidManifest.xml +++ b/client/android/WorldScope/app/src/main/AndroidManifest.xml @@ -37,20 +37,21 @@ - + + android:theme="@style/AppTheme.NoActionBar"/> + #000000 #EB8A3A diff --git a/client/android/WorldScope/app/src/main/res/values/styles.xml b/client/android/WorldScope/app/src/main/res/values/styles.xml index 21184e4..9cb14aa 100644 --- a/client/android/WorldScope/app/src/main/res/values/styles.xml +++ b/client/android/WorldScope/app/src/main/res/values/styles.xml @@ -18,8 +18,11 @@ + + From 12546d83c71bd36a894159eba27f32cb4c7c8343 Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sat, 13 Feb 2016 01:13:44 +0800 Subject: [PATCH 27/43] Refactored StreamActivity to act as a central hub for various fragments instead of just streaming videos --- .../com/litmus/worldscope/StreamActivity.java | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java index b3d63f6..91230dc 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamActivity.java @@ -4,40 +4,122 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import layout.StreamVideoControlFragment; import layout.StreamVideoFragment; public class StreamActivity extends AppCompatActivity implements StreamVideoFragment.OnStreamVideoFragmentListener, - StreamCreateFragment.OnStreamCreateFragmentListener { + StreamCreateFragment.OnStreamCreateFragmentListener, + StreamVideoControlFragment.OnStreamVideoControlFragmentListener{ private static final String TAG = "StreamActivity"; private StreamVideoFragment.StreamVideoControls control; + private StreamCreateFragment streamCreateFragment; + private StreamVideoControlFragment streamVideoControlFragment; + private android.support.v4.app.FragmentManager sfm; + private boolean streamWhenReady = false; + private boolean isRecording = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_stream); + sfm = getSupportFragmentManager(); + + // Get streamCreateFragment + streamCreateFragment = (StreamCreateFragment) sfm.findFragmentById(R.id.streamCreateFragment); + // Get streamVideoControlFragment + streamVideoControlFragment = (StreamVideoControlFragment) sfm.findFragmentById(R.id.streamVideoControlFragment); Log.d(TAG, "Streamer activity created!"); } - // Implement the callback for when streamVideoFragment is ready to stream + @Override + public boolean onTouchEvent(MotionEvent event) { + // On touch, calls the video control fragment back into view if hidden + Log.d(TAG, "Touch detected, showing controls"); + streamVideoControlFragment.restartHideButtonTimerTask(); + return true; + } + + /** + * Implementing StreamVideoFragment + */ @Override public void streamVideoFragmentReady(StreamVideoFragment.StreamVideoControls control) { this.control = control; Log.d(TAG, "Streamer ready!"); + if(streamWhenReady) { + control.startStreaming(); + } } + /** + * Implementing StreamCreateFragment + */ + @Override public void onStreamCreationSuccess(String rtmpLink) { Log.d(TAG, rtmpLink); + + // Find streamVideoFragment and set the rtmp link from streamCreateFragment + StreamVideoFragment streamVideoFragment = (StreamVideoFragment) sfm.findFragmentById(R.id.streamVideoFragment); + streamVideoFragment.setRTMPLink(rtmpLink); + + // If control is ready, start streaming, else stream when ready + if(control != null) { + isRecording = true; + control.startStreaming(); + } else { + streamWhenReady = true; + } + + // Start the streamVideoControls + streamVideoControlFragment.startStreaming(); } @Override public void onCancelStreamButtonClicked() { + control.destroyStreamer(); redirectToMainActivity(); } + + @Override + public void onStreamTerminationResolved(boolean isTerminated) { + if(isTerminated) { + redirectToMainActivity(); + } else { + // Release the controls back + streamVideoControlFragment.unBlockControls(); + } + } + + /** + * Implementing StreamVideoControlFragment + */ + + @Override + public void onStreamRecordButtonShortPress() { + // Signal to StreamVideoFragment to pause the stream + if(isRecording) { + control.stopStreaming(); + } else { + control.startStreaming(); + } + + // Toggle the isRecording boolean + isRecording = !isRecording; + } + + @Override + public void onStreamRecordButtonLongPress() { + // Signal to StreamCreateFragment to do a confirmation and stop stream + streamCreateFragment.confirmStreamTermination(); + } + private void redirectToMainActivity() { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); From 8e68ec7db72beb31562035e235a79c887743fcaa Mon Sep 17 00:00:00 2001 From: kylelwm Date: Sat, 13 Feb 2016 01:14:19 +0800 Subject: [PATCH 28/43] Included fragments into the stream activity UI --- .../app/src/main/res/layout/activity_stream.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml b/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml index c35faac..c9bad0b 100644 --- a/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml +++ b/client/android/WorldScope/app/src/main/res/layout/activity_stream.xml @@ -13,6 +13,14 @@ android:layout_gravity="center_horizontal|bottom" tools:layout="@layout/fragment_stream_video" /> + + Date: Sat, 13 Feb 2016 01:16:01 +0800 Subject: [PATCH 29/43] Fragment in charge of creating and stopping streams, also include UI elements for confirmation --- .../worldscope/StreamCreateFragment.java | 189 +++++++++++++++--- .../res/layout/fragment_stream_create.xml | 104 ++++++++-- 2 files changed, 239 insertions(+), 54 deletions(-) diff --git a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamCreateFragment.java b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamCreateFragment.java index 8adb05a..f0e5526 100644 --- a/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamCreateFragment.java +++ b/client/android/WorldScope/app/src/main/java/com/litmus/worldscope/StreamCreateFragment.java @@ -1,63 +1,110 @@ package com.litmus.worldscope; import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.Toast; + +import com.litmus.worldscope.model.WorldScopeCreatedStream; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; /** - * A simple {@link Fragment} subclass. - * Activities that contain this fragment must implement the - * {@link StreamCreateFragment.OnStreamCreateFragmentListener} interface - * to handle interaction events. - * Use the {@link StreamCreateFragment#newInstance} factory method to - * create an instance of this fragment. + * A simple fragment that creates a stream base on the form a user fill */ public class StreamCreateFragment extends Fragment { + private static final String TAG = "StreamCreateFragment"; + + private static final String WRONG_TITLE_MESSAGE = "Please enter a title"; + + private static final String STREAM_FAILED_MESSAGE = "Failed to get a response from WorldScope servers, please try again later"; + + private static final String STREAM_STARTED_MESSAGE = "You're LIVE now! Tap button to pause and hold to stop streaming"; + + private static final boolean TERMINATE_STREAM = true; + + private static final boolean DO_NOT_TERMINATE_STREAM = false; + + private Context context; private OnStreamCreateFragmentListener listener; + private EditText titleInput; + private EditText descriptionInput; + private View streamCreateView; + private View streamStopView; public StreamCreateFragment() { // Required empty public constructor } - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @return A new instance of fragment StreamCreateFragment. - */ - // TODO: Rename and change types and number of parameters - public static StreamCreateFragment newInstance() { - StreamCreateFragment fragment = new StreamCreateFragment(); - Bundle args = new Bundle(); - fragment.setArguments(args); - return fragment; - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_stream_create, container, false); + titleInput = (EditText)view.findViewById(R.id.titleInput); + + descriptionInput = (EditText)view.findViewById(R.id.descriptionInput); + + // Add functionality to the cancel button view.findViewById(R.id.cancelStreamButton).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { listener.onCancelStreamButtonClicked(); } }); + + // Add functionality to the stream button + view.findViewById(R.id.createStreamButton).setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + + if (validateInput()) { + hideKeyboard(); + createStream(); + } else { + Toast toast = Toast.makeText(context, WRONG_TITLE_MESSAGE, Toast.LENGTH_LONG); + toast.show(); + } + } + }); + + // Add functionality to the confirmStopStream Button + view.findViewById(R.id.confirmStopStreamButton).setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Log.d(TAG, "Record button press detected"); + stopStream(); + } + }); + + // Add functionality to the cancelStopStream Button + view.findViewById(R.id.cancelStopStreamButton).setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Log.d(TAG, "Record button long press detected"); + cancelStopStream(); + } + }); + + streamCreateView = view.findViewById(R.id.streamCreateForm); + + // Hide the streamStopView by default + streamStopView = view.findViewById(R.id.streamStopForm); + streamStopView.setVisibility(View.GONE); + return view; } @@ -65,6 +112,7 @@ public void onClick(View v) { public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnStreamCreateFragmentListener) { + this.context = context; listener = (OnStreamCreateFragmentListener) context; } else { throw new RuntimeException(context.toString() @@ -78,15 +126,91 @@ public void onDetach() { listener = null; } + // Hides the keyboard + private void hideKeyboard() { + //Minimize the soft keyboard + View view = getActivity().getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + // Validate that title is not blank + private boolean validateInput() { + + if(titleInput.getText().toString().length() > 0) { + Log.d(TAG, "Title is not empty"); + return true; + } else { + return false; + } + + } + + // Create a POST request to server for a stream + private void createStream() { + String title = titleInput.getText().toString(); + String description = descriptionInput.getText().toString(); + if(description.length() == 0) { + description = null; + } + + // Instantiate an instance of the call with the parameters + Call call = new WorldScopeRestAPI(context).buildWorldScopeAPIService() + .postStream(new WorldScopeAPIService.PostStreamRequest(title, description)); + // Make call to create stream + call.enqueue(new Callback() { + @Override + public void onResponse(Response response) { + if (response.isSuccess()) { + Log.d(TAG, "Success!"); + Log.d(TAG, "" + response.body().toString()); + listener.onStreamCreationSuccess(response.body().getStreamLink()); + hideStreamCreateView(); + Toast toast = Toast.makeText(context, STREAM_STARTED_MESSAGE, Toast.LENGTH_LONG); + toast.show(); + } else { + Log.d(TAG, "Failure" + response.code() + ": " + response.message()); + Toast toast = Toast.makeText(context, STREAM_FAILED_MESSAGE, Toast.LENGTH_LONG); + toast.show(); + } + } + + @Override + public void onFailure(Throwable t) { + Log.d(TAG, "Failure: " + t.getMessage()); + Toast toast = Toast.makeText(context, STREAM_FAILED_MESSAGE, Toast.LENGTH_LONG); + toast.show(); + } + }); + } + + // Shows the modal asking user if stream should be terminated + public void confirmStreamTermination() { + streamStopView.setVisibility(View.VISIBLE); + } + + // Stop the ongoing stream + private void stopStream() { + // TODO: Implement stop stream to App Server + listener.onStreamTerminationResolved(TERMINATE_STREAM); + } + + // Hide the modal asking user to stop stream + private void cancelStopStream() { + + listener.onStreamTerminationResolved(DO_NOT_TERMINATE_STREAM); + streamStopView.setVisibility(View.GONE); + } + + private void hideStreamCreateView() { + streamCreateView.setVisibility(View.GONE); + } + /** - * This interface must be implemented by activities that contain this - * fragment to allow an interaction in this fragment to be communicated - * to the activity and potentially other fragments contained in that - * activity. - *

- * See the Android Training lesson Communicating with Other Fragments for more information. + * This interface contains the two key functionalities of this fragment and must be implemented + * by any activities containing this fragment */ public interface OnStreamCreateFragmentListener { // Implement to receive update upon stream creation success @@ -95,5 +219,6 @@ public interface OnStreamCreateFragmentListener { // Implement to handle CancelStreamButton void onCancelStreamButtonClicked(); + void onStreamTerminationResolved(boolean isTerminated); } } diff --git a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_create.xml b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_create.xml index 43c204d..42a08e0 100644 --- a/client/android/WorldScope/app/src/main/res/layout/fragment_stream_create.xml +++ b/client/android/WorldScope/app/src/main/res/layout/fragment_stream_create.xml @@ -3,13 +3,70 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" - tools:context="com.litmus.worldscope.StreamCreateFragment" - android:background="#64000000"> + tools:context="com.litmus.worldscope.StreamCreateFragment"> + + + + + + + + + +