diff --git a/.gitignore b/.gitignore index 39fb081..4f6bb6a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ /build /captures .externalNativeBuild +Cucumber.html +Cucumber.json + diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000..5db8926 Binary files /dev/null and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43e..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf3..0000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 22b0b13..f342a0b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -24,7 +24,10 @@ - - + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8397bc0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,61 @@ +language: android +sudo: false +install: + - git fetch --unshallow +addons: + apt: + update: true + sonarcloud: + organization: "eitan12345om-github" + token: + secure: "e97750523d801bb3d958658a3544ddc84487bfb6" + +android: + components: + # Uncomment the lines below if you want to + # use the latest revision of Android SDK Tools + - tools + - platform-tools + + + # The BuildTools version used by your project + - build-tools-28.0.0 + + + # The SDK version used to compile your project + - android-26 + - extra-google-google_play_services + - extra-google-m2repository + - addon-google_apis-google-$ANDROID_API_LEVEL + + licences : + - 'android-sdk-preview-license-.+' + - 'android-sdk-license-.+' + - 'google-gdk-license-.+' + +before_script : + - mkdir -p "$ANDROID_HOME/licenses" + - cp ./licenses/* "$ANDROID_HOME/licenses" + +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + - $HOME/.android/build-cache + - $HOME/.m2/repository + - $HOME/.sonar/cache + - $HOME/.gradle + - .gradle + +script: + - ./gradlew build check + - ./gradlew test + - ./gradlew sonarqube + +before_install: + - chmod +x gradlew + diff --git a/HouseParty.iml b/HouseParty.iml index f0b188b..364d9d0 100644 --- a/HouseParty.iml +++ b/HouseParty.iml @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d0d6982 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Eitan Simler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e99d80d..4de5d36 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# HouseParty \ No newline at end of file +# HouseParty + +![!Travis CI build badge](https://travis-ci.org/eitan12345om/Houseparty.svg) + +Never fight for the aux again. + +## Collaborative Live Queues +- Start a Party or join a friend's. +- Connect with Spotify Premium to queue up your favorite songs. +- Up-vote the songs you want to hear first. +- Hosts can pause/skip songs and control who's in their Party. diff --git a/app/app.iml b/app/app.iml index 5ae8225..da1e63f 100644 --- a/app/app.iml +++ b/app/app.iml @@ -50,13 +50,6 @@ - - - - - - - @@ -64,6 +57,13 @@ + + + + + + + @@ -71,13 +71,6 @@ - - - - - - - @@ -85,87 +78,123 @@ + + + + + + + - - - - - - - + + + - - + + + + + + + + + + + + + + - + + + + - - - - + + + + - - + + - - + - - - + + - + + - - - + + + - + + + + + + + + - - - - + + + + + + + + + + + + + + + + - + - - - + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6be3ce1..8d3d9d4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,11 +4,13 @@ android { compileSdkVersion 26 defaultConfig { applicationId "com.houseparty.houseparty" - minSdkVersion 15 + minSdkVersion 16 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + } buildTypes { release { @@ -16,6 +18,13 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + testOptions { + unitTests.returnDefaultValues = true + } + + lintOptions { + abortOnError false + } } repositories { @@ -27,23 +36,48 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.github.kaaes:spotify-web-api-android:0.4.1' + implementation 'com.github.kaaes:spotify-web-api-android:0.4.1' + implementation 'com.squareup.picasso:picasso:2.71828' + implementation fileTree(dir: 'libs', include: ['*.jar']) + //implementation 'com.squareup.retrofit:converter-gson:2.0.0-beta2' // This library handles authentication and authorization - compile 'com.github.spotify:android-auth:9425c6a140' + implementation 'com.github.spotify:android-auth:9425c6a140' // This library handles music playback - compile 'com.spotify.sdk:spotify-player-24-noconnect:2.20b@aar' + implementation 'com.spotify.sdk:spotify-player-24-noconnect:2.20b@aar' implementation 'com.android.support:appcompat-v7:26.1.0' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' + implementation 'com.android.support:exifinterface:26.1.0' + implementation 'com.android.support:customtabs:26.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.1' implementation 'com.android.support:design:26.1.0' - //implementation 'com.google.firebase:firebase-database:11.0.4' + implementation 'com.android.support:support-annotations:27.1.1' + implementation 'com.google.firebase:firebase-database:16.0.1' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.1' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' - compile 'com.google.firebase:firebase-core:11.8.0' - compile 'com.google.firebase:firebase-database:11.8.0' + implementation 'com.google.firebase:firebase-core:16.0.0' + implementation 'com.google.firebase:firebase-database:16.0.1' + implementation 'com.google.firebase:firebase-auth:16.0.1' + implementation 'com.android.support:design:26.1.0' + implementation 'org.apache.commons:commons-lang3:3.5' + implementation 'org.jsoup:jsoup:1.11.3' + + androidTestImplementation 'info.cukes:cucumber-android:1.2.5@jar' + androidTestImplementation 'info.cukes:cucumber-picocontainer:1.2.4' + + testImplementation 'io.cucumber:cucumber-java:2.4.0' + testImplementation 'io.cucumber:cucumber-junit:2.4.0' + + androidTestImplementation('com.android.support.test:runner:1.0.2') { + // Necessary if your app targets Marshmallow (since the test runner + // hasn't moved to Marshmallow yet) + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2') { + // Necessary if your app targets Marshmallow (since Espresso + // hasn't moved to Marshmallow yet) + exclude group: 'com.android.support', module: 'support-annotations' + } } diff --git a/app/libs/NapsterSdk_1.2.jar b/app/libs/NapsterSdk_1.2.jar new file mode 100644 index 0000000..f10cf72 Binary files /dev/null and b/app/libs/NapsterSdk_1.2.jar differ diff --git a/app/src/androidTest/java/com/houseparty/houseparty/DeletePlaylistTest.java b/app/src/androidTest/java/com/houseparty/houseparty/DeletePlaylistTest.java new file mode 100644 index 0000000..5649623 --- /dev/null +++ b/app/src/androidTest/java/com/houseparty/houseparty/DeletePlaylistTest.java @@ -0,0 +1,43 @@ +package com.houseparty.houseparty; + +import android.support.test.runner.AndroidJUnit4; +import android.view.View; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.swipeLeft; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.withId; + +/** + * @author Eitan created on 6/8/2018. + */ +@RunWith(AndroidJUnit4.class) +public class DeletePlaylistTest { + @Rule + public ActivityTestRule loginActivityActivityTestRule = new ActivityTestRule<>(LoginActivity.class); + + @Test + public void deletePlaylist() { + onView(withId(R.id.playlist1)).perform(swipeLeft()).check(View.visibility("Gone")); + } + + @Test + public void clickPlaylist() { + onView(withId(R.id.playlist2)).click().checkContext(matches(BuilderDialog.this)); + } + + @Test + public void logOut() { + onView(withId(R.id.action_settings)).click(R.id.logout).checkContext(matches(LoginActivity.this)); + } + + @Test + public void addPlaylist() { + int original = onView(playlists.count); + onView(withId(R.id.fab)).click.submit().countPlaylists(matches(original + 1)) + } +} diff --git a/app/src/androidTest/java/com/houseparty/houseparty/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/houseparty/houseparty/ExampleInstrumentedTest.java index 86850ef..173bb72 100644 --- a/app/src/androidTest/java/com/houseparty/houseparty/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/com/houseparty/houseparty/ExampleInstrumentedTest.java @@ -7,7 +7,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.assertEquals; +import static junit.framework.Assert.assertEquals; /** * Instrumented test, which will execute on an Android device. diff --git a/app/src/androidTest/java/com/houseparty/houseparty/LoginTextTest.java b/app/src/androidTest/java/com/houseparty/houseparty/LoginTextTest.java new file mode 100644 index 0000000..ce20597 --- /dev/null +++ b/app/src/androidTest/java/com/houseparty/houseparty/LoginTextTest.java @@ -0,0 +1,39 @@ +package com.houseparty.houseparty; + +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +/** + * @author Eitan created on 6/1/2018. + */ + +@RunWith(AndroidJUnit4.class) +public class LoginTextTest { + @Rule + public ActivityTestRule loginActivityActivityTestRule = new ActivityTestRule<>(LoginActivity.class); + + @Test + public void validateEmailAddress() { + onView(withId(R.id.editText)).perform(typeText("example@email.com")).check(matches(withText("example@email.com"))); + } + + @Test + public void validatePassword() { + onView(withId(R.id.editText2)).perform(typeText("password")).check(matches(withText("password"))); + } + + @Test + public void validatePassword2() { + onView(withId(R.id.passwordConfirm)).perform(typeText("password2")).check(matches(withText("password2"))); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f56796..433029a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,11 @@ + + + + + @@ -20,11 +25,19 @@ - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/houseparty/houseparty/GooglePlaySong.java b/app/src/main/java/com/houseparty/houseparty/GooglePlaySong.java new file mode 100644 index 0000000..41b94ff --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/GooglePlaySong.java @@ -0,0 +1,15 @@ +package com.houseparty.houseparty; + +/** + * Created by jacksonkurtz on 4/12/18. + */ + +public class GooglePlaySong extends Song { + public GooglePlaySong(String title, String uri, String artist) { + super(title, uri, artist); + } + + public void playSong() { + throw new UnsupportedOperationException(); + } +} diff --git a/app/src/main/java/com/houseparty/houseparty/HousePartyPreferences.java b/app/src/main/java/com/houseparty/houseparty/HousePartyPreferences.java new file mode 100644 index 0000000..c3dc932 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/HousePartyPreferences.java @@ -0,0 +1,9 @@ +package com.houseparty.houseparty; + +/** + * @author Eitan created on 5/21/2018. + */ +public class HousePartyPreferences { + public static final String PREF_USERNAME = "username"; + public static final String PREF_PASSWORD = "password"; +} diff --git a/app/src/main/java/com/houseparty/houseparty/LoginActivity.java b/app/src/main/java/com/houseparty/houseparty/LoginActivity.java index c379f44..05ba3b0 100755 --- a/app/src/main/java/com/houseparty/houseparty/LoginActivity.java +++ b/app/src/main/java/com/houseparty/houseparty/LoginActivity.java @@ -1,24 +1,216 @@ package com.houseparty.houseparty; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; +import android.text.InputType; import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Toast; -public class LoginActivity extends AppCompatActivity { +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.AuthResult; +import com.google.firebase.auth.FirebaseAuth; + +import static com.houseparty.houseparty.HousePartyPreferences.PREF_PASSWORD; +import static com.houseparty.houseparty.HousePartyPreferences.PREF_USERNAME; + +public class LoginActivity extends AppCompatActivity implements View.OnClickListener { + + private FirebaseAuth firebaseAuth; + private ProgressDialog progressDialog; + private Button buttonSignIn; + private boolean loginSuccess; + private boolean loginResult = true; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); + buttonSignIn = findViewById(R.id.buttonSignIn); + buttonSignIn.setOnClickListener(this); + ActionBar actionBar = getSupportActionBar(); actionBar.hide(); + + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); + String username = pref.getString(PREF_USERNAME, null); + String password = pref.getString(PREF_PASSWORD, null); + + if (username == null || password == null) { + progressDialog = new ProgressDialog(this); + firebaseAuth = FirebaseAuth.getInstance(); + } else { + finish(); + Intent intent = new Intent(LoginActivity.this, PlaylistActivity.class); + startActivity(intent); + } + } + + public boolean userLogin(final String usernameString, final String passwordString) { + + progressDialog.setMessage("Logging in..."); + progressDialog.show(); + + firebaseAuth.signInWithEmailAndPassword(usernameString, passwordString) + .addOnCompleteListener(LoginActivity.this, new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + progressDialog.dismiss(); + if (task.isSuccessful()) { + Toast.makeText(LoginActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); + + PreferenceManager.getDefaultSharedPreferences(LoginActivity.this) + .edit() + .putString(PREF_USERNAME, usernameString) + .putString(PREF_PASSWORD, passwordString) + .apply(); + + finish(); + Intent intent = new Intent(LoginActivity.this, PlaylistActivity.class); + startActivity(intent); + loginSuccess = true; + + + } else { + Toast.makeText(LoginActivity.this, "Login failed, invalid email or password", Toast.LENGTH_SHORT).show(); + loginSuccess = false; + } + + } + }); + return loginSuccess; } public void playlistPage(View v) { - Intent intent = new Intent(this, MainActivity.class); - startActivity(intent); + } + + public void dialogueBoxRegister(View v) { + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Register"); + + final EditText inputEmail = new EditText(this); + inputEmail.setHint("Email"); + layout.addView(inputEmail); + final EditText inputPasscode = new EditText(this); + inputPasscode.setHint("Password(six characters minimum)"); + layout.addView(inputPasscode); + final EditText inputPasscode2 = new EditText(this); + inputPasscode2.setHint("Confirm password"); + layout.addView(inputPasscode2); + + inputEmail.setInputType(InputType.TYPE_CLASS_TEXT); + inputPasscode.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + inputPasscode2.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + builder.setView(layout); + + builder.setPositiveButton("Submit", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String email = inputEmail.getText().toString(); + String pass = inputPasscode.getText().toString(); + String pass2 = inputPasscode2.getText().toString(); + + Boolean exitCode = registerUser(email, pass, pass2 ); + + if( exitCode ) { + dialog.cancel(); + } + } + }); + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + public boolean registerUser( String email, String pass, String pass2) { + if (!pass.equals(pass2)) { + Toast.makeText(LoginActivity.this, "Registration failed, passwords don't match", Toast.LENGTH_SHORT).show(); + return false; + } else if (pass.length() < 6) { + Toast.makeText(LoginActivity.this, "Registration failed, passwords must be at least 6 characters", Toast.LENGTH_SHORT).show(); + return false; + } else { + + progressDialog.setMessage("Registering User..."); + progressDialog.show(); + + firebaseAuth.createUserWithEmailAndPassword(email, pass) + .addOnCompleteListener(LoginActivity.this, new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + //user successfully registers + Toast.makeText(LoginActivity.this, "Registered Successfully", Toast.LENGTH_SHORT).show(); + progressDialog.hide(); + + + } else { + //user fails to register + Toast.makeText(LoginActivity.this, "Failed to register", Toast.LENGTH_SHORT).show(); + progressDialog.hide(); + + + } + } + }); + } + return true; + } + + public boolean testUserLogin() { + userLogin(); + return true; + } + + public boolean testUserLogin(String user, String pass) { + + firebaseAuth.signInWithEmailAndPassword(user, pass) + .addOnCompleteListener(LoginActivity.this, new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + progressDialog.dismiss(); + if (task.isSuccessful()) { + loginResult = true; + } else { + loginResult = false; + } + + } + }); + + return loginResult; + + } + + @Override + public void onClick(View view) { + if (view == buttonSignIn) { + EditText username = findViewById(R.id.editText); + EditText password = findViewById(R.id.editText2); + final String usernameString = username.getText().toString(); + final String passwordString = password.getText().toString(); + userLogin(usernameString, passwordString); + } } } diff --git a/app/src/main/java/com/houseparty/houseparty/MainActivity.java b/app/src/main/java/com/houseparty/houseparty/MainActivity.java deleted file mode 100755 index 7fb1e8d..0000000 --- a/app/src/main/java/com/houseparty/houseparty/MainActivity.java +++ /dev/null @@ -1,232 +0,0 @@ -package com.houseparty.houseparty; - -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.text.InputType; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListView; - -import com.google.firebase.database.ChildEventListener; -import com.google.firebase.database.DataSnapshot; -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.FirebaseDatabase; -import com.spotify.sdk.android.player.ConnectionStateCallback; -import com.spotify.sdk.android.player.Error; -import com.spotify.sdk.android.player.PlayerEvent; -import com.spotify.sdk.android.player.SpotifyPlayer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; - -public class MainActivity extends AppCompatActivity implements - SpotifyPlayer.NotificationCallback, ConnectionStateCallback { - - private ListView listView; - private List list; - private ArrayAdapter adapter; - private ActionBar actionBar; - private String playlist_name = ""; - private String title = "HouseParty"; - private static Hashtable idTable; - //private static String selected_list; - private static String selected_list; - private FirebaseDatabase pFirebaseDatabase; - private DatabaseReference pPlaylistDatabaseReference; - private ChildEventListener pChildEventListener; - - // Required constants for Spotify API connection. - static final String CLIENT_ID = "4c6b32bf19e4481abdcfbe77ab6e46c0"; - static final String REDIRECT_URI = "houseparty-android://callback"; - - // Used to verify if Spotify results come from correct activity. - static final int REQUEST_CODE = 777; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - pFirebaseDatabase = FirebaseDatabase.getInstance(); - pPlaylistDatabaseReference = pFirebaseDatabase.getReference().child("playlists"); - //pPlaylistDatabaseReference.keepSynced(true); - idTable = new Hashtable(); - - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - actionBar = getSupportActionBar(); - - actionBar.setTitle(title); - - listView = (ListView) findViewById(R.id.listView); - - list = new ArrayList<>(); - - for (int i = 0; i < 20; i++) { - list.add("Playlist_" + i); - } - list = new ArrayList(); - - adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list); - listView.setAdapter(adapter); - actionBar.show(); - - pChildEventListener = new ChildEventListener() { - @Override - public void onChildAdded(DataSnapshot dataSnapshot, String s) { - - HashMap h = (HashMap) dataSnapshot.getValue(); - idTable.put(h.get("name"), dataSnapshot.getKey()); - //System.out.println("Add to list: " + name); - list.add(h.get("name")); - adapter.notifyDataSetChanged(); - } - - @Override - public void onChildChanged(DataSnapshot dataSnapshot, String s) { - } - - @Override - public void onChildRemoved(DataSnapshot dataSnapshot) { - - } - - @Override - public void onChildMoved(DataSnapshot dataSnapshot, String s) { - - } - - @Override - public void onCancelled(DatabaseError databaseError) { - - } - }; - pPlaylistDatabaseReference.addChildEventListener(pChildEventListener); - - - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - selected_list = list.get(i); - Intent intent = new Intent(MainActivity.this, SongActivity.class); - intent.putExtra("CLIENT_ID", CLIENT_ID); - intent.putExtra("REDIRECT_URI", REDIRECT_URI); - intent.putExtra("REQUEST_CODE", REQUEST_CODE); - startActivity(intent); - } - }); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - - return super.onOptionsItemSelected(item); - } - - public void dialogueBox_Playlist(View v) { - System.out.flush(); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Enter Playlist Name:"); - - final EditText input = new EditText(this); - - input.setInputType(InputType.TYPE_CLASS_TEXT); - - builder.setView(input); - - builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - playlist_name = input.getText().toString(); - if (playlist_name.isEmpty()) { - dialog.cancel(); - } else { - selected_list = playlist_name; - Playlist plist = new Playlist(selected_list); - pPlaylistDatabaseReference.push().setValue(plist); - Intent intent = new Intent(getBaseContext(), SongActivity.class); - startActivity(intent); - } - } - }); - builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); - - builder.show(); - } - - public static String selection() { - return selected_list; - } - - public static Hashtable getIdTable() { - return idTable; - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - } - - @Override - public void onLoggedIn() { - Log.d("MainActivity", "User logged in"); - } - - @Override - public void onLoggedOut() { - } - - @Override - public void onLoginFailed(Error error) { - } - - @Override - public void onTemporaryError() { - } - - @Override - public void onConnectionMessage(String s) { - } - - @Override - public void onPlaybackEvent(PlayerEvent playerEvent) { - } - - @Override - public void onPlaybackError(Error error) { - } -} diff --git a/app/src/main/java/com/houseparty/houseparty/NapsterSong.java b/app/src/main/java/com/houseparty/houseparty/NapsterSong.java new file mode 100644 index 0000000..cd88f42 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/NapsterSong.java @@ -0,0 +1,27 @@ +package com.houseparty.houseparty; + +import com.napster.cedar.player.data.Track; + +/** + * Created by jacksonkurtz on 6/5/18. + */ + +public class NapsterSong extends Song { + + protected Track track; + + public NapsterSong() { + super(); + } + + public NapsterSong(String title, String uri, String artist, Track track, String coverArtUrl) { + super(title, uri, artist, coverArtUrl); + this.track = track; + } + + @Override + public void playSong() { + NewSongActivity.napsterPlayer.play(track); + } + +} diff --git a/app/src/main/java/com/houseparty/houseparty/NewSongActivity.java b/app/src/main/java/com/houseparty/houseparty/NewSongActivity.java new file mode 100644 index 0000000..9fbea4d --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/NewSongActivity.java @@ -0,0 +1,618 @@ +package com.houseparty.houseparty; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearSnapHelper; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SnapHelper; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.text.InputType; +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.houseparty.houseparty.napsterSampleLibrary.Search; +import com.napster.cedar.Napster; +import com.spotify.sdk.android.authentication.AuthenticationClient; +import com.spotify.sdk.android.authentication.AuthenticationRequest; +import com.spotify.sdk.android.authentication.AuthenticationResponse; +import com.spotify.sdk.android.player.Config; +import com.spotify.sdk.android.player.ConnectionStateCallback; +import com.spotify.sdk.android.player.Error; +import com.spotify.sdk.android.player.PlayerEvent; +import com.spotify.sdk.android.player.Spotify; +import com.spotify.sdk.android.player.SpotifyPlayer; +import com.squareup.picasso.Picasso; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import kaaes.spotify.webapi.android.SpotifyService; +import kaaes.spotify.webapi.android.models.Track; +import kaaes.spotify.webapi.android.models.TracksPager; +import retrofit.Callback; +import retrofit.RetrofitError; +import retrofit.client.Response; + +public class NewSongActivity extends AppCompatActivity implements + SpotifyPlayer.NotificationCallback, ConnectionStateCallback { + private boolean pause = true; + private RecyclerView recyclerView; + private SongAdapter adapter; + private ArrayList songs; + private Map songIDs; + private String songName; + + private static final String CLIENT_ID = "4c6b32bf19e4481abdcfbe77ab6e46c0"; + private static final String REDIRECT_URI = "houseparty-android://callback"; + private static final int REQUEST_CODE = 777; + + private String accessToken; + public static SpotifyPlayer spotifyPlayer; + private SpotifyService spotify; + + private String currentImage; + + private String host; + private FirebaseUser currentUser; /* Variable to keep track of who I is */ + + private FirebaseDatabase sFirebaseDatabase; + private DatabaseReference songDatabaseReference; + private ChildEventListener sChildEventListener; + + protected Napster napster; + public static com.napster.cedar.player.Player napsterPlayer; + protected com.houseparty.houseparty.napsterSampleLibrary.Metadata metadata; + + @Override + public void onLoggedIn() { + + } + + @Override + public void onLoggedOut() { + + } + + @Override + public void onLoginFailed(Error error) { + + } + + @Override + public void onTemporaryError() { + + } + + @Override + public void onConnectionMessage(String s) { + + } + + @Override + public void onPlaybackEvent(PlayerEvent playerEvent) { + + } + + @Override + public void onPlaybackError(Error error) { + + } + + private interface AsyncCallback { + void onSuccess(String uri); + } + + private interface NapsterCallback { + void onSuccess(com.napster.cedar.player.data.Track track); + } + + private final SpotifyPlayer.OperationCallback mOperationCallback = new SpotifyPlayer.OperationCallback() { + @Override + public void onSuccess() { + Log.d("OperationCallback", "OK!"); + } + + @Override + public void onError(Error error) { + Log.d("OperationCallback", "Error: " + error); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.new_activity_song); + + currentImage = ""; + + setUpButton(); + setUpAdapter(); + + sFirebaseDatabase = FirebaseDatabase.getInstance(); + currentUser = FirebaseAuth.getInstance().getCurrentUser(); + host = getIntent().getExtras().getString("HOST"); + Map t = PlaylistActivity.getIdTable(); + String id = t.get(PlaylistActivity.selection()); + songDatabaseReference = sFirebaseDatabase.getReference().child("playlists").child(id).child("songs"); + songIDs = new HashMap<>(); + + authenticateUser(); + + napster = Napster.register(this, "OTY2ZDkxYTctZDZlYy00MDBkLWE2ZWQtMGQ5YzhhOGIyZjMw", "NTI2NWVjOGYtODY3ZS00YTg5LWIyNWYtZDkyNzY5ODM0Nzcw"); + napsterPlayer = napster.getPlayer(); + metadata = new com.houseparty.houseparty.napsterSampleLibrary.Metadata("OTY2ZDkxYTctZDZlYy00MDBkLWE2ZWQtMGQ5YzhhOGIyZjMw"); + + + sChildEventListener = new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot dataSnapshot, String s) { + HashMap dataTable = (HashMap) dataSnapshot.getValue(); + + songIDs.put(dataTable.get(getString(R.string.title)), dataSnapshot.getKey()); + + songs.add(new SpotifySong( + dataTable.get(getString(R.string.title)), + dataTable.get("uri"), + dataTable.get("artist"), + accessToken, + dataTable.get("coverArtUrl") + )); + + Collections.sort(songs); + + adapter.notifyDataSetChanged(); + } + + @Override + public void onChildChanged(DataSnapshot dataSnapshot, String s) { + Collections.sort(songs); + + adapter.notifyDataSetChanged(); + } + + @Override + public void onChildRemoved(DataSnapshot dataSnapshot) { + HashMap dataTable = (HashMap) dataSnapshot.getValue(); + + songs.remove(new SpotifySong( + dataTable.get("title"), + dataTable.get("uri"), + dataTable.get("artist") + )); + + adapter.notifyDataSetChanged(); + } + + @Override + public void onChildMoved(DataSnapshot dataSnapshot, String s) { + throw new UnsupportedOperationException(); + } + + @Override + public void onCancelled(DatabaseError databaseError) { + throw new UnsupportedOperationException(); + } + }; + songDatabaseReference.addChildEventListener(sChildEventListener); + + ImageView view = findViewById(R.id.imageView2); + view.setImageResource(R.drawable.ic_launcher_background); + } + + public void setUpButton() { + pause = false; + final Button myButton = findViewById(R.id.button3); + myButton.setText(R.string.play); + myButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!pause) { + myButton.setText(R.string.play); + pause = true; + spotifyPlayer.pause(mOperationCallback); + } else { + myButton.setText(R.string.pause); + pause = false; + spotifyPlayer.resume(mOperationCallback); + } + } + }); + } + + public void dialogueBoxSong(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Enter Song Name:"); + + LinearLayout layout = new LinearLayout(this); + final EditText input = new EditText(this); + + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint(R.string.song_name_hint); + layout.addView(input); + final Spinner selectAPI = new Spinner(this); + final ArrayList apiList = new ArrayList(); + apiList.add("Spotify"); + apiList.add("Apple Music"); + apiList.add("Napster"); + apiList.add("Soundcloud"); + apiList.add("Youtube"); + apiList.add("Google Play"); + + ArrayAdapter arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, apiList); + selectAPI.setAdapter(arrayAdapter); + layout.addView(selectAPI); + layout.setOrientation(LinearLayout.VERTICAL); + //layout.setGravity(Gravity.CENTER); + + builder.setView(layout); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + songName = input.getText().toString(); + Log.d("SongActivity: ", songName); + if (songName.isEmpty()) { + dialog.cancel(); + } else { + if (selectAPI.getSelectedItem() == "Spotify") { + spotifySearchForTrack(songName, new NewSongActivity.AsyncCallback() { + @Override + public void onSuccess(String uri) { + Log.i("SongActivity", "this is the uri: " + uri); + Log.i("PushToFirebaseImageURL", currentImage); + + Song song = new SpotifySong(songName, uri, null, accessToken, currentImage); + //Song song = songFactory.createSong(songName, spotify, "spotify"); + songDatabaseReference.push().setValue(song); + Log.i("SongActivity", "This is the song name: " + song.title); + + } + }); + } else if (selectAPI.getSelectedItem() == "Napster") { + napsterSearchForTrack(songName); + + } + } + } + }); + + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + boolean napsterSearchForTrack(String query) { + Log.d("NapsterSearchForTrack", "search for track is called"); + + metadata.queryTrack(query, new Callback() { + + @Override + public void success(Search search, Response response) { + try { + Log.d("NapsterSearchForTrack", "successful search"); + Log.d("NapsterSearchForTrack", "tracks: " + search.toString()); + com.napster.cedar.player.data.Track firstTrack = search.search.get(0); + songs.add(new NapsterSong( + firstTrack.name, + firstTrack.id, + firstTrack.artistName, + firstTrack, + "" + )); + adapter.notifyDataSetChanged(); + napsterPlayer.play(search.search.get(0)); + } catch (Exception e) { + Log.d("NapsterSearchForTrack", e.getMessage()); + + Log.d("NapsterSearchForTrack", "Failed to play napster song"); + + + } + } + + @Override + public void failure(RetrofitError error) { + } + }); + + return true; + } + + boolean spotifySearchForTrack(String query, final NewSongActivity.AsyncCallback callback) { + + query = query.replaceAll(" ", "+"); + + Map options = new HashMap<>(); + options.put("market", "US"); + options.put("limit", 20); + try { + spotify.searchTracks(query, options, new Callback() { + @Override + public void success(TracksPager tracksPager, Response response) { + String songUri = ""; + String imageURL = ""; + try { + List searchResults = tracksPager.tracks.items; + Log.d("SpotifySearchForTrack", "beginning of try block"); + songUri = searchResults.get(0).uri; + try { + String webpageURL = searchResults.get(0).external_urls.get("spotify"); + currentImage = new RetrieveImage().execute(webpageURL).get(); + Log.d("AsyncRetrieveImage", currentImage); + } catch (Exception e) { + Log.d("SpotifySearchForTrack", "JSoup failure"); + Log.d("SpotifySearchForTrack", e.toString()); + } + + } catch (Exception e) { + Log.d("SearchTracksInner: ", "No results"); + } + + + Log.d("FetchSongTask", "1st song uri = " + songUri); + //imageURL = searchResults.get(0).external_urls.get("images"); + callback.onSuccess(songUri); + } + + @Override + public void failure(RetrofitError error) { + error.printStackTrace(); + } + }); + + } catch (IndexOutOfBoundsException e) { + Log.d("SEARCHTRACKS", "No results"); + } + + return true; + + } + + // used in spotifySearchForTrack to scrape web for cover art url + class RetrieveImage extends AsyncTask { + + private Exception exception; + private String imageURL; + + protected String doInBackground(String... args) { + //String imageURL = ""; + try { + Log.d("doInBackgroundArg", args[0]); + Document doc = Jsoup.connect(args[0]).timeout(10000).get(); + imageURL = doc.select("img").get(1).absUrl("src"); + Log.d("ImageForLoop", imageURL); + + } catch (Exception e) { + Log.d("Retrieve Image", e.toString()); + + } + return imageURL; + } + } + + boolean authenticateUser() { + // ---------USER AUTHENTICATION---------- + + AuthenticationRequest.Builder builder = new AuthenticationRequest.Builder(CLIENT_ID, + AuthenticationResponse.Type.TOKEN, + REDIRECT_URI); + builder.setScopes(new String[]{"user-read-private", "streaming", "playlist-modify-public", + "playlist-modify-private", "playlist-read-collaborative", "user-library-read", + "user-library-modify", "user-read-playback-state", "user-modify-playback-state", + "user-read-currently-playing"}); + AuthenticationRequest request = builder.build(); + + AuthenticationClient.openLoginActivity(NewSongActivity.this, REQUEST_CODE, request); + + return true; + + } + + public static Drawable LoadImage(String url) { + try { + InputStream is = (InputStream) new URL(url).getContent(); + Drawable d = Drawable.createFromStream(is, "src name"); + return d; + } catch (Exception e) { + return null; + } + } + + public void printSongs() { + for (Song song : songs) { + Log.d(getClass().getSimpleName(), song.getTitle()); + } + } + + public void setUpAdapter() { + recyclerView = findViewById(R.id.recyclerView); + songs = new ArrayList<>(); + + adapter = new SongAdapter(songs); + adapter.setOnItemClickListener(new SongAdapter.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + Song song = songs.get(position); + authenticateUser(); + songs.set(position, new SpotifySong(song.title, song.uri, song.artist, accessToken, song.coverArtUrl)); + song = songs.get(position); + song.playSong(); + try { + Log.d("CurrentSongCoverURL", song.coverArtUrl); + ImageView currentView = findViewById(R.id.imageView2); + Picasso.get().load(song.coverArtUrl).into(currentView); + } catch (Exception e) { + Log.d("ImageOnPlay", "Failed to get image"); + + } + Snackbar.make(view, "Now playing: " + song.getTitle(), Snackbar.LENGTH_LONG).show(); + Button myButton = findViewById(R.id.button3); + myButton.setText(R.string.pause); + pause = false; + } + }); + + adapter.setOnThumbClickListener(new SongAdapter.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + Song song = songs.get(position); + ImageButton thumbImageButton = view.findViewById(R.id.thumb); + thumbImageButton.setSelected(!thumbImageButton.isSelected()); + + if (thumbImageButton.isSelected()) { + song.addThumbsUp(currentUser.getUid()); + } else { + song.removeThumbsUp(currentUser.getUid()); + } + + Collections.sort(songs); + adapter.notifyDataSetChanged(); + } + }); + + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new GridLayoutManager(getParent(), 1)); + + recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); + + // Add snap scrolling + SnapHelper snapHelper = new LinearSnapHelper(); + snapHelper.attachToRecyclerView(recyclerView); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { + Drawable background = new ColorDrawable(Color.RED); + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(final RecyclerView.ViewHolder viewHolder, int swipeDir) { + // Don't create dialogbox if the user doesn't own the playlist + if (!currentUser.getUid().equals(host)) { + return; + } + + + final int position = viewHolder.getAdapterPosition(); + + final Song song = songs.remove(position); + adapter.notifyItemRemoved(position); + + Snackbar.make(findViewById(R.id.recyclerView), "1 item removed", Snackbar.LENGTH_LONG) + .setAction("undo", new View.OnClickListener() { + @Override + public void onClick(View view) { + songs.add(position, song); + adapter.notifyItemInserted(position); + } + }) + .setActionTextColor(Color.MAGENTA) + .addCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (event == DISMISS_EVENT_TIMEOUT) { + songDatabaseReference.child(songIDs.get(song.getTitle())).removeValue(); + } + } + }) + .show(); + } + + @Override + public void onChildDraw(Canvas c, RecyclerView recyclerView, + RecyclerView.ViewHolder viewHolder, + float dX, float dY, int actionState, + boolean isCurrentlyActive) { + View itemView = viewHolder.itemView; + + // not sure why, but this method get's called for viewholder that are already swiped away + if (viewHolder.getAdapterPosition() == -1) { + // not interested in those + return; + } + + // Don't swipe if the user doesn't own the playlist + if (!currentUser.getUid().equals(host)) { + return; + } + + // draw red background + background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom()); + background.draw(c); + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + }); + + itemTouchHelper.attachToRecyclerView(recyclerView); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + + // Check if result comes from the correct activity + if (requestCode == REQUEST_CODE) { + AuthenticationResponse response = AuthenticationClient.getResponse(resultCode, intent); + if (response.getType() == AuthenticationResponse.Type.TOKEN) { + accessToken = response.getAccessToken(); + Config playerConfig = new Config(NewSongActivity.this, accessToken, CLIENT_ID); + Spotify.getPlayer(playerConfig, NewSongActivity.this, new SpotifyPlayer.InitializationObserver() { + @Override + public void onInitialized(SpotifyPlayer spotifyPlayer) { + NewSongActivity.this.spotifyPlayer = spotifyPlayer; + Log.d("SongActivity", "spotifyPlayer initialized."); + NewSongActivity.this.spotifyPlayer.addConnectionStateCallback(NewSongActivity.this); + NewSongActivity.this.spotifyPlayer.addNotificationCallback(NewSongActivity.this); + Log.d("SongActivity", "spotifyPlayer: callbacks added."); + } + + @Override + public void onError(Throwable throwable) { + Log.e("SongActivity", "Could not initialize player: " + throwable.getMessage()); + } + }); + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/houseparty/houseparty/Playlist.java b/app/src/main/java/com/houseparty/houseparty/Playlist.java index e21a20e..8cc626f 100644 --- a/app/src/main/java/com/houseparty/houseparty/Playlist.java +++ b/app/src/main/java/com/houseparty/houseparty/Playlist.java @@ -1,17 +1,24 @@ package com.houseparty.houseparty; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + import java.util.ArrayList; +import java.util.List; public class Playlist { private String name; - private ArrayList songs; + private String host; + private String passcode; + private List songs = new ArrayList<>(); public Playlist() { } - public Playlist(String name) { + public Playlist(String name, String passcode, String host) { this.name = name; - this.songs = new ArrayList<>(); + this.passcode = passcode; + this.host = host; } public void addSong(Song song) { @@ -22,12 +29,55 @@ public String getName() { return this.name; } - public ArrayList getSongs() { + public String getPasscode() { + return this.passcode; + } + + public String getHost() { + return host; + } + + public boolean isHost(String uid) { + return host.equals(uid); + } + + public List getSongs() { return this.songs; } @Override public String toString() { - return this.name + songs.get(0).getTitle(); + return this.name + songs.get(0).toString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (this.getClass() != obj.getClass()) { + return false; + } + + Playlist playlist = (Playlist) obj; + return new EqualsBuilder() + .append(name, playlist.name) + .append(host, playlist.host) + .append(passcode, playlist.passcode) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 31) + .append(name) + .append(host) + .append(passcode) + .toHashCode(); } } diff --git a/app/src/main/java/com/houseparty/houseparty/PlaylistActivity.java b/app/src/main/java/com/houseparty/houseparty/PlaylistActivity.java new file mode 100644 index 0000000..eed71e0 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/PlaylistActivity.java @@ -0,0 +1,414 @@ +package com.houseparty.houseparty; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.design.widget.Snackbar; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearSnapHelper; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SnapHelper; +import android.support.v7.widget.Toolbar; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.text.InputType; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.houseparty.houseparty.HousePartyPreferences.PREF_PASSWORD; +import static com.houseparty.houseparty.HousePartyPreferences.PREF_USERNAME; + +public class PlaylistActivity extends AppCompatActivity { + + private RecyclerView recyclerView; + private PlaylistAdapter adapter; + private ActionBar actionBar; + /* What is this variable for? */ + private static HashMap idTable; + private String code = ""; + private View currentView; + private FirebaseUser currentFirebaseUser; + public List playlists; + + private String title = "HouseParty"; + private static String selectedList; + private HashMap dataTable; + private String playlistName = ""; + private static String passcode; + private FirebaseDatabase pFirebaseDatabase; + private DatabaseReference pPlaylistDatabaseReference; + private ChildEventListener pChildEventListener; + + // Required constants for Spotify API connection. + static final String CLIENT_ID = "4c6b32bf19e4481abdcfbe77ab6e46c0"; + static final String REDIRECT_URI = "houseparty-android://callback"; + + // Used to verify if Spotify results come from correct activity. + static final int REQUEST_CODE = 777; + + public static String selection() { + return selectedList; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + switch (id) { + case R.id.action_logout: + onClickLogout(); + return true; + case R.id.action_settings: + return true; + default: + break; + } + + return super.onOptionsItemSelected(item); + } + + public static Map getIdTable() { + return idTable; + } + + public void onClickLogout() { + FirebaseAuth.getInstance().signOut(); + PreferenceManager.getDefaultSharedPreferences(PlaylistActivity.this) + .edit() + .putString(PREF_USERNAME, null) + .putString(PREF_PASSWORD, null) + .apply(); + + finish(); + Intent intent = new Intent(PlaylistActivity.this, LoginActivity.class); + startActivity(intent); + } + + public void dialogueBoxInvalidPasscode(View v) { + Snackbar.make(findViewById(R.id.recyclerView), "Invalid passcode", Snackbar.LENGTH_SHORT).show(); + } + + public void dialogueBoxPasscode(View v, final Playlist playlist) { + final String thePassword = playlist.getPasscode(); + Log.d("Passcode of selected: ", thePassword); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Enter passcode:"); + final EditText inputPasscode = new EditText(this); + inputPasscode.setHint("XXXX"); + layout.addView(inputPasscode); + inputPasscode.setInputType(InputType.TYPE_CLASS_TEXT); + + builder.setView(layout); + final View thisView = v; + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String enteredCode = inputPasscode.getText().toString(); + if(!selectPlaylist(playlist, enteredCode) ) { + if (!enteredCode.equals(thePassword)) { + dialogueBoxInvalidPasscode(thisView); + dialog.cancel(); + } + else { + Intent intent = new Intent(PlaylistActivity.this, NewSongActivity.class); + intent.putExtra("CLIENT_ID", CLIENT_ID); + intent.putExtra("REDIRECT_URI", REDIRECT_URI); + intent.putExtra("REQUEST_CODE", REQUEST_CODE); + intent.putExtra("HOST", playlist.getHost()); + startActivity(intent); + } + } + }); + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + public boolean selectPlaylist(Playlist playlist, String enteredCode) { + if (!enteredCode.equals(passcode)) { + return false; + + } else { + return true; + } + } + + public void dialogueBoxPlaylist(View v) { + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Enter Playlist Name:"); + + final EditText inputTitle = new EditText(this); + inputTitle.setHint("Title"); + layout.addView(inputTitle); + final EditText inputPasscode = new EditText(this); + inputPasscode.setHint("Passcode (XXXX)"); + layout.addView(inputPasscode); + + inputPasscode.setInputType(InputType.TYPE_CLASS_NUMBER); + + builder.setView(layout); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + playlistName = inputTitle.getText().toString(); + code = inputPasscode.getText().toString(); + Log.d("NONNUMBERCHECK", Boolean.toString(code.matches("\\d+"))); + selectedList = playlistName; + passcode = code; + if( !createPlaylist( selectedList, passcode )) { + dialog.cancel(); + + } + else { + if (playlistName.isEmpty() || + !code.matches("\\d+") + || code.length() != 4) { + dialog.cancel(); + } else { + selectedList = playlistName; + passcode = code; + Playlist plist = new Playlist(selectedList, passcode, currentFirebaseUser.getUid()); + pPlaylistDatabaseReference.push().setValue(plist); + Intent intent = new Intent(getBaseContext(), NewSongActivity.class); + intent.putExtra("CLIENT_ID", CLIENT_ID); + intent.putExtra("REDIRECT_URI", REDIRECT_URI); + intent.putExtra("REQUEST_CODE", REQUEST_CODE); + intent.putExtra("HOST", currentFirebaseUser.getUid()); + startActivity(intent); + } + + } + }); + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + public boolean createPlaylist(String selectedList, String passcode) { + if (playlistName.isEmpty() || + !code.matches("\\d+") + || code.length() != 4) { + return false; + } else { + Playlist plist = new Playlist(selectedList, passcode, currentFirebaseUser.getUid()); + pPlaylistDatabaseReference.push().setValue(plist); + return true; + } + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + pFirebaseDatabase = FirebaseDatabase.getInstance(); + pPlaylistDatabaseReference = pFirebaseDatabase.getReference().child("playlists"); + currentFirebaseUser = FirebaseAuth.getInstance().getCurrentUser(); + if (currentFirebaseUser == null) { + Log.d("PlaylistActivity", "User not authenticated"); + } + + idTable = new HashMap<>(); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + actionBar = getSupportActionBar(); + + actionBar.setTitle(title); + + recyclerView = findViewById(R.id.recyclerView); + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { + Drawable background = new ColorDrawable(Color.RED); + Drawable delete = getResources().getDrawable(R.drawable.delete); + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(final RecyclerView.ViewHolder viewHolder, int swipeDir) { + // Don't create dialogbox if the user doesn't own the playlist + if (!playlists.get(viewHolder.getAdapterPosition()).isHost(currentFirebaseUser.getUid())) { + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(PlaylistActivity.this); + builder.setTitle("Are you sure you want to delete this playlist?"); + builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + int position = viewHolder.getAdapterPosition(); + pPlaylistDatabaseReference.child(idTable.get(playlists.get(position).getName())).removeValue(); + playlists.remove(position); + adapter.notifyDataSetChanged(); + } + }); + builder.setNegativeButton("No", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + adapter.notifyDataSetChanged(); + } + }); + builder.show(); + } + + @Override + public void onChildDraw(Canvas c, RecyclerView recyclerView, + RecyclerView.ViewHolder viewHolder, + float dX, float dY, int actionState, + boolean isCurrentlyActive) { + View itemView = viewHolder.itemView; + + // not sure why, but this method get's called for viewholder that are already swiped away + if (viewHolder.getAdapterPosition() == -1) { + // not interested in those + return; + } + + // Don't swipe if the user doesn't own the playlist + if (!playlists.get(viewHolder.getAdapterPosition()).isHost(currentFirebaseUser.getUid())) { + return; + } + + // draw red background + background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom()); + background.draw(c); + + delete.setBounds(itemView.getRight() - 140, itemView.getTop() + 25, itemView.getRight() - 20, itemView.getBottom() - 25); + delete.draw(c); + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + }); + + itemTouchHelper.attachToRecyclerView(recyclerView); + + playlists = new ArrayList<>(); + + adapter = new PlaylistAdapter(playlists); + adapter.setOnItemClickListener(new PlaylistAdapter.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + Playlist playlist = playlists.get(position); + selectedList = playlists.get(position).getName(); + String id = idTable.get(selectedList); + dialogueBoxPasscode(view, playlist); + } + }); + + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new GridLayoutManager(getParent(), 1)); + + recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); + + // Add snap scrolling + SnapHelper snapHelper = new LinearSnapHelper(); + snapHelper.attachToRecyclerView(recyclerView); + + actionBar.show(); + + pChildEventListener = new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot dataSnapshot, String s) { + dataTable = (HashMap) dataSnapshot.getValue(); + idTable.put(dataTable.get("name"), dataSnapshot.getKey()); + + playlists.add(new Playlist( + dataTable.get("name"), + dataTable.get("passcode"), + dataTable.get("host") + )); + + adapter.notifyDataSetChanged(); + } + + @Override + public void onChildChanged(DataSnapshot dataSnapshot, String s) { + Log.i("PlaylistActivity", "Child Changed!"); + + adapter.notifyDataSetChanged(); + } + + @Override + public void onChildRemoved(DataSnapshot dataSnapshot) { + dataTable = (HashMap) dataSnapshot.getValue(); + + playlists.remove(new Playlist( + dataTable.get("name"), + dataTable.get("passcode"), + dataTable.get("host") + )); + + adapter.notifyDataSetChanged(); + } + + @Override + public void onChildMoved(DataSnapshot dataSnapshot, String s) { + throw new UnsupportedOperationException(); + } + + @Override + public void onCancelled(DatabaseError databaseError) { + throw new UnsupportedOperationException(); + } + }; + pPlaylistDatabaseReference.addChildEventListener(pChildEventListener); + } +} diff --git a/app/src/main/java/com/houseparty/houseparty/PlaylistAdapter.java b/app/src/main/java/com/houseparty/houseparty/PlaylistAdapter.java new file mode 100644 index 0000000..c8f452d --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/PlaylistAdapter.java @@ -0,0 +1,101 @@ +package com.houseparty.houseparty; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; + +import java.util.List; + +/** + * @author Eitan created on 5/17/2018. + */ +public class PlaylistAdapter extends + RecyclerView.Adapter { + + private List mPlaylists; + + private FirebaseUser currentUser; + + // Define listener member variable + private OnItemClickListener listener; + + public PlaylistAdapter(List playlists) { + mPlaylists = playlists; + currentUser = FirebaseAuth.getInstance().getCurrentUser(); + } + + // Define the method that allows the parent activity or fragment to define the listener + public void setOnItemClickListener(OnItemClickListener listener) { + this.listener = listener; + } + + @Override + public PlaylistViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + // Inflate the custom layout + View playlistView = inflater.inflate(R.layout.playlist_item, parent, false); + + // Return a new holder instance + return new PlaylistViewHolder(playlistView); + } + + @Override + public void onBindViewHolder(PlaylistViewHolder holder, int position) { + // Get the data model based on position + Playlist playlist = mPlaylists.get(position); + + // Set item views based on your views and data model + TextView textView = holder.playlistName; + textView.setText(playlist.getName()); + + TextView hostTextView = holder.hostNotifier; + + if (playlist.isHost(currentUser.getUid())) { + hostTextView.setVisibility(View.VISIBLE); + } + } + + @Override + public int getItemCount() { + return mPlaylists.size(); + } + + // Define the listener interface + public interface OnItemClickListener { + void onItemClick(View itemView, int position); + } + + public class PlaylistViewHolder extends RecyclerView.ViewHolder { + private TextView playlistName; + private TextView hostNotifier; + + public PlaylistViewHolder(final View itemView) { + super(itemView); + + playlistName = itemView.findViewById(R.id.playlist_name); + hostNotifier = itemView.findViewById(R.id.host_notifier); + + // Setup the click listener + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // Triggers click upwards to the adapter on click + if (listener != null) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onItemClick(itemView, position); + } + } + } + }); + } + } +} diff --git a/app/src/main/java/com/houseparty/houseparty/Song.java b/app/src/main/java/com/houseparty/houseparty/Song.java index 6788208..608b1e3 100644 --- a/app/src/main/java/com/houseparty/houseparty/Song.java +++ b/app/src/main/java/com/houseparty/houseparty/Song.java @@ -1,39 +1,159 @@ package com.houseparty.houseparty; +import android.support.annotation.NonNull; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import java.util.ArrayList; +import java.util.List; + /** * Created by jacksonkurtz on 2/23/18. */ -public class Song { - private String title; - private String artist; - private String uri; - private String api; +public abstract class Song implements Comparable { + protected String title; + protected String artist; + protected String uri; + protected String coverArtUrl; + private List thumbs = new ArrayList<>(); + private Long timeAdded; + private String uid; public Song() { + timeAdded = System.currentTimeMillis(); } - public Song(String title) { + @Deprecated + public Song(String title, String uri, String artist) { this.title = title; + this.uri = uri; + this.artist = artist; + timeAdded = System.currentTimeMillis(); } - public Song(String title, String artist, String uri, String api) { + public Song(String title, String uri, String artist, String coverArtUrl) { this.title = title; - this.artist = artist; this.uri = uri; - this.api = api; + this.artist = artist; + this.coverArtUrl = coverArtUrl; + timeAdded = System.currentTimeMillis(); } public String getTitle() { return this.title; } + public String getUri() { + return this.uri; + } + public String getArtist() { return this.artist; } - public String getName() { - return this.title + " - " + this.artist; + public String getCoverArtUrl() { + return coverArtUrl; + } + + public List getThumbs() { + return thumbs; + } + + public void setThumbs(List thumbs) { + this.thumbs = thumbs; + } + + public int retrieveThumbsSize() { + return thumbs.size(); + } + + public void addThumbsUp(String uid) { + thumbs.add(uid); + } + + public void removeThumbsUp(String uid) { + thumbs.remove(uid); + } + + public boolean thumbsContains(String uid) { + return thumbs.contains(uid); + } + + public Long getTimeAdded() { + return this.timeAdded; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + @Override + public String toString() { + return this.title; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (this.getClass() != obj.getClass()) { + return false; + } + + Song song = (Song) obj; + return new EqualsBuilder() + .append(title, song.title) + .append(artist, song.artist) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 31) + .append(title) + .append(artist) + .append(uri) + .toHashCode(); + } + + @Override + public int compareTo(@NonNull Song other) { + int result; + + result = other.retrieveThumbsSize() - retrieveThumbsSize(); + if (result != 0) { + return result; + } + + result = (int) (getTimeAdded() - other.getTimeAdded()); + if (result != 0) { + return result; + } + + result = other.getTitle().compareTo(getTitle()); + if (result != 0) { + return result; + } + + result = other.getArtist().compareTo(getArtist()); + if (result != 0) { + return result; + } + + return 0; } + public abstract void playSong(); } diff --git a/app/src/main/java/com/houseparty/houseparty/SongActivity.java b/app/src/main/java/com/houseparty/houseparty/SongActivity.java deleted file mode 100755 index 4794c77..0000000 --- a/app/src/main/java/com/houseparty/houseparty/SongActivity.java +++ /dev/null @@ -1,320 +0,0 @@ -package com.houseparty.houseparty; - -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.media.MediaPlayer; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.text.InputType; -import android.util.Log; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListView; - -import com.google.firebase.database.ChildEventListener; -import com.google.firebase.database.DataSnapshot; -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.FirebaseDatabase; -import com.spotify.sdk.android.authentication.AuthenticationClient; -import com.spotify.sdk.android.authentication.AuthenticationRequest; -import com.spotify.sdk.android.authentication.AuthenticationResponse; -import com.spotify.sdk.android.player.Config; -import com.spotify.sdk.android.player.ConnectionStateCallback; -import com.spotify.sdk.android.player.Error; -import com.spotify.sdk.android.player.PlayerEvent; -import com.spotify.sdk.android.player.Spotify; -import com.spotify.sdk.android.player.SpotifyPlayer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; - -import kaaes.spotify.webapi.android.SpotifyApi; -import kaaes.spotify.webapi.android.SpotifyService; -import kaaes.spotify.webapi.android.models.Track; -import kaaes.spotify.webapi.android.models.TracksPager; -import retrofit.Callback; -import retrofit.RetrofitError; -import retrofit.client.Response; - -public class SongActivity extends AppCompatActivity implements - SpotifyPlayer.NotificationCallback, ConnectionStateCallback { - - private ListView listView; - private List list; - private ArrayAdapter adapter; - private MediaPlayer mediaPlayer; - private String title = "HouseParty - "; - private static String song_name; - - private FirebaseDatabase sFirebaseDatabase; - private DatabaseReference songDatabaseReference; - private ChildEventListener sChildEventListener; - - private String CLIENT_ID; - private String REDIRECT_URI; - private int REQUEST_CODE; - private String accessToken; - private SpotifyPlayer spotifyPlayer; - private SpotifyService spotify; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_song); - - sFirebaseDatabase = FirebaseDatabase.getInstance(); - Hashtable t = MainActivity.getIdTable(); - String id = t.get(MainActivity.selection()); - songDatabaseReference = sFirebaseDatabase.getReference().child("playlists").child(id).child("songs"); - - ActionBar actionBar = getSupportActionBar(); - actionBar.show(); - actionBar.setTitle(title + MainActivity.selection()); - - Bundle extras = getIntent().getExtras(); - CLIENT_ID = extras.getString("CLIENT_ID"); - Log.d("SongActivity", "CLIENT_ID = " + CLIENT_ID); - REDIRECT_URI = extras.getString("REDIRECT_URI"); - Log.d("SongActivity", "REDIRECT_URI = " + REDIRECT_URI); - REQUEST_CODE = extras.getInt("REQUEST_CODE"); - Log.d("SongActivity", "REQUEST_CODE = " + REQUEST_CODE); - - listView = (ListView) findViewById(R.id.listView); - - list = new ArrayList<>(); - /* - Field[] fields = R.raw.class.getFields(); - for(int i = 0; i < fields.length; i++){ - list.add(fields[i].getName()); - for (Field f : fields) - list.add(f.getName()); - */ - list.add("Headlines"); - - adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list); - listView.setAdapter(adapter); - - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - if (mediaPlayer != null) - mediaPlayer.release(); - if (list.get(i).equals("Headlines")) - authenticateUser(); - else { - int resID = getResources().getIdentifier(list.get(i), "raw", getPackageName()); - mediaPlayer = MediaPlayer.create(SongActivity.this, resID); - mediaPlayer.start(); - } - } - }); - - sChildEventListener = new ChildEventListener() { - @Override - public void onChildAdded(DataSnapshot dataSnapshot, String s) { - System.out.println("Size of list is: " + list.size()); - Song sList = dataSnapshot.getValue(Song.class); - list.add(sList.getTitle()); - adapter.notifyDataSetChanged(); - - } - - @Override - public void onChildChanged(DataSnapshot dataSnapshot, String s) { - } - - @Override - public void onChildRemoved(DataSnapshot dataSnapshot) { - - } - - @Override - public void onChildMoved(DataSnapshot dataSnapshot, String s) { - - } - - @Override - public void onCancelled(DatabaseError databaseError) { - - } - }; - songDatabaseReference.addChildEventListener(sChildEventListener); - - } - - public void dialogueBox_Song(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Enter Song Name:"); - - final EditText input = new EditText(this); - - input.setInputType(InputType.TYPE_CLASS_TEXT); - builder.setView(input); - - builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - song_name = input.getText().toString(); - if (song_name.isEmpty()) { - dialog.cancel(); - } else { - Song song = new Song(song_name); - songDatabaseReference.push().setValue(song); - //list.add(song_name); - //Intent intent = new Intent(getBaseContext(), SongActivity.class); - //startActivity(intent); - } - } - }); - builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); - - builder.show(); - } - - String spotifySearchForTrack(String query) { - - query = query.replaceAll(" ", "+"); - - Map options = new HashMap(); - //options.put("Authorization", accessToken); - options.put("market", "US"); - options.put("limit", 20); - - final String[] songUri = {""}; - spotify.searchTracks(query, options, new Callback() { - @Override - public void success(TracksPager tracksPager, Response response) { - List searchResults = tracksPager.tracks.items; - songUri[0] = searchResults.get(0).uri; - Log.d("FetchSongTask", "1st song uri = " + songUri[0]); - } - - @Override - public void failure(RetrofitError error) { - error.printStackTrace(); - } - }); - - return songUri[0]; - } - - void authenticateUser() { - // ---------USER AUTHENTICATION---------- - - AuthenticationRequest.Builder builder = new AuthenticationRequest.Builder(CLIENT_ID, - AuthenticationResponse.Type.TOKEN, - REDIRECT_URI); - builder.setScopes(new String[]{"user-read-private", "streaming", "playlist-modify-public", - "playlist-modify-private", "playlist-read-collaborative", "user-library-read", - "user-library-modify", "user-read-playback-state", "user-modify-playback-state", - "user-read-currently-playing"}); - AuthenticationRequest request = builder.build(); - - AuthenticationClient.openLoginActivity(SongActivity.this, REQUEST_CODE, request); - - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - - // Check if result comes from the correct activity - if (requestCode == REQUEST_CODE) { - AuthenticationResponse response = AuthenticationClient.getResponse(resultCode, intent); - if (response.getType() == AuthenticationResponse.Type.TOKEN) { - accessToken = response.getAccessToken(); - Config playerConfig = new Config(SongActivity.this, accessToken, CLIENT_ID); - Spotify.getPlayer(playerConfig, SongActivity.this, new SpotifyPlayer.InitializationObserver() { - @Override - public void onInitialized(SpotifyPlayer spotifyPlayer) { - SongActivity.this.spotifyPlayer = spotifyPlayer; - Log.d("SongActivity", "spotifyPlayer initialized."); - SongActivity.this.spotifyPlayer.addConnectionStateCallback(SongActivity.this); - SongActivity.this.spotifyPlayer.addNotificationCallback(SongActivity.this); - Log.d("SongActivity", "spotifyPlayer: callbacks added."); - } - - @Override - public void onError(Throwable throwable) { - Log.e("SongActivity", "Could not initialize player: " + throwable.getMessage()); - } - }); - } - } - } - - @Override - public void onLoggedIn() { - Log.d("SongActivity", "User authenticated."); - SpotifyApi wrapper = new SpotifyApi(); - wrapper.setAccessToken(accessToken); - spotify = wrapper.getService(); - final String songUri = spotifySearchForTrack("Headlines"); - spotifyPlayer.playUri(null, songUri, 0, 0); - } - - @Override - public void onLoggedOut() { - Log.d("SongActivity", "User logged out"); - } - - @Override - public void onLoginFailed(Error error) { - Log.e("SongActivity", "Login failed"); - } - - @Override - protected void onDestroy() { - // --------SUPER IMPORTANT TO AVOID LEAKAGE---------- - Spotify.destroyPlayer(SongActivity.this); - - if (spotifyPlayer != null) { - spotifyPlayer.destroy(); - } - - super.onDestroy(); - } - - @Override - public void onTemporaryError() { - Log.d("SongActivity", "Temporary error occurred"); - } - - @Override - public void onConnectionMessage(String message) { - Log.d("SongActivity", "Received connection message: " + message); - } - - @Override - public void onPlaybackEvent(PlayerEvent playerEvent) { - Log.d("SongActivity", "Playback event received: " + playerEvent.name()); - switch (playerEvent) { - // Handle event type as necessary - default: - break; - } - } - - @Override - public void onPlaybackError(Error error) { - Log.d("SongActivity", "Playback error received: " + error.name()); - switch (error) { - // Handle error type as necessary - default: - break; - } - } -} diff --git a/app/src/main/java/com/houseparty/houseparty/SongAdapter.java b/app/src/main/java/com/houseparty/houseparty/SongAdapter.java new file mode 100644 index 0000000..a5efc46 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/SongAdapter.java @@ -0,0 +1,119 @@ +package com.houseparty.houseparty; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; + +import java.util.List; + +/** + * @author Eitan created on 5/17/2018. + */ +public class SongAdapter extends + RecyclerView.Adapter { + + private List mSongs; + private FirebaseUser currentUser; + + // Define listener member variable + private OnItemClickListener listener; + private OnItemClickListener thumbListener; + + // Define the method that allows the parent activity or fragment to define the listener + public void setOnItemClickListener(OnItemClickListener listener) { + this.listener = listener; + } + + public void setOnThumbClickListener(OnItemClickListener listener) { + this.thumbListener = listener; + currentUser = FirebaseAuth.getInstance().getCurrentUser(); + } + + public SongAdapter(List songs) { + mSongs = songs; + } + + // Define the listener interface + public interface OnItemClickListener { + void onItemClick(View itemView, int position); + } + + @Override + public SongAdapter.SongViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + // Inflate the custom layout + View songView = inflater.inflate(R.layout.song_item, parent, false); + + // Return a new holder instance + return new SongViewHolder(songView); + } + + @Override + public void onBindViewHolder(SongAdapter.SongViewHolder holder, int position) { + // Get the data model based on position + Song song = mSongs.get(position); + + // Set item views based on your views and data model + TextView textView = holder.songTextView; + textView.setText(song.getTitle()); + + ImageButton thumbImageButton = holder.thumbImageButton; + thumbImageButton.setSelected(song.thumbsContains(currentUser.getUid())); + + holder.songLikesTextView.setText(String.valueOf(song.retrieveThumbsSize())); + } + + @Override + public int getItemCount() { + return mSongs.size(); + } + + public class SongViewHolder extends RecyclerView.ViewHolder { + private TextView songTextView; + private TextView songLikesTextView; + private ImageButton thumbImageButton; + + public SongViewHolder(final View itemView) { + super(itemView); + + songTextView = itemView.findViewById(R.id.song_title); + songLikesTextView = itemView.findViewById(R.id.song_likes); + thumbImageButton = itemView.findViewById(R.id.thumb); + + // Setup the click listener + songTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // Triggers click upwards to the adapter on click + if (listener != null) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onItemClick(itemView, position); + } + } + } + }); + + thumbImageButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (thumbListener != null) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + thumbListener.onItemClick(itemView, position); + } + } + } + }); + } + } +} diff --git a/app/src/main/java/com/houseparty/houseparty/SongFactory.java b/app/src/main/java/com/houseparty/houseparty/SongFactory.java new file mode 100644 index 0000000..27f10c5 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/SongFactory.java @@ -0,0 +1,40 @@ +package com.houseparty.houseparty; + +/** + * @author Nathan Boyd april 12 2018 + */ + + +public class SongFactory { + private static SongFactory instance; + + private Song song; + + private SongFactory() { + } + + public static SongFactory getInstance() { + if (instance == null) { + instance = new SongFactory(); + } + return instance; + + } + + public Song createSong(final String title, String artist, String api) { + if ("spotify".equals(api)) { + String uri = ""; + song = new SpotifySong(title, uri, artist); + } else if ("soundcloud".equals(api)) { + String uri = ""; + song = new SoundCloudSong(title, uri, artist); + } else if ("tidal".equals(api)) { + String uri = ""; + song = new TidalSong(title, uri, artist); + } else if ("googleplay".equals(api)) { + String uri = ""; + song = new GooglePlaySong(title, uri, artist); + } + return song; + } +} diff --git a/app/src/main/java/com/houseparty/houseparty/SoundCloudSong.java b/app/src/main/java/com/houseparty/houseparty/SoundCloudSong.java new file mode 100644 index 0000000..10c5b62 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/SoundCloudSong.java @@ -0,0 +1,16 @@ +package com.houseparty.houseparty; + +/** + * Created by jacksonkurtz on 4/12/18. + */ + +public class SoundCloudSong extends Song { + public SoundCloudSong(String title, String uri, String artist) { + super(title, uri, artist); + } + + public void playSong() { + throw new UnsupportedOperationException(); + } + +} diff --git a/app/src/main/java/com/houseparty/houseparty/SpotifySong.java b/app/src/main/java/com/houseparty/houseparty/SpotifySong.java new file mode 100644 index 0000000..6c23a5c --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/SpotifySong.java @@ -0,0 +1,40 @@ +package com.houseparty.houseparty; + +import android.util.Log; + +import static com.houseparty.houseparty.NewSongActivity.spotifyPlayer; + +/** + * @author hanzy created on 4/11/2018. + */ +public class SpotifySong extends Song { + + protected String accessToken; + + public SpotifySong() { + super(); + } + + public SpotifySong(String title, String uri, String artist) { + super(title, uri, artist); + } + + public SpotifySong(String title, String uri, String artist, String token, String coverArtUrl) { + super(title, uri, artist, coverArtUrl); + this.accessToken = token; + } + + @Override + public void playSong() { + if (!NewSongActivity.spotifyPlayer.isLoggedIn()) { + Log.d("playSong", "Logging in spotifyPlayer."); + NewSongActivity.spotifyPlayer.login(accessToken); + } + Log.d("playSong", "spotifyPlayer is logged in."); + NewSongActivity.spotifyPlayer.playUri(null, super.uri, 0, 0); + } + + public String getAccessToken() { + return accessToken; + } +} diff --git a/app/src/main/java/com/houseparty/houseparty/TidalSong.java b/app/src/main/java/com/houseparty/houseparty/TidalSong.java new file mode 100644 index 0000000..ca00872 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/TidalSong.java @@ -0,0 +1,15 @@ +package com.houseparty.houseparty; + +/** + * Created by jacksonkurtz on 4/12/18. + */ + +public class TidalSong extends Song { + public TidalSong(String title, String uri, String artist) { + super(title, uri, artist); + } + + public void playSong() { + throw new UnsupportedOperationException(); + } +} diff --git a/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Constants.java b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Constants.java new file mode 100644 index 0000000..4f29c6b --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Constants.java @@ -0,0 +1,9 @@ +package com.houseparty.houseparty.napsterSampleLibrary; + +class Constants { + public static final String LIMIT = "limit"; + public static final String OFFSET = "offset"; + public static final String TRACK_ID = "trackId"; + public static final String APIKEY = "apikey"; + public static final String AUTHORIZAION = "Authorization"; +} diff --git a/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Metadata.java b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Metadata.java new file mode 100644 index 0000000..f20dbd7 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Metadata.java @@ -0,0 +1,30 @@ +package com.houseparty.houseparty.napsterSampleLibrary; + +import retrofit.Callback; +import retrofit.RestAdapter; + + +public class Metadata { + + TrackService trackService; + String apiKey; + + public Metadata(String apiKey) { + RestAdapter adapter = new RestAdapter.Builder().setEndpoint("http://api.napster.com").build(); + trackService = adapter.create(TrackService.class); + this.apiKey = apiKey; + } + + public void getTopTracks(int limit, int offset, Callback callback) { + trackService.getTopTracks(apiKey, limit, offset, callback); + } + + public void queryTrack( String query, Callback callback ) { + trackService.queryTrack( apiKey, 1, query, callback); + } + + public TrackService getTrackService() { + return trackService; + } + +} diff --git a/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Search.java b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Search.java new file mode 100644 index 0000000..60e3c02 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Search.java @@ -0,0 +1,31 @@ +package com.houseparty.houseparty.napsterSampleLibrary; + +import com.google.gson.annotations.SerializedName; +import com.napster.cedar.player.data.Track; + +import java.util.List; + +/** + * Created by jacksonkurtz on 6/4/18. + */ + +public class Search { + + @SerializedName("data") + public final List search; + + public Search(List search) { + this.search = search; + } + + @Override + public String toString() { + + String output = ""; + for( Track t : search ) { + output += t.name + " "; + } + return output; + } + +} diff --git a/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/TrackService.java b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/TrackService.java new file mode 100644 index 0000000..c6d0eb2 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/TrackService.java @@ -0,0 +1,31 @@ +package com.houseparty.houseparty.napsterSampleLibrary; + +import retrofit.Callback; +import retrofit.http.GET; +import retrofit.http.Header; +import retrofit.http.Path; +import retrofit.http.Query; + +public interface TrackService { + + @GET("/v2.0/tracks/top") + public void getTopTracks( + @Query(Constants.APIKEY) String apikey, + @Query(Constants.LIMIT) int limit, + @Query(Constants.OFFSET) int offset, + Callback callback); + + + @GET("/v2.0/search" ) + public void queryTrack( + @Query(Constants.APIKEY) String apikey, + @Query("limit") int limit, + @Query("q") String q, + //@Query("type") String type, + Callback callback); + + @GET("/v2.0/me/listens") + public void getListeningHistory( + @Header(Constants.AUTHORIZAION) String authorization, + Callback callback); +} diff --git a/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Tracks.java b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Tracks.java new file mode 100644 index 0000000..a3e36e8 --- /dev/null +++ b/app/src/main/java/com/houseparty/houseparty/napsterSampleLibrary/Tracks.java @@ -0,0 +1,31 @@ +package com.houseparty.houseparty.napsterSampleLibrary; + +import com.google.gson.annotations.SerializedName; +import com.napster.cedar.player.data.Track; + +import java.util.List; + +/** + * Created by jacksonkurtz on 6/3/18. + */ + +public class Tracks { + + @SerializedName("tracks") + public final List tracks; + + public Tracks(List tracks) { + this.tracks = tracks; + } + + @Override + public String toString() { + + String output = ""; + for( Track t : tracks ) { + output += t.name + " "; + } + return output; + } + +} diff --git a/app/src/main/res/drawable/delete.png b/app/src/main/res/drawable/delete.png new file mode 100644 index 0000000..a355143 Binary files /dev/null and b/app/src/main/res/drawable/delete.png differ diff --git a/app/src/main/res/drawable/thumb_selector.xml b/app/src/main/res/drawable/thumb_selector.xml new file mode 100644 index 0000000..ba37eeb --- /dev/null +++ b/app/src/main/res/drawable/thumb_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb_up_black.png b/app/src/main/res/drawable/thumb_up_black.png new file mode 100644 index 0000000..b753ac3 Binary files /dev/null and b/app/src/main/res/drawable/thumb_up_black.png differ diff --git a/app/src/main/res/drawable/thumb_up_yellow.png b/app/src/main/res/drawable/thumb_up_yellow.png new file mode 100644 index 0000000..f85b449 Binary files /dev/null and b/app/src/main/res/drawable/thumb_up_yellow.png differ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 2d05d73..7d5818a 100755 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -54,19 +54,37 @@ app:layout_constraintVertical_bias="0.499" />