diff --git a/.github/workflows/bintray-publish.yml b/.github/workflows/bintray-publish.yml
new file mode 100644
index 0000000..f4a40f2
--- /dev/null
+++ b/.github/workflows/bintray-publish.yml
@@ -0,0 +1,26 @@
+name: Publish to bintray on closed PR
+on:
+ release:
+ types: [released]
+
+jobs:
+ gradle:
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v2
+ # Export properties
+ - name: Setup bintray credentials
+ env:
+ BINTRAY_APIKEY: ${{ secrets.BINTRAY_APIKEY }}
+ run: |
+ echo "bintray.user=queueitdevs" > ./local.properties
+ echo "bintray.apiKey=${BINTRAY_APIKEY}" >> ./local.properties
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 11
+ - uses: eskatos/gradle-command-action@v1
+ with:
+ arguments: assembleLibrary_androidx :library:assembleLibrary bintrayUpload
\ No newline at end of file
diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml
new file mode 100644
index 0000000..388a247
--- /dev/null
+++ b/.github/workflows/gradle-build.yml
@@ -0,0 +1,18 @@
+name: Run Gradle Build on Push
+on: push
+jobs:
+ gradle:
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Create local.properties
+ run: echo "" >> ./local.properties
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 11
+ - uses: eskatos/gradle-command-action@v1
+ with:
+ arguments: build
\ No newline at end of file
diff --git a/App integration flow.PNG b/App integration flow.PNG
new file mode 100644
index 0000000..00eab46
Binary files /dev/null and b/App integration flow.PNG differ
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ddabc19
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Queue-it
+
+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/android-webui-sdk.iml b/android-webui-sdk.iml
new file mode 100644
index 0000000..760fee4
--- /dev/null
+++ b/android-webui-sdk.iml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/library/java/com/queue_it/shopdemo/DeepActivity.java b/demoapp/src/library/java/com/queue_it/shopdemo/DeepActivity.java
new file mode 100644
index 0000000..339d760
--- /dev/null
+++ b/demoapp/src/library/java/com/queue_it/shopdemo/DeepActivity.java
@@ -0,0 +1,29 @@
+package com.queue_it.shopdemo;
+
+import android.os.Bundle;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+
+public class DeepActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_deep);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ FloatingActionButton fab = findViewById(R.id.fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/library/java/com/queue_it/shopdemo/FirstFragment.java b/demoapp/src/library/java/com/queue_it/shopdemo/FirstFragment.java
new file mode 100644
index 0000000..54e7b63
--- /dev/null
+++ b/demoapp/src/library/java/com/queue_it/shopdemo/FirstFragment.java
@@ -0,0 +1,33 @@
+package com.queue_it.shopdemo;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+public class FirstFragment extends Fragment {
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState
+ ) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_first, container, false);
+ }
+
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ view.findViewById(R.id.button_first).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+// NavHostFragment.findNavController(FirstFragment.this)
+// .navigate(R.id.action_FirstFragment_to_SecondFragment);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/library/java/com/queue_it/shopdemo/MainActivity.java b/demoapp/src/library/java/com/queue_it/shopdemo/MainActivity.java
new file mode 100644
index 0000000..2212f74
--- /dev/null
+++ b/demoapp/src/library/java/com/queue_it/shopdemo/MainActivity.java
@@ -0,0 +1,194 @@
+package com.queue_it.shopdemo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.RadioButton;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.queue_it.androidsdk.*;
+import com.queue_it.androidsdk.Error;
+
+public class MainActivity extends AppCompatActivity {
+ FloatingActionButton queue_button;
+ EditText customerIdEditText;
+ EditText eventIdEditText;
+ EditText layoutNameEditText;
+ EditText languageEditText;
+ EditText enqueueTokenEditText;
+ EditText enqueueKeyEditText;
+ RadioButton testRadioButton;
+
+ private void runQueue(QueueITEngine queueITEngine) throws QueueITException {
+ String enqueueToken = enqueueTokenEditText.getText().toString();
+ String enqueueKey = enqueueKeyEditText.getText().toString();
+ if (enqueueToken.length() > 0) {
+ queueITEngine.runWithEnqueueToken(MainActivity.this, enqueueToken);
+ } else if (enqueueKey.length() > 0) {
+ queueITEngine.runWithEnqueueKey(MainActivity.this, enqueueKey);
+ } else {
+ queueITEngine.run(MainActivity.this);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ queue_button = findViewById(R.id.queue_button);
+ customerIdEditText = findViewById(R.id.customerid_edittext);
+ eventIdEditText = findViewById(R.id.eventid_edittext);
+ layoutNameEditText = findViewById(R.id.layoutname_edittext);
+ languageEditText = findViewById(R.id.language_edittext);
+ testRadioButton = findViewById(R.id.radio_environment_test);
+ customerIdEditText.addTextChangedListener(getRequiredTextValidator(customerIdEditText));
+ eventIdEditText.addTextChangedListener(getRequiredTextValidator(eventIdEditText));
+ enqueueTokenEditText = findViewById(R.id.enqueuetoken_edittext);
+ enqueueKeyEditText = findViewById(R.id.enqueuekey_edittext);
+
+ final SharedPreferences sharedPreferences = getPreferences(Context.MODE_PRIVATE);
+ String customerId = sharedPreferences.getString("customerId", "");
+ String eventOrAliasId = sharedPreferences.getString("eventOrAliasId", "");
+ String layoutName = sharedPreferences.getString("layoutName", "");
+ String language = sharedPreferences.getString("language", "");
+ String enqueueToken = sharedPreferences.getString("enqueueToken", "");
+ String enqueueKey = sharedPreferences.getString("enqueueKey", "");
+
+ customerIdEditText.setText(customerId);
+ eventIdEditText.setText(eventOrAliasId);
+ layoutNameEditText.setText(layoutName);
+ languageEditText.setText(language);
+ enqueueTokenEditText.setText(enqueueToken);
+ enqueueKeyEditText.setText(enqueueKey);
+
+ queue_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!TextUtils.isEmpty(customerIdEditText.getError()) || !TextUtils.isEmpty(eventIdEditText.getError())) {
+ return;
+ }
+
+ queue_button.setEnabled(false);
+ QueueService.IsTest = testRadioButton.isChecked();
+ hideKeyboard();
+
+ String customerId = customerIdEditText.getText().toString();
+ String eventOrAliasId = eventIdEditText.getText().toString();
+ String layoutName = layoutNameEditText.getText().toString();
+ String language = languageEditText.getText().toString();
+ String enqueueToken = enqueueTokenEditText.getText().toString();
+ String enqueueKey = enqueueKeyEditText.getText().toString();
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString("customerId", customerId);
+ editor.putString("eventOrAliasId", eventOrAliasId);
+ editor.putString("layoutName", layoutName);
+ editor.putString("language", language);
+ editor.putString("enqueueToken", enqueueToken);
+ editor.putString("enqueueKey", enqueueKey);
+ editor.commit();
+
+ Toast.makeText(getApplicationContext(), "Please wait for your turn.", Toast.LENGTH_SHORT).show();
+
+ QueueITEngine queueITEngine = new QueueITEngine(MainActivity.this, customerId, eventOrAliasId, layoutName, language, new QueueListener() {
+
+ @Override
+ public void onSessionRestart(QueueITEngine queueITEngine) {
+ try {
+ runQueue(queueITEngine);
+ } catch (QueueITException e) {
+ Toast.makeText(getApplicationContext(), "Please try again.", Toast.LENGTH_LONG).show();
+ queue_button.setEnabled(true);
+ }
+ }
+
+ @Override
+ public void onQueuePassed(QueuePassedInfo queuePassedInfo) {
+ showResultActivity("You passed the queue! Your token: " + queuePassedInfo.getQueueItToken(), true);
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onQueueViewWillOpen() {
+ Toast.makeText(getApplicationContext(), "onQueueViewWillOpen", Toast.LENGTH_SHORT).show();
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onUserExited() {
+ Toast.makeText(getApplicationContext(), "onUserExited", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onQueueDisabled() {
+ showResultActivity("The queue is disabled.", false);
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onQueueItUnavailable() {
+ showResultActivity("Queue-it is unavailable", false);
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onError(Error error, String errorMessage) {
+ showResultActivity("Critical error: " + errorMessage, false);
+ queue_button.setEnabled(true);
+ }
+
+ });
+ try {
+ runQueue(queueITEngine);
+ } catch (QueueITException e) {
+ Toast.makeText(getApplicationContext(), "Please try again.", Toast.LENGTH_LONG).show();
+ queue_button.setEnabled(true);
+ }
+ }
+ });
+ }
+
+ private void showResultActivity(String result, boolean success) {
+ Intent intent = new Intent(this, ResultActivity.class);
+ intent.putExtra("success", success);
+ intent.putExtra("result", result);
+ startActivity(intent);
+ }
+
+ private boolean isAlphaNumeric(String s) {
+ String pattern = "^[a-zA-Z0-9]*$";
+ return s.matches(pattern);
+ }
+
+ private void hideKeyboard() {
+ View view = this.getCurrentFocus();
+ if (view != null) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+
+ private TextValidator getRequiredTextValidator(TextView textView) {
+ return new TextValidator(textView) {
+ @Override
+ public void validate(TextView textView, String text) {
+ if (TextUtils.isEmpty(text)) {
+ textView.setError("Field required");
+ } else if (!isAlphaNumeric(text)) {
+ textView.setError("Must be alphanumeric");
+ }
+ }
+ };
+ }
+}
diff --git a/demoapp/src/library/java/com/queue_it/shopdemo/ResultActivity.java b/demoapp/src/library/java/com/queue_it/shopdemo/ResultActivity.java
new file mode 100644
index 0000000..bff5f51
--- /dev/null
+++ b/demoapp/src/library/java/com/queue_it/shopdemo/ResultActivity.java
@@ -0,0 +1,56 @@
+package com.queue_it.shopdemo;
+
+import android.content.Intent;
+import android.os.Bundle;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+
+public class ResultActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_result);
+
+ String result;
+ boolean success;
+ if (savedInstanceState == null) {
+ Bundle extras = getIntent().getExtras();
+ if (extras == null) {
+ result = null;
+ success = false;
+ } else {
+ result = extras.getString("result");
+ success = extras.getBoolean("success");
+ }
+ } else {
+ result = (String) savedInstanceState.getSerializable("result");
+ success = (Boolean) savedInstanceState.getSerializable("success");
+ }
+
+ final TextView resultText = (TextView)findViewById(R.id.result_text);
+ final ImageView checkedImageView = (ImageView)findViewById(R.id.image_view_checked);
+ final FloatingActionButton retry_button = (FloatingActionButton)findViewById(R.id.retry_button);
+
+ resultText.setText(result);
+ if (success){
+ checkedImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_check_black_24dp, null));
+ } else {
+ checkedImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_error_black_24dp, null));
+ }
+
+ retry_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(ResultActivity.this, MainActivity.class);
+ startActivity(intent);
+ finish();
+ }
+ });
+ }
+}
diff --git a/demoapp/src/library/java/com/queue_it/shopdemo/SecondFragment.java b/demoapp/src/library/java/com/queue_it/shopdemo/SecondFragment.java
new file mode 100644
index 0000000..991ee41
--- /dev/null
+++ b/demoapp/src/library/java/com/queue_it/shopdemo/SecondFragment.java
@@ -0,0 +1,32 @@
+package com.queue_it.shopdemo;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class SecondFragment extends Fragment {
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState
+ ) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_second, container, false);
+ }
+
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ view.findViewById(R.id.button_second).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+// NavHostFragment.findNavController(SecondFragment.this)
+// .navigate(R.id.action_SecondFragment_to_FirstFragment);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/library/res/layout/activity_deep.xml b/demoapp/src/library/res/layout/activity_deep.xml
new file mode 100644
index 0000000..08580ef
--- /dev/null
+++ b/demoapp/src/library/res/layout/activity_deep.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/library/res/layout/activity_main.xml b/demoapp/src/library/res/layout/activity_main.xml
new file mode 100644
index 0000000..799260f
--- /dev/null
+++ b/demoapp/src/library/res/layout/activity_main.xml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demoapp/src/library/res/layout/activity_result.xml b/demoapp/src/library/res/layout/activity_result.xml
new file mode 100644
index 0000000..b898426
--- /dev/null
+++ b/demoapp/src/library/res/layout/activity_result.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demoapp/src/library/res/layout/content_deep.xml b/demoapp/src/library/res/layout/content_deep.xml
new file mode 100644
index 0000000..96f084d
--- /dev/null
+++ b/demoapp/src/library/res/layout/content_deep.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/demoapp/src/library/res/layout/fragment_first.xml b/demoapp/src/library/res/layout/fragment_first.xml
new file mode 100644
index 0000000..8cf9428
--- /dev/null
+++ b/demoapp/src/library/res/layout/fragment_first.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/library/res/layout/fragment_second.xml b/demoapp/src/library/res/layout/fragment_second.xml
new file mode 100644
index 0000000..3a332e9
--- /dev/null
+++ b/demoapp/src/library/res/layout/fragment_second.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/library_androidx/java/com/queue_it/shopdemo/DeepActivity.java b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/DeepActivity.java
new file mode 100644
index 0000000..bd1eddb
--- /dev/null
+++ b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/DeepActivity.java
@@ -0,0 +1,28 @@
+package com.queue_it.shopdemo;
+
+import android.os.Bundle;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import android.view.View;
+
+public class DeepActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_deep);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ FloatingActionButton fab = findViewById(R.id.fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/library_androidx/java/com/queue_it/shopdemo/FirstFragment.java b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/FirstFragment.java
new file mode 100644
index 0000000..c9887a1
--- /dev/null
+++ b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/FirstFragment.java
@@ -0,0 +1,35 @@
+package com.queue_it.shopdemo;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import androidx.navigation.fragment.NavHostFragment;
+
+public class FirstFragment extends Fragment {
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState
+ ) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_first, container, false);
+ }
+
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ view.findViewById(R.id.button_first).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ NavHostFragment.findNavController(FirstFragment.this)
+ .navigate(R.id.action_FirstFragment_to_SecondFragment);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/library_androidx/java/com/queue_it/shopdemo/MainActivity.java b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/MainActivity.java
new file mode 100644
index 0000000..453b1b3
--- /dev/null
+++ b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/MainActivity.java
@@ -0,0 +1,305 @@
+package com.queue_it.shopdemo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.RadioButton;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.queue_it.androidsdk.*;
+import com.queue_it.androidsdk.Error;
+
+public class MainActivity extends AppCompatActivity {
+ FloatingActionButton queue_button;
+ FloatingActionButton queueSession_button;
+ FloatingActionButton show_queue_button;
+ EditText customerIdEditText;
+ EditText eventIdEditText;
+ EditText layoutNameEditText;
+ EditText languageEditText;
+ EditText enqueueTokenEditText;
+ EditText enqueueKeyEditText;
+ RadioButton testRadioButton;
+ private QueueTryPassResult _queueTryPassResult;
+
+
+ QueueListener queueListener = new QueueListener() {
+
+ @Override
+ public void onSessionRestart(QueueITEngine queueITEngine) {
+ try {
+ runQueue(queueITEngine);
+ } catch (QueueITException e) {
+ Toast.makeText(getApplicationContext(), "Please try again.", Toast.LENGTH_LONG).show();
+ queue_button.setEnabled(true);
+ }
+ }
+
+ @Override
+ public void onQueuePassed(QueuePassedInfo queuePassedInfo) {
+ showResultActivity("You passed the queue! Your token: " + queuePassedInfo.getQueueItToken(), true);
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onQueueViewWillOpen() {
+ Toast.makeText(getApplicationContext(), "onQueueViewWillOpen", Toast.LENGTH_SHORT).show();
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onUserExited() {
+ Toast.makeText(getApplicationContext(), "onUserExited", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ protected void onQueueDisabled(QueueDisabledInfo queueDisabledInfo) {
+ showResultActivity("The queue is disabled. Your token: " + queueDisabledInfo.getQueueItToken(), false);
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onQueueItUnavailable() {
+ showResultActivity("Queue-it is unavailable", false);
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onError(Error error, String errorMessage) {
+ showResultActivity("Critical error: " + errorMessage, false);
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onWebViewClosed() {
+ showResultActivity("WebView closed ", false);
+ queue_button.setEnabled(true);
+ }
+ };
+
+ private void runQueue(QueueITEngine queueITEngine) throws QueueITException {
+ String enqueueToken = enqueueTokenEditText.getText().toString();
+ String enqueueKey = enqueueKeyEditText.getText().toString();
+ if (enqueueToken.length() > 0) {
+ queueITEngine.runWithEnqueueToken(this, enqueueToken);
+ } else if (enqueueKey.length() > 0) {
+ queueITEngine.runWithEnqueueKey(this, enqueueKey);
+ } else {
+ queueITEngine.run(this);
+ }
+ }
+
+ private void runSessionEngine(QueueITWaitingRoomProvider queueITWaitingRoomProvider) throws QueueITException {
+ String enqueueToken = enqueueTokenEditText.getText().toString();
+ String enqueueKey = enqueueKeyEditText.getText().toString();
+ if (enqueueToken.length() > 0) {
+ queueITWaitingRoomProvider.tryPassWithEnqueueToken(enqueueToken);
+ } else if (enqueueKey.length() > 0) {
+ queueITWaitingRoomProvider.tryPassWithEnqueueKey(enqueueKey);
+ } else {
+ queueITWaitingRoomProvider.tryPass();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ queue_button = findViewById(R.id.queue_button);
+ queueSession_button = findViewById(R.id.queueSession_button);
+ show_queue_button = findViewById(R.id.show_queue_button);
+ customerIdEditText = findViewById(R.id.customerid_edittext);
+ eventIdEditText = findViewById(R.id.eventid_edittext);
+ layoutNameEditText = findViewById(R.id.layoutname_edittext);
+ languageEditText = findViewById(R.id.language_edittext);
+ testRadioButton = findViewById(R.id.radio_environment_test);
+ customerIdEditText.addTextChangedListener(getRequiredTextValidator(customerIdEditText));
+ eventIdEditText.addTextChangedListener(getRequiredTextValidator(eventIdEditText));
+ enqueueTokenEditText = findViewById(R.id.enqueuetoken_edittext);
+ enqueueKeyEditText = findViewById(R.id.enqueuekey_edittext);
+
+ final SharedPreferences sharedPreferences = getPreferences(Context.MODE_PRIVATE);
+ String customerId = sharedPreferences.getString("customerId", "");
+ String eventOrAliasId = sharedPreferences.getString("eventOrAliasId", "");
+ String layoutName = sharedPreferences.getString("layoutName", "");
+ String language = sharedPreferences.getString("language", "");
+ String enqueueToken = sharedPreferences.getString("enqueueToken", "");
+ String enqueueKey = sharedPreferences.getString("enqueueKey", "");
+
+ customerIdEditText.setText(customerId);
+ eventIdEditText.setText(eventOrAliasId);
+ layoutNameEditText.setText(layoutName);
+ languageEditText.setText(language);
+ enqueueTokenEditText.setText(enqueueToken);
+ enqueueKeyEditText.setText(enqueueKey);
+
+ queue_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ QueueITEngine queueITEngine = getQueueITEngine(sharedPreferences);
+ try {
+ runQueue(queueITEngine);
+ } catch (QueueITException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ queueSession_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ QueueITWaitingRoomProvider queueITWaitingRoomProvider = getSessionEngine(sharedPreferences);
+ try {
+ runSessionEngine(queueITWaitingRoomProvider);
+ } catch (QueueITException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ show_queue_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(_queueTryPassResult != null && _queueTryPassResult.getRedirectType() == RedirectType.queue) {
+ QueueItEngineOptions queueItEngineOptions = getQueueItEngineOptions();
+ QueueITWaitingRoomView queueITWaitingRoomView =
+ new QueueITWaitingRoomView(MainActivity.this, queueListener, queueItEngineOptions);
+ queueITWaitingRoomView.showQueue(_queueTryPassResult, queueItEngineOptions.getWebViewUserAgent() );
+ _queueTryPassResult = null;
+ }
+ }
+ });
+ }
+
+ private QueueITEngine getQueueITEngine(SharedPreferences sharedPreferences) {
+ if (!TextUtils.isEmpty(customerIdEditText.getError()) || !TextUtils.isEmpty(eventIdEditText.getError())) {
+ return null;
+ }
+
+ queue_button.setEnabled(false);
+ QueueITApiClient.IsTest = testRadioButton.isChecked();
+ hideKeyboard();
+
+ String customerId = customerIdEditText.getText().toString();
+ String eventOrAliasId = eventIdEditText.getText().toString();
+ String layoutName = layoutNameEditText.getText().toString();
+ String language = languageEditText.getText().toString();
+ String enqueueToken = enqueueTokenEditText.getText().toString();
+ String enqueueKey = enqueueKeyEditText.getText().toString();
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString("customerId", customerId);
+ editor.putString("eventOrAliasId", eventOrAliasId);
+ editor.putString("layoutName", layoutName);
+ editor.putString("language", language);
+ editor.putString("enqueueToken", enqueueToken);
+ editor.putString("enqueueKey", enqueueKey);
+ editor.commit();
+
+ Toast.makeText(getApplicationContext(), "Please wait for your turn.", Toast.LENGTH_SHORT).show();
+ QueueItEngineOptions options = getQueueItEngineOptions();
+ return new QueueITEngine(MainActivity.this, customerId, eventOrAliasId, layoutName, language, queueListener, options);
+ }
+
+ @NonNull
+ private QueueItEngineOptions getQueueItEngineOptions() {
+ QueueItEngineOptions options = new QueueItEngineOptions();
+ options.setBackButtonDisabledFromWR(false);
+ return options;
+ }
+
+
+ private QueueITWaitingRoomProvider getSessionEngine(SharedPreferences sharedPreferences) {
+ if (!TextUtils.isEmpty(customerIdEditText.getError()) || !TextUtils.isEmpty(eventIdEditText.getError())) {
+ return null;
+ }
+
+ queue_button.setEnabled(false);
+ QueueITApiClient.IsTest = testRadioButton.isChecked();
+ hideKeyboard();
+
+ String customerId = customerIdEditText.getText().toString();
+ String eventOrAliasId = eventIdEditText.getText().toString();
+ String layoutName = layoutNameEditText.getText().toString();
+ String language = languageEditText.getText().toString();
+ String enqueueToken = enqueueTokenEditText.getText().toString();
+ String enqueueKey = enqueueKeyEditText.getText().toString();
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString("customerId", customerId);
+ editor.putString("eventOrAliasId", eventOrAliasId);
+ editor.putString("layoutName", layoutName);
+ editor.putString("language", language);
+ editor.putString("enqueueToken", enqueueToken);
+ editor.putString("enqueueKey", enqueueKey);
+ editor.commit();
+
+ Toast.makeText(getApplicationContext(), "Please wait for your turn.", Toast.LENGTH_SHORT).show();
+
+
+ QueueITWaitingRoomProviderListener queueITWaitingRoomProviderListener = new QueueITWaitingRoomProviderListener() {
+ @Override
+ public void onSuccess(QueueTryPassResult queueTryPassResult) {
+ _queueTryPassResult = queueTryPassResult;
+ Toast.makeText(getApplicationContext(), String.format("Queue status: %s", _queueTryPassResult.getRedirectType() ), Toast.LENGTH_SHORT).show();
+ queue_button.setEnabled(true);
+ }
+
+ @Override
+ public void onFailure(String errorMessage, Error errorCode) {
+ Log.e("QueueITEngine",errorMessage);
+ Toast.makeText(getApplicationContext(), String.format("Error! %s", errorMessage), Toast.LENGTH_SHORT).show();
+ }
+ };
+
+ return new QueueITWaitingRoomProvider(MainActivity.this, customerId, eventOrAliasId, layoutName, language, queueITWaitingRoomProviderListener);
+ }
+
+
+ private void showResultActivity(String result, boolean success) {
+ Intent intent = new Intent(this, ResultActivity.class);
+ intent.putExtra("success", success);
+ intent.putExtra("result", result);
+ startActivity(intent);
+ }
+
+ private boolean isAlphaNumeric(String s) {
+ String pattern = "^[a-zA-Z0-9]*$";
+ return s.matches(pattern);
+ }
+
+ private void hideKeyboard() {
+ View view = this.getCurrentFocus();
+ if (view != null) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+
+ private TextValidator getRequiredTextValidator(TextView textView) {
+ return new TextValidator(textView) {
+ @Override
+ public void validate(TextView textView, String text) {
+ if (TextUtils.isEmpty(text)) {
+ textView.setError("Field required");
+ } else if (!isAlphaNumeric(text)) {
+ textView.setError("Must be alphanumeric");
+ }
+ }
+ };
+ }
+}
diff --git a/demoapp/src/library_androidx/java/com/queue_it/shopdemo/ResultActivity.java b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/ResultActivity.java
new file mode 100644
index 0000000..fb52e00
--- /dev/null
+++ b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/ResultActivity.java
@@ -0,0 +1,55 @@
+package com.queue_it.shopdemo;
+
+import android.content.Intent;
+import android.os.Bundle;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+
+public class ResultActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_result);
+
+ String result;
+ boolean success;
+ if (savedInstanceState == null) {
+ Bundle extras = getIntent().getExtras();
+ if (extras == null) {
+ result = null;
+ success = false;
+ } else {
+ result = extras.getString("result");
+ success = extras.getBoolean("success");
+ }
+ } else {
+ result = (String) savedInstanceState.getSerializable("result");
+ success = (Boolean) savedInstanceState.getSerializable("success");
+ }
+
+ final TextView resultText = (TextView)findViewById(R.id.result_text);
+ final ImageView checkedImageView = (ImageView)findViewById(R.id.image_view_checked);
+ final FloatingActionButton retry_button = (FloatingActionButton)findViewById(R.id.retry_button);
+
+ resultText.setText(result);
+ if (success){
+ checkedImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_check_black_24dp, null));
+ } else {
+ checkedImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_error_black_24dp, null));
+ }
+
+ retry_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(ResultActivity.this, MainActivity.class);
+ startActivity(intent);
+ finish();
+ }
+ });
+ }
+}
diff --git a/demoapp/src/library_androidx/java/com/queue_it/shopdemo/SecondFragment.java b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/SecondFragment.java
new file mode 100644
index 0000000..6b6a211
--- /dev/null
+++ b/demoapp/src/library_androidx/java/com/queue_it/shopdemo/SecondFragment.java
@@ -0,0 +1,35 @@
+package com.queue_it.shopdemo;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import androidx.navigation.fragment.NavHostFragment;
+
+public class SecondFragment extends Fragment {
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState
+ ) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_second, container, false);
+ }
+
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ view.findViewById(R.id.button_second).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ NavHostFragment.findNavController(SecondFragment.this)
+ .navigate(R.id.action_SecondFragment_to_FirstFragment);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/library_androidx/res/layout/activity_deep.xml b/demoapp/src/library_androidx/res/layout/activity_deep.xml
new file mode 100644
index 0000000..dc5ac47
--- /dev/null
+++ b/demoapp/src/library_androidx/res/layout/activity_deep.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/library_androidx/res/layout/activity_main.xml b/demoapp/src/library_androidx/res/layout/activity_main.xml
new file mode 100644
index 0000000..8943ac9
--- /dev/null
+++ b/demoapp/src/library_androidx/res/layout/activity_main.xml
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demoapp/src/library_androidx/res/layout/activity_result.xml b/demoapp/src/library_androidx/res/layout/activity_result.xml
new file mode 100644
index 0000000..b898426
--- /dev/null
+++ b/demoapp/src/library_androidx/res/layout/activity_result.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demoapp/src/library_androidx/res/layout/content_deep.xml b/demoapp/src/library_androidx/res/layout/content_deep.xml
new file mode 100644
index 0000000..3691469
--- /dev/null
+++ b/demoapp/src/library_androidx/res/layout/content_deep.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/demoapp/src/library_androidx/res/layout/fragment_first.xml b/demoapp/src/library_androidx/res/layout/fragment_first.xml
new file mode 100644
index 0000000..fb44a3d
--- /dev/null
+++ b/demoapp/src/library_androidx/res/layout/fragment_first.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/library_androidx/res/layout/fragment_second.xml b/demoapp/src/library_androidx/res/layout/fragment_second.xml
new file mode 100644
index 0000000..bd90524
--- /dev/null
+++ b/demoapp/src/library_androidx/res/layout/fragment_second.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/main/res/navigation/nav_graph.xml b/demoapp/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..4061ade
--- /dev/null
+++ b/demoapp/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/.gitignore b/demowithprotectedapi/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/demowithprotectedapi/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/demowithprotectedapi/build.gradle b/demowithprotectedapi/build.gradle
new file mode 100644
index 0000000..1bfa9e1
--- /dev/null
+++ b/demowithprotectedapi/build.gradle
@@ -0,0 +1,58 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ applicationId "com.example.demowithprotectedapi"
+ minSdkVersion 21
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ buildFeatures {
+ viewBinding true
+ }
+
+ flavorDimensions "androidx"
+ productFlavors{
+ library_androidx
+ //Note that this requires useAndroidx=false
+ library
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'com.google.android.material:material:1.3.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation 'androidx.navigation:navigation-fragment:2.3.5'
+ implementation 'androidx.navigation:navigation-ui:2.3.5'
+ // retrofit
+ implementation 'com.squareup.retrofit2:retrofit:2.1.0'
+ implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
+ //Queue-it
+ //implementation project(path: ':library')
+ library_androidxImplementation project(path: ':library')
+
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+}
\ No newline at end of file
diff --git a/demowithprotectedapi/proguard-rules.pro b/demowithprotectedapi/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/demowithprotectedapi/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/demowithprotectedapi/src/androidTest/java/com/example/demowithprotectedapi/ExampleInstrumentedTest.java b/demowithprotectedapi/src/androidTest/java/com/example/demowithprotectedapi/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..3329d0a
--- /dev/null
+++ b/demowithprotectedapi/src/androidTest/java/com/example/demowithprotectedapi/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.example.demowithprotectedapi;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.example.demowithprotectedapi", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/AndroidManifest.xml b/demowithprotectedapi/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1439de8
--- /dev/null
+++ b/demowithprotectedapi/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/FirstFragment.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/FirstFragment.java
new file mode 100644
index 0000000..3953325
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/FirstFragment.java
@@ -0,0 +1,176 @@
+package com.example.demowithprotectedapi;
+
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import com.example.demowithprotectedapi.api.Product;
+import com.example.demowithprotectedapi.databinding.FragmentFirstBinding;
+import com.example.demowithprotectedapi.exceptions.MustBeQueued;
+import com.example.demowithprotectedapi.repos.IProductRepository;
+import com.example.demowithprotectedapi.repos.RetrofitProductRepository;
+import com.queue_it.androidsdk.Error;
+import com.queue_it.androidsdk.QueueDisabledInfo;
+import com.queue_it.androidsdk.QueueITApiClient;
+import com.queue_it.androidsdk.QueueITEngine;
+import com.queue_it.androidsdk.QueueITException;
+import com.queue_it.androidsdk.QueueListener;
+import com.queue_it.androidsdk.QueuePassedInfo;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class FirstFragment extends Fragment {
+
+ private FragmentFirstBinding binding;
+ private IProductRepository _productRepo;
+ private final Object queuedLock = new Object();
+ private final AtomicBoolean _queuePassed = new AtomicBoolean(false);
+
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState
+ ) {
+ _productRepo = new RetrofitProductRepository("https://fastly.v3.ticketania.com");
+ binding = FragmentFirstBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ binding.buttonFirst.setOnClickListener(view1 -> {
+ ProductHandler productHandler = new ProductHandler();
+ productHandler.execute();
+ });
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
+ }
+
+ private void queueUser(String value) {
+ try {
+ Uri valueUri = Uri.parse(URLDecoder.decode(value, StandardCharsets.UTF_8.name()));
+ String customerId = valueUri.getQueryParameter("c");
+ String wrId = valueUri.getQueryParameter("e");
+ QueueITApiClient.IsTest = true;
+ final QueueITEngine q = new QueueITEngine(MainActivity.getInstance(), customerId, wrId, "", "", new QueueListener() {
+ @Override
+ protected void onSessionRestart(QueueITEngine queueITEngine) {
+ try {
+ queueITEngine.run(MainActivity.getInstance());
+ } catch (QueueITException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ protected void onQueuePassed(QueuePassedInfo queuePassedInfo) {
+ _productRepo.addQueueToken(queuePassedInfo.getQueueItToken());
+ _queuePassed.set(true);
+ Toast.makeText(MainActivity.getInstance(), "You passed the queue! You can try again.", Toast.LENGTH_SHORT).show();
+ synchronized (queuedLock) {
+ queuedLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueViewWillOpen() {
+ Toast.makeText(MainActivity.getInstance(), "onQueueViewWillOpen", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onUserExited() {
+ Toast.makeText(MainActivity.getInstance(), "onUserExited", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onQueueDisabled(QueueDisabledInfo queueDisabledInfo) {
+ Toast.makeText(MainActivity.getInstance(), "The queue is disabled. Your token: " + queueDisabledInfo.getQueueItToken()
+ , Toast.LENGTH_SHORT).show();
+ _productRepo.addQueueToken(queueDisabledInfo.getQueueItToken());
+ _queuePassed.set(true);
+ synchronized (queuedLock) {
+ queuedLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueItUnavailable() {
+ Toast.makeText(MainActivity.getInstance(), "Queue-it is unavailable", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onError(Error error, String errorMessage) {
+ Toast.makeText(MainActivity.getInstance(), "Critical error: " + errorMessage, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onWebViewClosed() {
+ Toast.makeText(MainActivity.getInstance(), "WebView closed", Toast.LENGTH_SHORT).show();
+ }
+ });
+ q.run(MainActivity.getInstance());
+ } catch (QueueITException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public class ProductHandler extends AsyncTask {
+
+
+ @Override
+ protected Product doInBackground(Void[] objects) {
+ synchronized (queuedLock) {
+ try {
+ return _productRepo.getProduct();
+ } catch (IOException e) {
+ if (!(e instanceof MustBeQueued)) {
+ e.printStackTrace();
+ }
+ assert e instanceof MustBeQueued;
+ Handler handler = new Handler(MainActivity.getInstance().getMainLooper());
+ handler.post(() -> queueUser(((MustBeQueued) e).getValue()));
+
+ //Maybe wait for completion and repeat the call? This is optional.
+ try {
+ while (!_queuePassed.get()) {
+ queuedLock.wait();
+ }
+ Thread.sleep(1000);
+ return _productRepo.getProduct();
+ } catch (InterruptedException | IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Product p) {
+ if (p == null) {
+ Toast.makeText(MainActivity.getInstance(), "Couldn't fetch product", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(MainActivity.getInstance(), "Fetched product: " + p.getName(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/MainActivity.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/MainActivity.java
new file mode 100644
index 0000000..cd32911
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/MainActivity.java
@@ -0,0 +1,82 @@
+package com.example.demowithprotectedapi;
+
+import android.os.Bundle;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.view.View;
+
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+
+import com.example.demowithprotectedapi.databinding.ActivityMainBinding;
+
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class MainActivity extends AppCompatActivity {
+
+ private AppBarConfiguration appBarConfiguration;
+ private ActivityMainBinding binding;
+ private static MainActivity _instance;
+
+ public static MainActivity getInstance(){
+ return _instance;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ _instance = this;
+ super.onCreate(savedInstanceState);
+
+ binding = ActivityMainBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setSupportActionBar(binding.toolbar);
+
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
+ appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
+ NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
+
+ binding.fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show();
+ }
+ });
+ }
+
+ @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);
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
+ return NavigationUI.navigateUp(navController, appBarConfiguration)
+ || super.onSupportNavigateUp();
+ }
+}
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/Product.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/Product.java
new file mode 100644
index 0000000..d4ba2d9
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/Product.java
@@ -0,0 +1,13 @@
+package com.example.demowithprotectedapi.api;
+
+public class Product {
+ String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/ProductFilter.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/ProductFilter.java
new file mode 100644
index 0000000..95c3ffc
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/ProductFilter.java
@@ -0,0 +1,5 @@
+package com.example.demowithprotectedapi.api;
+
+public class ProductFilter {
+ public String id;
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/ProductsService.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/ProductsService.java
new file mode 100644
index 0000000..bce6625
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/api/ProductsService.java
@@ -0,0 +1,14 @@
+package com.example.demowithprotectedapi.api;
+
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+
+public interface ProductsService {
+ @GET("safeaction?queue-event1-nodomain=t")
+ Call getProduct();
+}
+
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/exceptions/MustBeQueued.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/exceptions/MustBeQueued.java
new file mode 100644
index 0000000..d795b7b
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/exceptions/MustBeQueued.java
@@ -0,0 +1,16 @@
+package com.example.demowithprotectedapi.exceptions;
+
+import java.io.IOException;
+
+public class MustBeQueued extends IOException {
+ private final String value;
+
+ public MustBeQueued(String s) {
+ super("Must be queued in: " + s);
+ this.value = s;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/AddCookiesInterceptor.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/AddCookiesInterceptor.java
new file mode 100644
index 0000000..405a871
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/AddCookiesInterceptor.java
@@ -0,0 +1,31 @@
+package com.example.demowithprotectedapi.http;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.HashSet;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class AddCookiesInterceptor implements Interceptor {
+
+ private final CookieStorage _storage;
+
+ public AddCookiesInterceptor(CookieStorage storage){
+ _storage = storage;
+ }
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Request.Builder builder = chain.request().newBuilder();
+ HashSet preferences = _storage.getCookies();
+ for (String cookie : preferences) {
+ builder.addHeader("Cookie", cookie);
+ Log.v("OkHttp", "Adding Header: " + cookie); // This is done so I know which headers are being added; this interceptor is used after the normal logging of OkHttp
+ }
+
+ return chain.proceed(builder.build());
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/CookieStorage.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/CookieStorage.java
new file mode 100644
index 0000000..ab86f0a
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/CookieStorage.java
@@ -0,0 +1,23 @@
+package com.example.demowithprotectedapi.http;
+
+import java.util.HashSet;
+
+public class CookieStorage {
+ private HashSet _cookies;
+
+ public CookieStorage() {
+ _cookies = new HashSet<>();
+ }
+
+ public void store(HashSet cookies) {
+ _cookies = cookies;
+ }
+
+ public HashSet getCookies() {
+ return _cookies;
+ }
+
+ public void clear() {
+ _cookies.clear();
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/QueueITInterceptor.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/QueueITInterceptor.java
new file mode 100644
index 0000000..94f7940
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/QueueITInterceptor.java
@@ -0,0 +1,61 @@
+package com.example.demowithprotectedapi.http;
+
+import com.example.demowithprotectedapi.exceptions.MustBeQueued;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+import okhttp3.Headers;
+import okhttp3.HttpUrl;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class QueueITInterceptor implements Interceptor {
+
+ private final CookieStorage _cookies;
+ private String _queueItToken;
+
+ public QueueITInterceptor(CookieStorage cookies) {
+ _cookies = cookies;
+ }
+
+ public void setToken(String token){
+ _queueItToken = token;
+ }
+
+ @NotNull
+ @Override
+ public Response intercept(@NotNull Chain chain) throws IOException {
+ HttpUrl.Builder urlBuilder = chain.request().url().newBuilder();
+ if (_queueItToken!=null && _queueItToken.length() > 0) {
+ urlBuilder.addQueryParameter("queueittoken", _queueItToken);
+ }
+
+ Request chainReq = chain.request();
+ Request req = chainReq.newBuilder()
+ .addHeader("x-queueit-ajaxpageurl", chainReq.url().toString())
+ .url(urlBuilder.build())
+ .build();
+
+ Response res = chain.proceed(req);
+ if (mustQueue(res)) {
+ resetToken();
+ _cookies.clear();
+ Headers resHeaders = res.headers();
+ throw new MustBeQueued(resHeaders.get("x-queueit-redirect"));
+ }
+
+ return res;
+ }
+
+ public void resetToken() {
+ _queueItToken = null;
+ }
+
+ public boolean mustQueue(Response response) {
+ Headers responseHeaders = response.headers();
+ return responseHeaders.names().contains("x-queueit-redirect");
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/ReceivedCookiesInterceptor.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/ReceivedCookiesInterceptor.java
new file mode 100644
index 0000000..45bba1f
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/ReceivedCookiesInterceptor.java
@@ -0,0 +1,46 @@
+package com.example.demowithprotectedapi.http;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.HashSet;
+
+import okhttp3.HttpUrl;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class ReceivedCookiesInterceptor implements Interceptor {
+
+ private final CookieStorage _storage;
+
+ public ReceivedCookiesInterceptor(CookieStorage storage) {
+ _storage = storage;
+ }
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Response originalResponse = chain.proceed(chain.request());
+
+ if (!originalResponse.headers("Set-Cookie").isEmpty()) {
+ HashSet cookies = new HashSet<>();
+
+ for (String header : originalResponse.headers("Set-Cookie")) {
+ cookies.add(header);
+ }
+
+ _storage.store(cookies);
+ }
+ if (originalResponse.priorResponse() != null && !originalResponse.priorResponse().headers("Set-Cookie").isEmpty()) {
+ HashSet cookies = new HashSet<>();
+
+ for (String header : originalResponse.priorResponse().headers("Set-Cookie")) {
+ cookies.add(header);
+ }
+
+ _storage.store(cookies);
+ }
+
+ return originalResponse;
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/UserAgentInterceptor.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/UserAgentInterceptor.java
new file mode 100644
index 0000000..088ae09
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/http/UserAgentInterceptor.java
@@ -0,0 +1,40 @@
+package com.example.demowithprotectedapi.http;
+
+import android.os.Build;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class UserAgentInterceptor implements Interceptor {
+
+ public final String userAgent;
+
+ public UserAgentInterceptor(String userAgent) {
+ this.userAgent = userAgent;
+ }
+
+ public UserAgentInterceptor(String appName, String appVersion) {
+ this(String.format(Locale.US,
+ "%s/%s (Android %s; %s; %s %s; %s)",
+ appName,
+ appVersion,
+ Build.VERSION.RELEASE,
+ Build.MODEL,
+ Build.BRAND,
+ Build.DEVICE,
+ Locale.getDefault().getLanguage()));
+ }
+
+ @Override
+ public Response intercept(Interceptor.Chain chain) throws IOException {
+ Request userAgentRequest = chain.request()
+ .newBuilder()
+ .header("User-Agent", userAgent)
+ .build();
+ return chain.proceed(userAgentRequest);
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/IProductRepository.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/IProductRepository.java
new file mode 100644
index 0000000..db7cef6
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/IProductRepository.java
@@ -0,0 +1,12 @@
+package com.example.demowithprotectedapi.repos;
+
+import com.example.demowithprotectedapi.exceptions.MustBeQueued;
+import com.example.demowithprotectedapi.api.Product;
+
+import java.io.IOException;
+
+public interface IProductRepository {
+ Product getProduct() throws IOException, MustBeQueued;
+
+ void addQueueToken(String queueItToken);
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/ProductRepository.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/ProductRepository.java
new file mode 100644
index 0000000..d10ee19
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/ProductRepository.java
@@ -0,0 +1,54 @@
+package com.example.demowithprotectedapi.repos;
+
+import com.example.demowithprotectedapi.exceptions.MustBeQueued;
+import com.example.demowithprotectedapi.api.Product;
+import com.example.demowithprotectedapi.http.AddCookiesInterceptor;
+import com.example.demowithprotectedapi.http.CookieStorage;
+import com.example.demowithprotectedapi.http.QueueITInterceptor;
+import com.example.demowithprotectedapi.http.ReceivedCookiesInterceptor;
+import com.example.demowithprotectedapi.http.UserAgentInterceptor;
+import com.google.gson.Gson;
+
+import java.io.IOException;
+
+import okhttp3.Headers;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class ProductRepository implements IProductRepository {
+ private final OkHttpClient _client;
+ private final String _baseUrl;
+ private final QueueITInterceptor _queueItInterceptor;
+
+ public ProductRepository() {
+ _baseUrl = "https://fastly.v3.ticketania.com";
+ CookieStorage cookies = new CookieStorage();
+ _queueItInterceptor = new QueueITInterceptor(cookies);
+ _client = new OkHttpClient().newBuilder()
+ .addInterceptor(_queueItInterceptor)
+ .addInterceptor(new AddCookiesInterceptor(cookies))
+ .addInterceptor(new ReceivedCookiesInterceptor(cookies))
+ .hostnameVerifier((hostname, session) -> true)
+ .build();
+ }
+
+ private Request.Builder getRequestBuilder() {
+ return new Request.Builder()
+ .header("x-queueit-ajaxpageurl", "someValue");
+ }
+
+ public Product getProduct() throws IOException, MustBeQueued {
+ String fullURL = _baseUrl + "/safeaction?queue-event1-nodomain=t&";
+ Request request = getRequestBuilder()
+ .url(fullURL)
+ .build();
+ Response response = _client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+ return new Gson().fromJson(response.body().string(), Product.class);
+ }
+
+ public void addQueueToken(String queueItToken) {
+ _queueItInterceptor.setToken(queueItToken);
+ }
+}
diff --git a/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/RetrofitProductRepository.java b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/RetrofitProductRepository.java
new file mode 100644
index 0000000..887bdf2
--- /dev/null
+++ b/demowithprotectedapi/src/main/java/com/example/demowithprotectedapi/repos/RetrofitProductRepository.java
@@ -0,0 +1,54 @@
+package com.example.demowithprotectedapi.repos;
+
+import com.example.demowithprotectedapi.api.Product;
+import com.example.demowithprotectedapi.api.ProductFilter;
+import com.example.demowithprotectedapi.api.ProductsService;
+import com.example.demowithprotectedapi.http.AddCookiesInterceptor;
+import com.example.demowithprotectedapi.http.CookieStorage;
+import com.example.demowithprotectedapi.http.QueueITInterceptor;
+import com.example.demowithprotectedapi.http.ReceivedCookiesInterceptor;
+import com.example.demowithprotectedapi.http.UserAgentInterceptor;
+
+import java.io.IOException;
+
+import okhttp3.OkHttpClient;
+import retrofit2.Call;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+public class RetrofitProductRepository implements IProductRepository {
+ private final ProductsService _service;
+ private final String _baseUrl;
+ private final QueueITInterceptor _queueItInterceptor;
+
+ public RetrofitProductRepository(String baseUrl) {
+ _baseUrl = baseUrl;
+ CookieStorage cookies = new CookieStorage();
+ _queueItInterceptor = new QueueITInterceptor(cookies);
+ OkHttpClient client = new OkHttpClient.Builder()
+ .addInterceptor(_queueItInterceptor)
+ .addInterceptor(new AddCookiesInterceptor(cookies))
+ .addInterceptor(new ReceivedCookiesInterceptor(cookies))
+ .addInterceptor(new UserAgentInterceptor("demoapp", "1.0.0"))
+ .hostnameVerifier((hostname, session) -> true)
+ .build();
+
+
+ Retrofit retrofit = new Retrofit.Builder()
+ .client(client)
+ .baseUrl(_baseUrl)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build();
+ _service = retrofit.create(ProductsService.class);
+ }
+
+ public Product getProduct() throws IOException {
+ Call pCall = _service.getProduct();
+ retrofit2.Response response = pCall.execute();
+ return response.body();
+ }
+
+ public void addQueueToken(String queueItToken) {
+ _queueItInterceptor.setToken(queueItToken);
+ }
+}
diff --git a/demowithprotectedapi/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demowithprotectedapi/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/drawable/ic_launcher_background.xml b/demowithprotectedapi/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demowithprotectedapi/src/main/res/layout/activity_main.xml b/demowithprotectedapi/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..6fac3f4
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/layout/activity_main.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/layout/content_main.xml b/demowithprotectedapi/src/main/res/layout/content_main.xml
new file mode 100644
index 0000000..4f68632
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/layout/content_main.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/layout/fragment_first.xml b/demowithprotectedapi/src/main/res/layout/fragment_first.xml
new file mode 100644
index 0000000..fb44a3d
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/layout/fragment_first.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/menu/menu_main.xml b/demowithprotectedapi/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..df686e4
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/menu/menu_main.xml
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demowithprotectedapi/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demowithprotectedapi/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/mipmap-hdpi/ic_launcher.webp b/demowithprotectedapi/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/demowithprotectedapi/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-mdpi/ic_launcher.webp b/demowithprotectedapi/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/demowithprotectedapi/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-xhdpi/ic_launcher.webp b/demowithprotectedapi/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/demowithprotectedapi/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/demowithprotectedapi/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/demowithprotectedapi/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/demowithprotectedapi/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/demowithprotectedapi/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/demowithprotectedapi/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/demowithprotectedapi/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/demowithprotectedapi/src/main/res/navigation/nav_graph.xml b/demowithprotectedapi/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..5e4bf1b
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/values-night/themes.xml b/demowithprotectedapi/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..5f574a9
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/values/colors.xml b/demowithprotectedapi/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/values/dimens.xml b/demowithprotectedapi/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..125df87
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+
+ 16dp
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/values/strings.xml b/demowithprotectedapi/src/main/res/values/strings.xml
new file mode 100644
index 0000000..253432f
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+
+ demoWithProtectedApi
+ Settings
+
+ First Fragment
+ Second Fragment
+ Next
+ Previous
+
+ Hello first fragment
+ Hello second fragment. Arg: %1$s
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/main/res/values/themes.xml b/demowithprotectedapi/src/main/res/values/themes.xml
new file mode 100644
index 0000000..86828a9
--- /dev/null
+++ b/demowithprotectedapi/src/main/res/values/themes.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demowithprotectedapi/src/test/java/com/example/demowithprotectedapi/ExampleUnitTest.java b/demowithprotectedapi/src/test/java/com/example/demowithprotectedapi/ExampleUnitTest.java
new file mode 100644
index 0000000..e78f517
--- /dev/null
+++ b/demowithprotectedapi/src/test/java/com/example/demowithprotectedapi/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.demowithprotectedapi;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/documentation/App + Connector integration with QueueITToken.png b/documentation/App + Connector integration with QueueITToken.png
new file mode 100644
index 0000000..dc4cb30
Binary files /dev/null and b/documentation/App + Connector integration with QueueITToken.png differ
diff --git a/documentation/protected_apis.md b/documentation/protected_apis.md
new file mode 100644
index 0000000..a64775f
--- /dev/null
+++ b/documentation/protected_apis.md
@@ -0,0 +1,25 @@
+# Using connector protected APIs with the SDK
+
+## Sample app
+
+A sample app that shows an example of this integration can be found in the [demowithprotectedapi](https://github.com/queueit/android-webui-sdk/tree/master/demowithprotectedapi) directory.
+There are a few OkHTTP interceptors in the `http` package that can be easily integrated.
+
+## Implementation
+
+To integrate with a protected API we need to handle the validation responses that we may get in case the user should be queued.
+All calls to protected APIs need to include the `x-queueit-ajaxpageurl` header with a non-empty value and a Queue-it accepted cookie (if present).
+The integration can be described in the following steps:
+
+1. API Request with is made
+2. We get a response which may be the API response or a notice that the user should be queued
+3. Scenario 1, user should be queued
+3.1. If the user should be queued we'll get a 302 response with a `x-queueit-redirect` header. We need to extract the `c`(Customer ID) and `e`(Waiting Room ID) query string parameters from the header and call `QueueITEngine.run` with them, just as you would normally do with the SDK.
+3.2. We wait for the `onQueuePassed` callback and we store the QueueITToken.
+3.3. We can repeat the API request appending the `queueittoken={QueueITToken}` query string parameter.
+3.4. We store the Queue-it cookies from the response, so they can be set in other API calls.
+
+4. Scenario 2, user should not be queued
+4.1. We store the Queue-it cookies from the response
+
+![API Integration Flow](https://github.com/queueit/android-webui-sdk/blob/master/documentation/App%20+%20Connector%20integration%20with%20QueueITToken.png "App Integration Flow")
diff --git a/library/src/library/java/com/queue_it/androidsdk/QueueActivity.java b/library/src/library/java/com/queue_it/androidsdk/QueueActivity.java
new file mode 100644
index 0000000..9ac676a
--- /dev/null
+++ b/library/src/library/java/com/queue_it/androidsdk/QueueActivity.java
@@ -0,0 +1,35 @@
+package com.queue_it.androidsdk;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class QueueActivity extends Activity {
+
+ private QueueActivityBase base = new QueueActivityBase(this);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ base.initialize(savedInstanceState);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (base.getOptions().isBackButtonDisabledFromWR()) {
+ return;
+ }
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ base.saveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ base.destroy();
+ super.onDestroy();
+ }
+}
diff --git a/library/src/library/java/com/queue_it/androidsdk/WaitingRoomStateBroadcaster.java b/library/src/library/java/com/queue_it/androidsdk/WaitingRoomStateBroadcaster.java
new file mode 100644
index 0000000..68cb127
--- /dev/null
+++ b/library/src/library/java/com/queue_it/androidsdk/WaitingRoomStateBroadcaster.java
@@ -0,0 +1,97 @@
+package com.queue_it.androidsdk;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.v4.content.LocalBroadcastManager;
+
+public class WaitingRoomStateBroadcaster implements IWaitingRoomStateBroadcaster {
+
+ private final Context _context;
+
+ public WaitingRoomStateBroadcaster(Context context) {
+ _context = context;
+ }
+
+ public void registerReceivers(BroadcastReceiver onPassed,
+ BroadcastReceiver onUrlChanged,
+ BroadcastReceiver onActivityClosed,
+ BroadcastReceiver onUserExited,
+ BroadcastReceiver onError,
+ BroadcastReceiver onWebViewClosed,
+ BroadcastReceiver onSessionRestartReceiver) {
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(_context);
+
+ localBroadcastManager.registerReceiver(onPassed, new IntentFilter("on-queue-passed"));
+ localBroadcastManager.registerReceiver(onUrlChanged, new IntentFilter("on-changed-queue-url"));
+ localBroadcastManager.registerReceiver(onActivityClosed, new IntentFilter("queue-activity-closed"));
+ localBroadcastManager.registerReceiver(onUserExited, new IntentFilter("queue-user-exited"));
+ localBroadcastManager.registerReceiver(onError, new IntentFilter("on-queue-error"));
+ localBroadcastManager.registerReceiver(onWebViewClosed, new IntentFilter("on-webview-close"));
+ localBroadcastManager.registerReceiver(onSessionRestartReceiver, new IntentFilter("on-session-restart"));
+ }
+
+ public void unregisterReceivers(BroadcastReceiver onPassed,
+ BroadcastReceiver onUrlChanged,
+ BroadcastReceiver onActivityClosed,
+ BroadcastReceiver onUserExited,
+ BroadcastReceiver onError,
+ BroadcastReceiver onWebViewClosed,
+ BroadcastReceiver onSessionRestartReceiver) {
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(_context);
+
+ localBroadcastManager.unregisterReceiver(onPassed);
+ localBroadcastManager.unregisterReceiver(onUrlChanged);
+ localBroadcastManager.unregisterReceiver(onActivityClosed);
+ localBroadcastManager.unregisterReceiver(onUserExited);
+ localBroadcastManager.unregisterReceiver(onError);
+ localBroadcastManager.unregisterReceiver(onWebViewClosed);
+ localBroadcastManager.unregisterReceiver(onSessionRestartReceiver);
+ }
+
+ @Override
+ public void broadcastChangedQueueUrl(String urlString) {
+ Intent intentChangedQueueUrl = new Intent("on-changed-queue-url");
+ intentChangedQueueUrl.putExtra("url", urlString);
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intentChangedQueueUrl);
+ }
+
+ @Override
+ public void broadcastQueuePassed(String queueItToken) {
+ Intent intent = new Intent("on-queue-passed");
+ intent.putExtra("queue-it-token", queueItToken);
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastQueueActivityClosed() {
+ Intent intent = new Intent("queue-activity-closed");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastUserExited() {
+ Intent intent = new Intent("queue-user-exited");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastQueueError(String errorMessage) {
+ Intent intent = new Intent("on-queue-error");
+ intent.putExtra("error-message", errorMessage);
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastWebViewClosed() {
+ Intent intent = new Intent("on-webview-close");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastOnSessionRestart() {
+ Intent intent = new Intent("on-session-restart");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+}
diff --git a/library/src/library_androidx/java/com/queue_it/androidsdk/QueueActivity.java b/library/src/library_androidx/java/com/queue_it/androidsdk/QueueActivity.java
new file mode 100644
index 0000000..56b07a9
--- /dev/null
+++ b/library/src/library_androidx/java/com/queue_it/androidsdk/QueueActivity.java
@@ -0,0 +1,34 @@
+package com.queue_it.androidsdk;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class QueueActivity extends Activity {
+ private QueueActivityBase base = new QueueActivityBase(this);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ base.initialize(savedInstanceState);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (base.getOptions().isBackButtonDisabledFromWR()) {
+ return;
+ }
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ base.saveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ base.destroy();
+ super.onDestroy();
+ }
+}
diff --git a/library/src/library_androidx/java/com/queue_it/androidsdk/WaitingRoomStateBroadcaster.java b/library/src/library_androidx/java/com/queue_it/androidsdk/WaitingRoomStateBroadcaster.java
new file mode 100644
index 0000000..e1ce574
--- /dev/null
+++ b/library/src/library_androidx/java/com/queue_it/androidsdk/WaitingRoomStateBroadcaster.java
@@ -0,0 +1,98 @@
+package com.queue_it.androidsdk;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+public class WaitingRoomStateBroadcaster implements IWaitingRoomStateBroadcaster {
+
+ private final Context _context;
+
+ public WaitingRoomStateBroadcaster(Context context) {
+ _context = context;
+ }
+
+ public void registerReceivers(BroadcastReceiver onPassed,
+ BroadcastReceiver onUrlChanged,
+ BroadcastReceiver onActivityClosed,
+ BroadcastReceiver onUserExited,
+ BroadcastReceiver onError,
+ BroadcastReceiver onWebViewClosed,
+ BroadcastReceiver onSessionRestartReceiver) {
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(_context);
+
+ localBroadcastManager.registerReceiver(onPassed, new IntentFilter("on-queue-passed"));
+ localBroadcastManager.registerReceiver(onUrlChanged, new IntentFilter("on-changed-queue-url"));
+ localBroadcastManager.registerReceiver(onActivityClosed, new IntentFilter("queue-activity-closed"));
+ localBroadcastManager.registerReceiver(onUserExited, new IntentFilter("queue-user-exited"));
+ localBroadcastManager.registerReceiver(onError, new IntentFilter("on-queue-error"));
+ localBroadcastManager.registerReceiver(onWebViewClosed, new IntentFilter("on-webview-close"));
+ localBroadcastManager.registerReceiver(onSessionRestartReceiver, new IntentFilter("on-session-restart"));
+ }
+
+ public void unregisterReceivers(BroadcastReceiver onPassed,
+ BroadcastReceiver onUrlChanged,
+ BroadcastReceiver onActivityClosed,
+ BroadcastReceiver onUserExited,
+ BroadcastReceiver onError,
+ BroadcastReceiver onWebViewClosed,
+ BroadcastReceiver onSessionRestartReceiver) {
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(_context);
+
+ localBroadcastManager.unregisterReceiver(onPassed);
+ localBroadcastManager.unregisterReceiver(onUrlChanged);
+ localBroadcastManager.unregisterReceiver(onActivityClosed);
+ localBroadcastManager.unregisterReceiver(onUserExited);
+ localBroadcastManager.unregisterReceiver(onError);
+ localBroadcastManager.unregisterReceiver(onWebViewClosed);
+ localBroadcastManager.unregisterReceiver(onSessionRestartReceiver);
+ }
+
+ @Override
+ public void broadcastChangedQueueUrl(String urlString) {
+ Intent intentChangedQueueUrl = new Intent("on-changed-queue-url");
+ intentChangedQueueUrl.putExtra("url", urlString);
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intentChangedQueueUrl);
+ }
+
+ @Override
+ public void broadcastQueuePassed(String queueItToken) {
+ Intent intent = new Intent("on-queue-passed");
+ intent.putExtra("queue-it-token", queueItToken);
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastQueueActivityClosed() {
+ Intent intent = new Intent("queue-activity-closed");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastUserExited() {
+ Intent intent = new Intent("queue-user-exited");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastQueueError(String errorMessage) {
+ Intent intent = new Intent("on-queue-error");
+ intent.putExtra("error-message", errorMessage);
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastWebViewClosed() {
+ Intent intent = new Intent("on-webview-close");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+
+ @Override
+ public void broadcastOnSessionRestart() {
+ Intent intent = new Intent("on-session-restart");
+ LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/IUriOverrider.java b/library/src/main/java/com/queue_it/androidsdk/IUriOverrider.java
new file mode 100644
index 0000000..963c773
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/IUriOverrider.java
@@ -0,0 +1,20 @@
+package com.queue_it.androidsdk;
+
+import android.net.Uri;
+import android.webkit.WebView;
+
+public interface IUriOverrider {
+ Uri getQueue();
+
+ void setQueue(Uri queue);
+
+ Uri getTarget();
+
+ void setTarget(Uri target);
+
+ String getUserId();
+
+ void setUserId(String userId);
+
+ boolean handleNavigationRequest(String uriString, WebView webview, UriOverrideWrapper uriOverride);
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/IWaitingRoomStateBroadcaster.java b/library/src/main/java/com/queue_it/androidsdk/IWaitingRoomStateBroadcaster.java
new file mode 100644
index 0000000..ed33587
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/IWaitingRoomStateBroadcaster.java
@@ -0,0 +1,35 @@
+package com.queue_it.androidsdk;
+
+import android.content.BroadcastReceiver;
+
+public interface IWaitingRoomStateBroadcaster {
+ void broadcastChangedQueueUrl(String urlString);
+
+ void broadcastQueuePassed(String queueItToken);
+
+ void broadcastQueueActivityClosed();
+
+ void broadcastUserExited();
+
+ void broadcastQueueError(String errorMessage);
+
+ void broadcastWebViewClosed();
+
+ void broadcastOnSessionRestart();
+
+ void registerReceivers(BroadcastReceiver queuePassedBroadcastReceiver,
+ BroadcastReceiver queueUrlChangedBroadcastReceiver,
+ BroadcastReceiver queueActivityClosedBroadcastReceiver,
+ BroadcastReceiver queueUserExitedBroadcastReceiver,
+ BroadcastReceiver queueErrorBroadcastReceiver,
+ BroadcastReceiver webViewClosedBroadcastReceiver,
+ BroadcastReceiver onSessionRestartReceiver);
+
+ void unregisterReceivers(BroadcastReceiver queuePassedBroadcastReceiver,
+ BroadcastReceiver queueUrlChangedBroadcastReceiver,
+ BroadcastReceiver queueActivityClosedBroadcastReceiver,
+ BroadcastReceiver queueUserExitedBroadcastReceiver,
+ BroadcastReceiver queueErrorBroadcastReceiver,
+ BroadcastReceiver webViewClosedBroadcastReceiver,
+ BroadcastReceiver onSessionRestartReceiver);
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueActivityBase.java b/library/src/main/java/com/queue_it/androidsdk/QueueActivityBase.java
new file mode 100644
index 0000000..f75b5ff
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueActivityBase.java
@@ -0,0 +1,198 @@
+package com.queue_it.androidsdk;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.webkit.CookieSyncManager;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+
+public class QueueActivityBase {
+ private final Activity _context;
+ private String queueUrl;
+ private String targetUrl;
+ private WebView webview;
+ private String webViewUserAgent;
+ @SuppressLint("StaticFieldLeak")
+ private static WebView previousWebView;
+ private IUriOverrider uriOverrider;
+ private final IWaitingRoomStateBroadcaster broadcaster;
+ private QueueItEngineOptions options;
+
+ public QueueActivityBase(Activity context) {
+ _context = context;
+ options = QueueItEngineOptions.getDefault();
+ broadcaster = new WaitingRoomStateBroadcaster(_context);
+ }
+
+ public QueueItEngineOptions getOptions(){
+ return options;
+ }
+
+ WebViewClient webviewClient = new WebViewClient() {
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ CookieSyncManager.getInstance().sync();
+ }
+
+ @Override
+ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
+ String errorMessage;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ errorMessage = String.format("%s %s: %s %s", request.getMethod(), request.getUrl(), errorResponse.getStatusCode(), errorResponse.getReasonPhrase());
+ } else {
+ errorMessage = errorResponse.toString();
+ }
+ Log.v("QueueActivity", String.format("%s: %s", "onReceivedHttpError", errorMessage));
+ super.onReceivedHttpError(view, request, errorResponse);
+ }
+
+ @Override
+ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+ String errorMessage;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ errorMessage = String.format("%s %s: %s %s", request.getMethod(), request.getUrl(), error.getErrorCode(), error.getDescription());
+ } else {
+ errorMessage = error.toString();
+ }
+ Log.v("QueueActivity", String.format("%s: %s", "onReceivedError", errorMessage));
+ super.onReceivedError(view, request, error);
+ }
+
+ @Override
+ public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
+ handler.cancel();
+ broadcaster.broadcastQueueError("SslError, code: " + error.getPrimaryError());
+ disposeWebview(webview);
+ }
+
+ public boolean shouldOverrideUrlLoading(WebView view, String urlString) {
+ return uriOverrider.handleNavigationRequest(urlString, webview, new UriOverrideWrapper() {
+
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ broadcaster.broadcastChangedQueueUrl(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ broadcaster.broadcastQueuePassed(queueItToken);
+ disposeWebview(webview);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+ broadcaster.broadcastWebViewClosed();
+ disposeWebview(webview);
+ }
+
+ @Override
+ protected void onSessionRestart() {
+ broadcaster.broadcastOnSessionRestart();
+ disposeWebview(webview);
+ }
+ });
+ }
+ };
+
+ private static void cleanupWebView() {
+ if (previousWebView == null) return;
+ previousWebView.destroy();
+ previousWebView = null;
+ }
+
+ //was onCreated
+ public void initialize(Bundle savedInstanceState) {
+ uriOverrider = new UriOverrider();
+ _context.setContentView(R.layout.activity_queue);
+ readActivityExtras(savedInstanceState);
+ cleanupWebView();
+ final ProgressBar progressBar = _context.findViewById(R.id.progressBar);
+
+ FrameLayout layout = _context.findViewById(R.id.relativeLayout);
+ webview = new WebView(_context);
+ layout.addView(webview);
+ previousWebView = webview;
+ webview.getSettings().setJavaScriptEnabled(true);
+ webview.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ Log.v("Progress", Integer.toString(newProgress));
+ if (newProgress < 100) {
+ progressBar.setVisibility(View.VISIBLE);
+ } else {
+ progressBar.setVisibility(View.GONE);
+ }
+ progressBar.setProgress(newProgress);
+ super.onProgressChanged(view, newProgress);
+ }
+ });
+ webview.setWebViewClient(webviewClient);
+ Log.v("QueueITEngine", "Loading initial URL: " + queueUrl);
+ setUserAgent(webViewUserAgent);
+ webview.loadUrl(queueUrl);
+ }
+
+ public void saveInstanceState(Bundle outState) {
+ outState.putString("queueUrl", queueUrl);
+ outState.putString("targetUrl", targetUrl);
+ outState.putString("webViewUserAgent", webViewUserAgent);
+ outState.putString("userId", uriOverrider.getUserId());
+ }
+
+ public void destroy() {
+ if (_context.isFinishing()) {
+ broadcaster.broadcastQueueActivityClosed();
+ }
+ }
+
+ private void readActivityExtras(Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ Bundle extras = _context.getIntent().getExtras();
+ if (extras == null) {
+ queueUrl = null;
+ targetUrl = null;
+ webViewUserAgent = null;
+ } else {
+ queueUrl = extras.getString("queueUrl");
+ targetUrl = extras.getString("targetUrl");
+ webViewUserAgent = extras.getString("webViewUserAgent");
+ uriOverrider.setUserId(extras.getString("userId"));
+ options = (QueueItEngineOptions)extras.getParcelable("options");
+ }
+ } else {
+ queueUrl = (String) savedInstanceState.getSerializable("queueUrl");
+ targetUrl = (String) savedInstanceState.getSerializable("targetUrl");
+ webViewUserAgent = (String) savedInstanceState.getSerializable("webViewUserAgent");
+ uriOverrider.setUserId((String) savedInstanceState.getSerializable("userId"));
+ }
+
+ uriOverrider.setTarget(Uri.parse(targetUrl));
+ uriOverrider.setQueue(Uri.parse(queueUrl));
+ }
+
+ private void disposeWebview(WebView webView) {
+ webView.loadUrl("about:blank");
+ _context.finish();
+ }
+
+ private void setUserAgent(String userAgent) {
+ userAgent = (userAgent != null) ? userAgent : UserAgentManager.getUserAgent();
+ System.setProperty("http.agent", userAgent);
+ webview.getSettings().setUserAgentString(userAgent);
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueDisabledInfo.java b/library/src/main/java/com/queue_it/androidsdk/QueueDisabledInfo.java
new file mode 100644
index 0000000..819a23a
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueDisabledInfo.java
@@ -0,0 +1,13 @@
+package com.queue_it.androidsdk;
+
+public class QueueDisabledInfo {
+ private final String _queueItToken;
+
+ public QueueDisabledInfo(String queueItToken) {
+ _queueItToken = queueItToken;
+ }
+
+ public String getQueueItToken() {
+ return _queueItToken;
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueITApiClient.java b/library/src/main/java/com/queue_it/androidsdk/QueueITApiClient.java
new file mode 100644
index 0000000..11969b6
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueITApiClient.java
@@ -0,0 +1,196 @@
+package com.queue_it.androidsdk;
+
+import android.content.Context;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class QueueITApiClient {
+
+ private final String _customerId;
+ private final String _eventOrAliasId;
+ private final String _userId;
+ private final String _userAgent;
+ private final String _sdkVersion;
+ private final String _layoutName;
+ private final String _language;
+ private final String _enqueueToken;
+ private final String _enqueueKey;
+ private final QueueITApiClientListener _queueITApiClientListener;
+
+ public static boolean IsTest = false;
+
+ private URL getApiUrl() {
+ String hostname = String.format(IsTest ?
+ "%s.test.queue-it.net" :
+ "%s.queue-it.net",
+ _customerId);
+
+ String path = String.format("api/mobileapp/queue/%s/%s/enqueue", _customerId, _eventOrAliasId);
+ return new HttpUrl.Builder()
+ .scheme("https")
+ .host(hostname)
+ .addPathSegments(path)
+ .addQueryParameter("userId", _userId)
+ .build().url();
+ }
+
+ public QueueITApiClient(String customerId,
+ String eventOrAliasId,
+ String userId,
+ String userAgent,
+ String sdkVersion,
+ String layoutName,
+ String language,
+ String enqueueToken,
+ String enqueueKey,
+ QueueITApiClientListener queueITApiClientListener){
+ _customerId = customerId;
+ _eventOrAliasId = eventOrAliasId;
+ _userId = userId;
+ _userAgent = userAgent;
+ _sdkVersion = sdkVersion;
+ _layoutName = layoutName;
+ _language = language;
+ _enqueueToken = enqueueToken;
+ _enqueueKey = enqueueKey;
+ _queueITApiClientListener = queueITApiClientListener;
+ }
+
+ public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+ public void init(final Context context) {
+ URL enqueueUrl = getApiUrl();
+ OkHttpClient client = new OkHttpClient();
+ String putBody = getJsonObject().toString();
+ RequestBody body = RequestBody.create(JSON, putBody);
+
+ Log.v("QueueITApiClient", "API call " + getISO8601StringForDate(Calendar.getInstance().getTime()) + ": " + enqueueUrl.toString() + ": " + putBody);
+
+ Request request = new Request.Builder()
+ .url(enqueueUrl)
+ .post(body)
+ .build();
+ client.newCall(request).enqueue(new Callback() {
+ final Handler mainHandler = new Handler(context.getMainLooper());
+
+ @Override
+ public void onFailure(Call call, IOException e) {
+ final String message = e.toString();
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ _queueITApiClientListener.onFailure(message, 0);
+ }
+ });
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ if (!response.isSuccessful()) {
+ final String errorMessage = String.format("%s %s", response.message(), response.body().string());
+ final int code = response.code();
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ _queueITApiClientListener.onFailure(errorMessage, code);
+ }
+ });
+ return;
+ }
+
+ final String body = response.body().string();
+ try {
+ JSONObject jsonObject = new JSONObject(body);
+ final String queueId = optString(jsonObject, "QueueId");
+ final String queueUrl = optString(jsonObject, "QueueUrl");
+ final int queueUrlTtlInMinutes = optInt(jsonObject, "QueueUrlTTLInMinutes");
+ final String eventTargetUrl = optString(jsonObject, "EventTargetUrl");
+ final String queueItToken = optString(jsonObject, "QueueitToken");
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ String updatedQueueUrl = QueueUrlHelper.urlUpdateNeeded(queueUrl, _userId) ?
+ QueueUrlHelper.updateUrl(queueUrl, _userId).toString() :
+ queueUrl;
+ _queueITApiClientListener.onSuccess(queueId, updatedQueueUrl, queueUrlTtlInMinutes, eventTargetUrl, queueItToken);
+ }
+ });
+ } catch (JSONException e) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ _queueITApiClientListener.onFailure("Server did not return valid JSON: " + body, 0);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private static String getISO8601StringForDate(Date date) {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return dateFormat.format(date);
+ }
+
+ private String optString(JSONObject json, String key) {
+ if (json.isNull(key))
+ return null;
+ else
+ return json.optString(key, null);
+ }
+
+ private int optInt(JSONObject json, String key) {
+ if (json.isNull(key))
+ return 0;
+ else
+ return json.optInt(key, 0);
+ }
+
+ private JSONObject getJsonObject() {
+ try {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("userId", _userId);
+ jsonObject.put("userAgent", _userAgent);
+ jsonObject.put("sdkVersion", _sdkVersion);
+ if (!TextUtils.isEmpty(_layoutName)) {
+ jsonObject.put("layoutName", _layoutName);
+ }
+ if (!TextUtils.isEmpty(_language)) {
+ jsonObject.put("language", _language);
+ }
+ if(!TextUtils.isEmpty(_enqueueToken)){
+ jsonObject.put("enqueueToken", _enqueueToken);
+ }
+ if(!TextUtils.isEmpty(_enqueueKey)){
+ jsonObject.put("enqueueKey", _enqueueKey);
+ }
+
+ return jsonObject;
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueITApiClientListener.java b/library/src/main/java/com/queue_it/androidsdk/QueueITApiClientListener.java
new file mode 100644
index 0000000..619c2d3
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueITApiClientListener.java
@@ -0,0 +1,6 @@
+package com.queue_it.androidsdk;
+
+interface QueueITApiClientListener {
+ void onSuccess(String queueId, String queueUrlString, int queueUrlTtlInMinutes, String eventTargetUrl, String queueItToken);
+ void onFailure(String errorMessage, int errorCode);
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomProvider.java b/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomProvider.java
new file mode 100644
index 0000000..3b42ca0
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomProvider.java
@@ -0,0 +1,235 @@
+package com.queue_it.androidsdk;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class QueueITWaitingRoomProvider {
+
+ private final String _customerId;
+ private final String _eventOrAliasId;
+ private final String _layoutName;
+ private final String _language;
+ private final QueueITWaitingRoomProviderListener _queueITWaitingRoomProviderListener;
+ private Context _context;
+ private final AtomicBoolean _requestInProgress;
+
+ private static final int INITIAL_WAIT_RETRY_SEC = 1;
+ private static final int MAX_RETRY_SEC = 10;
+ private int _isOnlineRetry = 0;
+ private int _deltaSec;
+
+ private EnqueueRunner _checkConnectionRunner = new EnqueueRunner();
+ private Handler _checkConnectionHandler;
+
+ private static final Pattern pattern = Pattern.compile("\\~rt_(.*?)\\~");
+
+ public QueueITWaitingRoomProvider(Context activityContext,
+ String customerId,
+ String eventOrAliasId,
+ QueueITWaitingRoomProviderListener queueITWaitingRoomProviderListener) {
+ this(activityContext,
+ customerId,
+ eventOrAliasId,
+ "",
+ "",
+ queueITWaitingRoomProviderListener
+ );
+ }
+
+ public QueueITWaitingRoomProvider(Context activityContext,
+ String customerId,
+ String eventOrAliasId,
+ String layoutName,
+ String language,
+ QueueITWaitingRoomProviderListener queueITWaitingRoomProviderListener) {
+ _requestInProgress = new AtomicBoolean(false);
+ UserAgentManager.initialize(activityContext);
+ if (TextUtils.isEmpty(customerId)) {
+ throw new IllegalArgumentException("customerId must have a value");
+ }
+ if (TextUtils.isEmpty(eventOrAliasId)) {
+ throw new IllegalArgumentException("eventOrAliasId must have a value");
+ }
+ _context = activityContext;
+ _customerId = customerId;
+ _eventOrAliasId = eventOrAliasId;
+ _layoutName = layoutName;
+ _language = language;
+ _queueITWaitingRoomProviderListener = queueITWaitingRoomProviderListener;
+ _deltaSec = INITIAL_WAIT_RETRY_SEC;
+ }
+
+
+ public void tryPass() throws QueueITException {
+ if (_requestInProgress.getAndSet(true)) {
+ throw new QueueITException("Request is already in progress");
+ }
+ _checkConnectionRunner = new QueueITWaitingRoomProvider.EnqueueRunner();
+ _checkConnectionHandler = new Handler();
+ _checkConnectionRunner.run();
+ }
+
+ public void tryPassWithEnqueueToken(String enqueueToken)
+ throws QueueITException {
+ if (_requestInProgress.getAndSet(true)) {
+ throw new QueueITException("Request is already in progress");
+ }
+
+ _checkConnectionRunner = new QueueITWaitingRoomProvider.EnqueueRunner(enqueueToken, null);
+ _checkConnectionHandler = new Handler();
+ _checkConnectionRunner.run();
+ }
+
+ public void tryPassWithEnqueueKey(String enqueueKey)
+ throws QueueITException {
+ if (_requestInProgress.getAndSet(true)) {
+ throw new QueueITException("Request is already in progress");
+ }
+
+ _checkConnectionRunner = new QueueITWaitingRoomProvider.EnqueueRunner(null, enqueueKey);
+ _checkConnectionHandler = new Handler();
+ _checkConnectionRunner.run();
+ }
+
+ private void runWithConnection(String enqueueToken, String enqueueKey) {
+ tryEnqueue(enqueueToken, enqueueKey);
+ _requestInProgress.set(false);
+ }
+
+ private void tryEnqueue(String enqueueToken, String enqueueKey) {
+ String userId = getUserId();
+ String userAgent = UserAgentManager.getUserAgent();
+ String sdkVersion = getSdkVersion();
+
+ QueueITApiClientListener queueITApiClientListener = new QueueITApiClientListener() {
+ @Override
+ public void onSuccess(String queueId,
+ String queueUrlString,
+ int queueUrlTtlInMinutes,
+ String eventTargetUrl,
+ String queueItToken) {
+ handleAppEnqueueResponse(queueId, queueUrlString, queueUrlTtlInMinutes, eventTargetUrl, queueItToken);
+ _requestInProgress.set(false);
+ }
+
+ @Override
+ public void onFailure(String errorMessage, int errorCode) {
+ Log.v("WaitingRoomProvider", String.format("Error: %s: %s", errorCode, errorMessage));
+ if (errorCode >= 400 && errorCode < 500) {
+ _queueITWaitingRoomProviderListener.onFailure(String.format("Error %s (%s)",errorMessage , errorCode), Error.INVALID_RESPONSE);
+ _requestInProgress.set(false);
+ } else {
+ QueueITWaitingRoomProvider.this.enqueueRetryMonitor(enqueueToken, enqueueKey);
+ }
+ }
+ };
+
+ QueueITApiClient queueITApiClient = new QueueITApiClient(_customerId, _eventOrAliasId, userId, userAgent, sdkVersion,
+ _layoutName, _language, enqueueToken, enqueueKey, queueITApiClientListener);
+ queueITApiClient.init(_context);
+ }
+
+ private void enqueueRetryMonitor(String enqueueToken, String enqueueKey) {
+ if (_deltaSec < MAX_RETRY_SEC) {
+ Handler handler = new Handler();
+ Runnable r = new Runnable() {
+ public void run() {
+ tryEnqueue(enqueueToken, enqueueKey);
+ }
+ };
+ handler.postDelayed(r, _deltaSec * 1000);
+
+ _deltaSec = _deltaSec * 2;
+ } else {
+ _deltaSec = INITIAL_WAIT_RETRY_SEC;
+ _requestInProgress.set(false);
+ _queueITWaitingRoomProviderListener.onFailure("Error! Queue is unavailable.", Error.Queueit_NotAvailable);
+ }
+ }
+
+ public boolean IsRequestInProgress() {
+ return _requestInProgress.get();
+ }
+
+ private void handleAppEnqueueResponse(String queueId, String queueUrlString, int queueUrlTtlInMinutes,
+ String eventTargetUrl, String queueItToken) {
+ Boolean isPassedThrough = queueItToken != null && !queueItToken.isEmpty();
+ RedirectType redirectType = getRedirectTypeFromToken(queueItToken);
+ QueueTryPassResult queueTryPassResult = new QueueTryPassResult(queueItToken, queueUrlString, eventTargetUrl, queueUrlTtlInMinutes, isPassedThrough, redirectType);
+ _queueITWaitingRoomProviderListener.onSuccess(queueTryPassResult);
+ _requestInProgress.set(false);
+ }
+
+ private class EnqueueRunner implements Runnable {
+ private final String _enqueueKey;
+ private final String _enqueueToken;
+
+ public EnqueueRunner(String enqueueToken, String enqueueKey) {
+ _enqueueToken = enqueueToken;
+ _enqueueKey = enqueueKey;
+ }
+
+ public EnqueueRunner() {
+ this(null, null);
+ }
+
+ @Override
+ public void run() {
+ if (isOnline()) {
+ runWithConnection(_enqueueToken, _enqueueKey);
+ return;
+ }
+ _isOnlineRetry++;
+ if (_isOnlineRetry > 5) {
+ _queueITWaitingRoomProviderListener.onFailure( "No connection", Error.NO_CONNECTION);
+ _requestInProgress.set(false);
+ return;
+ }
+
+ _checkConnectionHandler.postDelayed(this, 1000);
+ }
+ }
+
+ private boolean isOnline() {
+ ConnectivityManager cm = (ConnectivityManager) _context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm == null) {
+ return true;
+ }
+ NetworkInfo netInfo = cm.getActiveNetworkInfo();
+ return netInfo != null && netInfo.isConnected();
+ }
+
+ private String getUserId() {
+ return Settings.Secure.getString(_context.getContentResolver(), Settings.Secure.ANDROID_ID);
+ }
+
+ private static RedirectType getRedirectTypeFromToken(String queueItToken){
+ if(queueItToken == null || queueItToken.isEmpty()){
+ return RedirectType.queue;
+ }
+
+ Matcher matcher = pattern.matcher(queueItToken);
+
+ if (matcher.find()) {
+ String matchGroups = matcher.group(0);
+ String statusString = matchGroups.length() > 0 ? matchGroups.replace("~rt_", "").replace("~", "") : "";
+ return RedirectType.valueOf(statusString);
+ }else {
+ Log.e("QueueEngine", String.format("Waiting room status not found in the token: %s", queueItToken));
+ return RedirectType.unknown;
+ }
+ }
+
+ public String getSdkVersion() {
+ return "Android-" + BuildConfig.VERSION_NAME;
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomProviderListener.java b/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomProviderListener.java
new file mode 100644
index 0000000..8cc62be
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomProviderListener.java
@@ -0,0 +1,6 @@
+package com.queue_it.androidsdk;
+
+public interface QueueITWaitingRoomProviderListener {
+ void onSuccess(QueueTryPassResult queueTryPassResult);
+ void onFailure(String errorMessage, Error errorCode);
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomView.java b/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomView.java
new file mode 100644
index 0000000..fd7f249
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueITWaitingRoomView.java
@@ -0,0 +1,160 @@
+package com.queue_it.androidsdk;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Log;
+
+public class QueueITWaitingRoomView {
+ private final IWaitingRoomStateBroadcaster _stateBroadcaster;
+ private final QueueListener _queueListener;
+ private final QueueItEngineOptions _options;
+ private Context _context;
+
+ private int _delayInterval = 0;
+
+ public QueueITWaitingRoomView(Context activityContext,
+ QueueListener queueListener) {
+ this(activityContext,
+ queueListener,
+ QueueItEngineOptions.getDefault());
+ }
+
+ public QueueITWaitingRoomView(Context activityContext,
+ QueueListener queueListener,
+ QueueItEngineOptions options) {
+ if (options == null) {
+ options = QueueItEngineOptions.getDefault();
+ }
+ UserAgentManager.initialize(activityContext);
+ _context = activityContext;
+ _queueListener = queueListener;
+ _stateBroadcaster = new WaitingRoomStateBroadcaster(_context);
+ _options = options;
+ }
+
+ public void showQueue(final QueueTryPassResult queueTryPassResult, String webViewUserAgent) {
+ if(queueTryPassResult == null){
+ Log.e("QueueITWaitingRoomView", "queuePassedInfo parameter is empty");
+ return;
+ }
+ raiseQueueViewWillOpen();
+
+ Handler handler = new Handler();
+ Runnable r = new Runnable() {
+ public void run() {
+ showQueuePage(queueTryPassResult.getQueueUrl(), queueTryPassResult.getTargetUrl(), webViewUserAgent);
+ }
+ };
+ handler.postDelayed(r, _delayInterval);
+ }
+
+
+ public void setViewDelay(int delayInterval) {
+ _delayInterval = delayInterval;
+ }
+
+
+ private void showQueuePage(String queueUrl, final String targetUrl, String webViewUserAgent) {
+ _stateBroadcaster.registerReceivers(_queuePassedBroadcastReceiver,
+ _queueUrlChangedBroadcastReceiver,
+ _queueActivityClosedBroadcastReceiver,
+ _queueUserExitedBroadcastReceiver,
+ _queueErrorBroadcastReceiver,
+ _webViewClosedBroadcastReceiver,
+ _webViewOnSessionRestartReceiver);
+
+ Intent intent = new Intent(_context, QueueActivity.class);
+ intent.putExtra("queueUrl", queueUrl);
+ intent.putExtra("targetUrl", targetUrl);
+ intent.putExtra("webViewUserAgent", webViewUserAgent);
+ intent.putExtra("userId", getUserId());
+ intent.putExtra("options", _options);
+ _context.startActivity(intent);
+ }
+
+ private void raiseQueueViewWillOpen() {
+ _queueListener.onQueueViewWillOpen();
+ }
+
+ private void raiseUserExited() {
+ _queueListener.onUserExited();
+ }
+
+ private void raiseQueuePassed(String queueItToken) {
+ _queueListener.onQueuePassed(new QueuePassedInfo(queueItToken));
+ }
+
+ private void raiseWebViewClosed() {
+ _queueListener.onWebViewClosed();
+ }
+
+ private void raiseOnSessionRestart() {
+ _queueListener.onSessionRestart(null);
+ }
+
+ private void raiseQueueUrlChanged(String url) {
+ _queueListener.onQueueUrlChanged(url);
+ }
+
+
+ private final BroadcastReceiver _queuePassedBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ raiseQueuePassed(intent.getStringExtra("queue-it-token"));
+ }
+ };
+
+ private final BroadcastReceiver _queueErrorBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ _queueListener.onError(Error.SSL_ERROR, intent.getStringExtra("error-message"));
+ }
+ };
+
+ private final BroadcastReceiver _queueUrlChangedBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String url = intent.getExtras().getString("url");
+ raiseQueueUrlChanged(url);
+ }
+ };
+
+ private final BroadcastReceiver _queueUserExitedBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ raiseUserExited();
+ }
+ };
+
+ private final BroadcastReceiver _webViewClosedBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ raiseWebViewClosed();
+ }
+ };
+
+ private final BroadcastReceiver _webViewOnSessionRestartReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ raiseOnSessionRestart();
+ }
+ };
+
+ private final BroadcastReceiver _queueActivityClosedBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ _stateBroadcaster.unregisterReceivers(_queuePassedBroadcastReceiver, _queueUrlChangedBroadcastReceiver,
+ _queueActivityClosedBroadcastReceiver, _queueUserExitedBroadcastReceiver,
+ _queueErrorBroadcastReceiver, _webViewClosedBroadcastReceiver, _webViewOnSessionRestartReceiver);
+ }
+ };
+
+
+ private String getUserId() {
+ return Settings.Secure.getString(_context.getContentResolver(), Settings.Secure.ANDROID_ID);
+ }
+
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueItEngineOptions.java b/library/src/main/java/com/queue_it/androidsdk/QueueItEngineOptions.java
new file mode 100644
index 0000000..dc2c5ce
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueItEngineOptions.java
@@ -0,0 +1,73 @@
+package com.queue_it.androidsdk;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class QueueItEngineOptions implements Parcelable {
+ private boolean disableBackButtonFromWR;
+ private String webViewUserAgent;
+
+ public QueueItEngineOptions() {
+ }
+
+ public QueueItEngineOptions(boolean disableBackButtonFromWR) {
+ this.disableBackButtonFromWR = disableBackButtonFromWR;
+ this.webViewUserAgent = "";
+ }
+ public QueueItEngineOptions(String webViewUserAgent) {
+ this.disableBackButtonFromWR = true;
+ this.webViewUserAgent = webViewUserAgent;
+ }
+ public QueueItEngineOptions(boolean disableBackButtonFromWR, String webViewUserAgent) {
+ this.disableBackButtonFromWR = disableBackButtonFromWR;
+ this.webViewUserAgent = webViewUserAgent;
+ }
+
+ protected QueueItEngineOptions(Parcel in) {
+ disableBackButtonFromWR = in.readInt() != 0;
+ webViewUserAgent = in.readString();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public QueueItEngineOptions createFromParcel(Parcel in) {
+ return new QueueItEngineOptions(in);
+ }
+
+ @Override
+ public QueueItEngineOptions[] newArray(int size) {
+ return new QueueItEngineOptions[size];
+ }
+ };
+
+ public boolean isBackButtonDisabledFromWR() {
+ return disableBackButtonFromWR;
+ }
+
+ public void setBackButtonDisabledFromWR(boolean disableBackButtonFromWR) {
+ this.disableBackButtonFromWR = disableBackButtonFromWR;
+ }
+
+ public String getWebViewUserAgent() {
+ return webViewUserAgent;
+ }
+
+ public void setWebViewUserAgent(String webViewUserAgent) {
+ this.webViewUserAgent = webViewUserAgent;
+ }
+
+ public static QueueItEngineOptions getDefault() {
+ return new QueueItEngineOptions(true, "");
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(this.disableBackButtonFromWR ? 1 : 0);
+ dest.writeString(this.webViewUserAgent);
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueTryPassResult.java b/library/src/main/java/com/queue_it/androidsdk/QueueTryPassResult.java
new file mode 100644
index 0000000..ad2ebd8
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueTryPassResult.java
@@ -0,0 +1,45 @@
+package com.queue_it.androidsdk;
+
+public class QueueTryPassResult {
+ private final String _queueItToken;
+
+ private final boolean _isPassedThrough;
+ private final String _queueUrl;
+ private final String _targetUrl;
+ private final int _urlTTLInMinutes;
+ private final RedirectType _RedirectType;
+
+ public QueueTryPassResult(String queueItToken, String queueUrl, String targetUrl, int urlTTLInMinutes, boolean isPassedThrough, RedirectType redirectType) {
+ _queueItToken = queueItToken;
+ _queueUrl = queueUrl;
+ _targetUrl = targetUrl;
+ _urlTTLInMinutes = urlTTLInMinutes;
+ _isPassedThrough = isPassedThrough;
+ _RedirectType = redirectType;
+ }
+
+ public String getQueueItToken()
+ {
+ return _queueItToken;
+ }
+
+ public String getQueueUrl() {
+ return _queueUrl;
+ }
+
+ public String getTargetUrl() {
+ return _targetUrl;
+ }
+
+ public int getUrlTTLInMinutes() {
+ return _urlTTLInMinutes;
+ }
+
+ public Boolean isPassedThrough(){
+ return _isPassedThrough;
+ }
+
+ public RedirectType getRedirectType() {
+ return _RedirectType;
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/QueueUrlHelper.java b/library/src/main/java/com/queue_it/androidsdk/QueueUrlHelper.java
new file mode 100644
index 0000000..2891ca6
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/QueueUrlHelper.java
@@ -0,0 +1,53 @@
+package com.queue_it.androidsdk;
+
+import android.net.Uri;
+
+import okhttp3.HttpUrl;
+
+public abstract class QueueUrlHelper {
+
+ public static Uri updateUrl(String queueUrl, String userId) {
+ try {
+ return updateUrl(Uri.parse(queueUrl), userId);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return Uri.parse(queueUrl);
+ }
+ }
+
+ public static Uri updateUrl(Uri queueUrl, String userId) {
+ String encodedQuery = queueUrl.getEncodedQuery();
+ if(encodedQuery==null){
+ encodedQuery = "";
+ }
+ if(!encodedQuery.contains("userId=")){
+ encodedQuery = "userId=" + userId + "&" + encodedQuery;
+ }
+ String updatedUrl = new HttpUrl.Builder()
+ .scheme(queueUrl.getScheme())
+ .host(queueUrl.getHost())
+ .encodedPath(queueUrl.getPath())
+ .encodedQuery(encodedQuery)
+ .build()
+ .url()
+ .toString();
+ return Uri.parse(updatedUrl);
+ }
+
+ public static boolean urlUpdateNeeded(String queueUrl, String userId) {
+ if(queueUrl==null || userId==null){
+ return false;
+ }
+ Uri uri = Uri.parse(queueUrl);
+ return urlUpdateNeeded(uri, userId);
+ }
+
+ public static boolean urlUpdateNeeded(Uri queueUrl, String userId) {
+ if (queueUrl == null) return false;
+ String query = queueUrl.getQuery();
+ if (query == null) query = "";
+ String userIdQuery = String.format("userId=%s", userId);
+ boolean containsUserId = query.startsWith(userIdQuery) || query.contains("&" + userIdQuery);
+ return !containsUserId;
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/RedirectType.java b/library/src/main/java/com/queue_it/androidsdk/RedirectType.java
new file mode 100644
index 0000000..7143368
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/RedirectType.java
@@ -0,0 +1,11 @@
+package com.queue_it.androidsdk;
+
+public enum RedirectType {
+ unknown,
+ queue,
+ safetynet,
+ afterevent,
+ disabled,
+ directLink,
+ idle
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/UriOverrideWrapper.java b/library/src/main/java/com/queue_it/androidsdk/UriOverrideWrapper.java
new file mode 100644
index 0000000..55b9a3c
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/UriOverrideWrapper.java
@@ -0,0 +1,8 @@
+package com.queue_it.androidsdk;
+
+public abstract class UriOverrideWrapper{
+ protected abstract void onQueueUrlChange(String uri);
+ protected abstract void onPassed(String queueItToken);
+ protected abstract void onCloseClicked();
+ protected abstract void onSessionRestart();
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/UriOverrider.java b/library/src/main/java/com/queue_it/androidsdk/UriOverrider.java
new file mode 100644
index 0000000..b355d28
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/UriOverrider.java
@@ -0,0 +1,131 @@
+package com.queue_it.androidsdk;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.WebView;
+
+public class UriOverrider implements IUriOverrider {
+ private Uri queue;
+ private Uri target;
+ private String userId;
+
+ @Override
+ public Uri getQueue() {
+ return queue;
+ }
+
+ @Override
+ public void setQueue(Uri queue) {
+ this.queue = queue;
+ }
+
+ @Override
+ public Uri getTarget() {
+ return target;
+ }
+
+ @Override
+ public void setTarget(Uri target) {
+ this.target = target;
+ }
+
+ @Override
+ public String getUserId() {
+ return userId;
+ }
+
+ @Override
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ private boolean isBlockedUri(Uri uri) {
+ String path = uri.getPath();
+ return path.startsWith("/what-is-this.html");
+ }
+
+ private boolean isTargetUri(Uri destinationUri) {
+ String destinationHost = destinationUri.getHost();
+ String destinationPath = destinationUri.getPath();
+
+ String targetHost = target.getHost();
+ String targetPath = target.getPath();
+
+ return destinationHost.equalsIgnoreCase(targetHost)
+ && destinationPath.equals(targetPath);
+ }
+
+ private boolean isQueueItUri(Uri uri) {
+ return uri.getScheme().equals("queueit");
+ }
+
+ private boolean isCloseLink(Uri uri) {
+ if (!isQueueItUri(uri)) {
+ return false;
+ }
+ return uri.getHost().equals("close");
+ }
+
+ private boolean isSessionRestartLink(Uri uri) {
+ if (!isQueueItUri(uri)) {
+ return false;
+ }
+ return uri.getHost().equals("restartSession");
+ }
+
+ private boolean handleDeepLink(WebView webview, Uri destinationUri, UriOverrideWrapper uriOverride) {
+ if (isCloseLink(destinationUri)) {
+ uriOverride.onCloseClicked();
+ return true;
+ } else if (isSessionRestartLink(destinationUri)) {
+ uriOverride.onSessionRestart();
+ return true;
+ }
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, destinationUri);
+ webview.getContext().startActivity(browserIntent);
+ return true;
+ }
+
+ @Override
+ public boolean handleNavigationRequest(final String destinationUrlStr, WebView webview, UriOverrideWrapper uriOverride) {
+ Log.v("QueueITEngine", "URI loading: " + destinationUrlStr);
+ Uri destinationUri = Uri.parse(destinationUrlStr);
+ boolean isWeb = destinationUri.getScheme() != null && (destinationUri.getScheme().equals("http")
+ || destinationUri.getScheme().equals("https"));
+ if (!isWeb) {
+ return handleDeepLink(webview, destinationUri, uriOverride);
+ }
+ if (isBlockedUri(destinationUri)) {
+ return true;
+ }
+
+ String navigationHost = destinationUri.getHost();
+ String queueHost = queue.getHost();
+ boolean isQueueItUrl = navigationHost != null && queueHost != null && queueHost.equals(navigationHost);
+ if (isQueueItUrl) {
+ boolean needsRewrite = QueueUrlHelper.urlUpdateNeeded(destinationUri, userId);
+ if (needsRewrite) {
+ destinationUri = QueueUrlHelper.updateUrl(destinationUri, userId);
+ Log.v("QueueITEngine", "URL intercepting: " + destinationUri);
+ }
+ uriOverride.onQueueUrlChange(destinationUri.toString());
+ if (needsRewrite) {
+ webview.loadUrl(destinationUri.toString());
+ return true;
+ }
+ }
+
+ if (isTargetUri(destinationUri)) {
+ String queueItToken = destinationUri.getQueryParameter("queueittoken");
+ uriOverride.onPassed(queueItToken);
+ return true;
+ }
+ if (!isQueueItUrl) {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, destinationUri);
+ webview.getContext().startActivity(browserIntent);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/library/src/main/java/com/queue_it/androidsdk/UserAgentManager.java b/library/src/main/java/com/queue_it/androidsdk/UserAgentManager.java
new file mode 100644
index 0000000..1371f47
--- /dev/null
+++ b/library/src/main/java/com/queue_it/androidsdk/UserAgentManager.java
@@ -0,0 +1,20 @@
+package com.queue_it.androidsdk;
+
+import android.content.Context;
+import android.webkit.WebView;
+
+public class UserAgentManager {
+ public static final String SDKVersion = com.queue_it.androidsdk.BuildConfig.LIBRARY_PACKAGE_NAME + "@" + BuildConfig.VERSION_NAME;
+ private static String DeviceUserAgent;
+
+ private UserAgentManager(){
+ }
+
+ public static void initialize(Context context) {
+ DeviceUserAgent = new WebView(context).getSettings().getUserAgentString();
+ }
+
+ public static String getUserAgent() {
+ return DeviceUserAgent + " (sdk: " + SDKVersion + ")";
+ }
+}
diff --git a/library/src/test/java/com/queue_it/androidsdk/UriOverriderTest.java b/library/src/test/java/com/queue_it/androidsdk/UriOverriderTest.java
new file mode 100644
index 0000000..b9420a3
--- /dev/null
+++ b/library/src/test/java/com/queue_it/androidsdk/UriOverriderTest.java
@@ -0,0 +1,494 @@
+package com.queue_it.androidsdk;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.webkit.WebView;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+
+@RunWith(RobolectricTestRunner.class)
+public class UriOverriderTest {
+
+ @Test
+ public void givenUserNavigatesToBackLink_ThenBackCallbackShouldBeCalled() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://vavatest.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com"));
+ WebView webView = mock(WebView.class);
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+ final AtomicBoolean closeHandled = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest("queueit://close", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+ closeHandled.set(true);
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(queuePassed.get());
+ assertTrue(closeHandled.get());
+ }
+
+ @Test
+ public void givenUserNavigatesToSessionRestartLink_ThenSessionRestartCallbackShouldBeCalled() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://vavatest.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com"));
+ WebView webView = mock(WebView.class);
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+ final AtomicBoolean sessionRestarted = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest("queueit://restartSession", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+ sessionRestarted.set(true);
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(queuePassed.get());
+ assertTrue(sessionRestarted.get());
+ }
+
+ @Test
+ public void givenUserIsNavigatingToBlockedPage_ThenLoadShouldBeCancelled() {
+ String destinationUrl = "https://queue-it.com/what-is-this.html?customerId=vavatest&eventId=testendedroom&queueId=00000000-0000-0000-0000-000000000000&language=en-US";
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://vavatest.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com"));
+ WebView webView = mock(WebView.class);
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest(destinationUrl, webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(queuePassed.get());
+ }
+
+ @Test
+ public void givenuserIsNavigatingToUrlOnTargetDomainButNotTargetUrl_ThenQueueSHouldNotBePassedAndWebBrowserShouldOpen() {
+ String destinationUrl = "https://queue-it.com/what-is-this.html?customerId=vavatest&eventId=testendedroom&queueId=00000000-0000-0000-0000-000000000000&language=en-US";
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://vavatest.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com/q=iamthetarget"));
+ WebView webView = mock(WebView.class);
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+
+ boolean loadCancelled = testObj.handleNavigationRequest(destinationUrl, webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(queuePassed.get());
+ }
+
+ @Test
+ public void givenUserIsRedirectedToTargetLoadShouldBeCancelled() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://useraccount.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com"));
+ WebView webView = getMockedWebview();
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest("https://google.com?queueittoken=a", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertTrue(queuePassed.get());
+ }
+
+ @Test
+ public void givenUserIsNavigatingToExternalPageThenLoadShouldBeCancelledAndIntentShouldBeStarted() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://useraccount.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com"));
+ WebView webView = getMockedWebview();
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+ ArgumentCaptor argument = ArgumentCaptor.forClass(Intent.class);
+ String otherPage = "https://bing.com";
+
+ boolean loadCancelled = testObj.handleNavigationRequest(otherPage, webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(queuePassed.get());
+
+ verify(webView.getContext()).startActivity(argument.capture());
+ }
+
+ @Test
+ public void givenAppUserIsRedirectedToDeepLinkThenLoadShouldBeCancelled() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("qapp://enqueue"));
+ testObj.setTarget(Uri.parse("myapp://page1"));
+ WebView webView = getMockedWebview();
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+
+ boolean loadCancelled = testObj.handleNavigationRequest("myapp://page1", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(queuePassed.get());
+ }
+
+ @Test
+ public void givenAppUserIsRedirectedToTargetThenLoadShouldBeCancelled() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("qapp://enqueue"));
+ testObj.setTarget(Uri.parse("https://mypage.com"));
+ WebView webView = mock(WebView.class);
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest("https://mypage.com?queueittoken=1", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertTrue(queuePassed.get());
+ }
+
+ private WebView getMockedWebview() {
+ WebView webView = mock(WebView.class);
+ final Context mockedContext = mock(Context.class);
+ when(webView.getContext()).thenAnswer(new Answer() {
+ @Override
+ public Context answer(InvocationOnMock invocation) {
+ return mockedContext;
+ }
+ });
+ return webView;
+ }
+
+ @Test
+ public void givenUserIsRedirectedToDeepLinkThenLoadShouldBeCancelled() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://useraccount.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("myapp://page1"));
+ WebView webView = getMockedWebview();
+
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest("myapp://page1", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ System.out.print(uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ queuePassed.set(true);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(queuePassed.get());
+ }
+
+ @Test
+ public void givenUserIsNavigatingToExternalDeepUrlThenLoadShouldBeCancelledAndIntentShouldBeStarted() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://useraccount.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com"));
+ testObj.setUserId("myuser1");
+ WebView webView = getMockedWebview();
+ ArgumentCaptor argument = ArgumentCaptor.forClass(Intent.class);
+
+ final AtomicBoolean urlChangeHappened = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest("myapp://go?tab=activity1", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ urlChangeHappened.set(true);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ System.out.print(queueItToken);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertFalse(urlChangeHappened.get());
+
+ verify(webView.getContext()).startActivity(argument.capture());
+ }
+
+ @Test
+ public void givenUserIsRedirectedToLeaveLineThenLoadShouldBeCancelled() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://useraccount.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("https://google.com"));
+ testObj.setUserId("myuser1");
+ WebView webView = mock(WebView.class);
+
+ final AtomicBoolean urlChangeHappened = new AtomicBoolean(false);
+ boolean loadCancelled = testObj.handleNavigationRequest("https://useraccount.queue-it.net/app/leaveLine", webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ urlChangeHappened.set(true);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ System.out.print(queueItToken);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ assertTrue(loadCancelled);
+ assertTrue(urlChangeHappened.get());
+ }
+
+ @Test
+ public void givenUserNavigatesToExitLineThenLoadShouldCancel_AndQueueShouldNotBePassed() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://customer.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("http://localhost:3000"));
+ testObj.setUserId("myuser1");
+ WebView webView = mock(WebView.class);
+
+ final AtomicBoolean urlChangeHappened = new AtomicBoolean(false);
+ final AtomicBoolean queuePassed = new AtomicBoolean(false);
+
+ boolean loadCancelled = testObj.handleNavigationRequest("http://customer.queue-it.net/exitline.aspx?c=customer&e=otherroom2&q=qid&cid=en-US&l=myLayout&sdkv=Android-2.0.22",
+ webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ urlChangeHappened.set(true);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ System.out.print(queueItToken);
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+ assertTrue(loadCancelled);
+ assertTrue(urlChangeHappened.get());
+ assertFalse(queuePassed.get());
+ }
+
+
+ @Test
+ public void givenUserIsNavigatingToQueueItPageUrlShouldIncludeUserId() {
+ UriOverrider testObj = new UriOverrider();
+ testObj.setQueue(Uri.parse("https://customer.queue-it.net/app/enqueue"));
+ testObj.setTarget(Uri.parse("http://localhost:3000"));
+ testObj.setUserId("myuser1");
+ WebView webView = mock(WebView.class);
+
+ ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
+ final String expectedRewrittenUrl = "http://customer.queue-it.net/exitline.aspx?userId=myuser1&c=customer&e=otherroom2&q=qid&cid=en-US&l=myLayout&sdkv=Android-2.0.22";
+
+ boolean loadCancelled = testObj.handleNavigationRequest("http://customer.queue-it.net/exitline.aspx?c=customer&e=otherroom2&q=qid&cid=en-US&l=myLayout&sdkv=Android-2.0.22",
+ webView, new UriOverrideWrapper() {
+ @Override
+ protected void onQueueUrlChange(String uri) {
+ assertEquals(expectedRewrittenUrl, uri);
+ }
+
+ @Override
+ protected void onPassed(String queueItToken) {
+ }
+
+ @Override
+ protected void onCloseClicked() {
+
+ }
+
+ @Override
+ protected void onSessionRestart() {
+
+ }
+ });
+
+ verify(webView).loadUrl(argument.capture());
+ assertEquals(expectedRewrittenUrl, argument.getValue());
+ }
+}
\ No newline at end of file
diff --git a/library/src/test/resources/robolectric.properties b/library/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..d27d2bd
--- /dev/null
+++ b/library/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=29
\ No newline at end of file
diff --git a/publish.gradle b/publish.gradle
new file mode 100644
index 0000000..76b2f31
--- /dev/null
+++ b/publish.gradle
@@ -0,0 +1,110 @@
+apply plugin: 'maven-publish'
+apply plugin: 'signing'
+apply plugin: "de.marcphilipp.nexus-publish"
+
+group = groupId
+version = libraryVersion
+
+def pomConfig = {
+ description libraryDescription
+ licenses {
+ license {
+ name licenseName
+ url licenseUrl
+ }
+ }
+ scm {
+ connection gitUrl
+ developerConnection gitUrl
+ url siteUrl
+ }
+ developers {
+ developer {
+ name "Developers"
+ email "devs@queue-it.com"
+ organization organization
+ organizationUrl organizationUrl
+ }
+ }
+}
+
+afterEvaluate {
+ configurations.all {
+ if (it.name =~ /DebugAll(Api|Runtime)Publication$/) {
+ components.all.withVariantsFromConfiguration(it) {
+ skip()
+ }
+ }
+ }
+
+ publishing.publications {
+ android.libraryVariants.all { variant ->
+ if (variant.buildType.name == "debug") return // Prevents publishing debug library
+
+ def flavored = !variant.flavorName.isEmpty()
+
+ /**
+ * Translates "_" in flavor names to "-" for artifactIds, because "-" in flavor name is an
+ * illegal character, but is well used in artifactId names.
+ */
+ def variantArtifactId = flavored ? variant.flavorName.replace('_', '-') : project.name
+ def publicationName = "android-webui-sdk-${variant.name.capitalize()}"
+
+ def sourceDirs = variant.sourceSets.collect {
+ it.javaDirectories // Also includes kotlin sources if any.
+ }
+ def sourcesJar = task("${variant.name}SourcesJar", type: Jar) {
+ description "Puts sources for ${variant.name} in a jar."
+ from sourceDirs
+ classifier = 'sources'
+ }
+
+ "$publicationName"(MavenPublication) {
+ from components."${variant.flavorName}Release"
+ artifactId variantArtifactId
+ group groupId
+ version libraryVersion
+ artifact sourcesJar
+
+ pom {
+ withXml {
+ def root = asNode()
+ root.appendNode("name", "${libraryName}:${variantArtifactId}")
+ root.appendNode("url", siteUrl)
+ root.appendNode("description", libraryDescription)
+ root.children().last() + pomConfig
+ root.dependencies.removeAll { dep ->
+ dep.scope == "test"
+ }
+ // Create a signed pom if we need it
+ def pomFile = file("${project.buildDir}/generated-${variant.flavorName}-pom.xml")
+ writeTo(pomFile)
+ signing.sign(pomFile)
+ }
+ }
+ }
+ }
+ }
+}
+
+Properties localProps = new Properties()
+if (rootProject.file("local.properties").exists()) {
+ localProps.load(rootProject.file("local.properties").newDataInputStream())
+}
+
+//Sign all of our publications with our PGP private key
+signing {
+ useInMemoryPgpKeys(PGP_KEY, PGP_PASSWORD)
+ publishing.publications.all { publication ->
+ sign(publication)
+ }
+}
+
+nexusPublishing {
+ repositories {
+ sonatype {
+ username.set(OSSRH_USERNAME)
+ password.set(OSSRH_PASSWORD)
+ }
+ }
+}
\ No newline at end of file