Free Trial Support

Contents

Background and Further Resources
Step 1 - Add Firebase to Your Android Project
Step 2 - Add PDFTron Android SDK to your Android project
Step 3 - Implement Server Functionality
Authentication
Annotation Data Flow
Database Rules
Step 4 - Add the Viewer

In this tutorial, you will learn how to create a real-time document collaboration app using the PDFTron Android SDK and Firebase. This will let many users view the same PDF, Office or image file at the same time across their Android devices and communicate via comments, highlights, signatures and other annotations.

The full source code for this article can be found here. And the same steps will work similarly with any backend, whether that's the PDFTron WebViewer server or your own server.

Users discussing the same document in real time via the PDFTron SDK and Firebase

linkBackground and Further Resources

Firebase began in 2011 as a humble API allowing integration of online chat into a website. But Firebase founders James Tamplin and Andrew Lee soon discovered their solution could pass along a lot more than just chat messages.

Today, Firebase provides developers an API to store and sync almost any type of online data. And by taking care of backend functions like web hosting, authentication, usage tracking, and more, Firebase serves as one way to expedite development of scalable cloud-based apps. (Today, the company claims more than 1.5 million applications use Firebase products.)

There is great potential synergy between Firebase and the cross-platform PDFTron SDK. So we created a drop-in sample to show how the two could work together to enable real-time online collaboration and discussion. What follows are the sample instructions for your Android project. (The accompanying WebViewer documentation is available here in case you'd like to integrate a web application.) Once you've got your sample project up and running, there is a customization guide that will show you how to customize the open source UI. You can also see and learn more about PDFTron's customizable document annotation tools and extensible annotation functionality here.

linkStep 1 - Add Firebase to Your Android Project

Firebase has a very comprehensive tutorial on how to add Firebase to your Android project. Head here to get your project set up. Option 1 Add Firebase using the Firebase console is recommended. By the end of this step, your Firebase project console will recognize your Android app.

Next, let's add two additional packages: Firebase Authentication and Firebase Realtime Database. In your app module's build.gradle file (usually app/build.gradle), add the following:

dependencies {
    ...
    implementation 'com.google.firebase:firebase-core:16.0.8'
    // Add the authentication and realtime database dependencies
    implementation 'com.google.firebase:firebase-auth:16.2.0'
    implementation 'com.google.firebase:firebase-database:16.1.0'
}

linkStep 2 - Add PDFTron Android SDK to your Android project

You will now add three PDFTron Android packages to your app. For simplicity, we will use Gradle integration.

First, head over to our Gradle integration guide to see how to add the PDFTron Package and Tools Package to your Android project.

Then, in your app module's build.gradle file (usually app/build.gradle), add the following:

android {
    defaultConfig {
        minSdkVersion 21 // <== add this line
    }
    // Add Java 8
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    ...
    implementation "com.pdftron:pdftron:6.10.6"
    implementation "com.pdftron:tools:6.10.6"
    // Add PDFTron collaboration package
    implementation "com.pdftron:collab:6.10.6"
}

linkStep 3 - Implement Server Functionality

linkAuthentication

First, go to the Firebase console and click the "Authentication" button on the left panel and then click the "Sign-in Method" tab, just to the right of "Users". From this page click the "Anonymous" button and choose to enable anonymous login.

Then, create a Server.kt file in your project and add...

class Server(applicationContext: Context) : CustomService {

    private var mDatabase: CollabDatabase? = null

    init {
        mDatabase = CollabDatabase.getInstance(applicationContext)
    }

    private lateinit var mBroadcaster: FlowableEmitter<ServerEvent>
    private var mFlowableDisposable: Disposable? = null
    private val mFlowable = Flowable.create(
        FlowableOnSubscribe<ServerEvent> { emitter -> mBroadcaster = emitter },
        BackpressureStrategy.BUFFER
    )
    private var mDisposables: CompositeDisposable = CompositeDisposable()

    private var auth: FirebaseAuth = FirebaseAuth.getInstance()
    private var annotationsRef: DatabaseReference? = null
    private var authorsRef: DatabaseReference? = null

    fun signIn(): Flowable<ServerEvent> {
        mFlowableDisposable = mFlowable.subscribe()

        auth.signInAnonymously()
            .addOnCompleteListener {
                initDB()
                mBroadcaster.onNext(ServerEvent.SignIn(it)) // broadcast to UI to obtain user name
            }

        return mFlowable
    }

    private fun initDB() {
        val database = FirebaseDatabase.getInstance()
        annotationsRef = database.getReference("annotations")
        authorsRef = database.getReference("authors")
    }
}

linkAnnotation Data Flow

After successfully authenticating the user and having obtained a user name, let's add the user to the Firebase database and start subscribing to database changes.

fun updateAuthor(authorName: String) {
    if (auth.currentUser != null && authorsRef != null && annotationsRef != null) {
        val authorId = auth.currentUser!!.uid

        // add user and sample document
        mDisposables.add(addUserAndDocument(authorId, authorName).subscribeOn(Schedulers.io()).subscribe())

        authorsRef!!.child(authorId).child("authorName").setValue(authorName)

        // subscribe to authors
        authorsRef!!.addChildEventListener(authorChildEventListener)

        // subscribe to annotations
        annotationsRef!!.addListenerForSingleValueEvent(object: ValueEventListener {
            override fun onCancelled(p0: DatabaseError) {
            }

            override fun onDataChange(p0: DataSnapshot) {
                for (child in p0.children) {
                    val key = child.key
                    val newAnnot = child.getValue(Annotation::class.java)
                    mInitialAnnotMap[key!!] = convAnnotationToAnnotationEntity(key, newAnnot!!)
                }
                mDisposables.add(handleChildrenAdded(mInitialAnnotMap).subscribeOn(Schedulers.io()).subscribe())
                annotationsRef!!.addChildEventListener(annotChildEventListener)
            }
        })
    }
}

Next, let's add functionality to send client annotation changes to Firebase. PDFTron stores your annotations in XFDF -- the ISO Standard Format for annotations interchange -- which means your annotations can be preserved when sharing documents with people using other tools. Code is as follows:

private val OP_ADD = "add"
private val OP_MODIFY = "modify"
private val OP_REMOVE = "remove"

// CustomService start
override fun sendAnnotation(
    action: String?,
    annotations: ArrayList<AnnotationEntity>?,
    documentId: String?,
    userName: String?
) {
    if (Utils.isNullOrEmpty(action)) {
        return
    }
    for (entity in annotations!!) {
        val op = entity.at
        val annotId = entity.id
        val authorId = auth.currentUser!!.uid
        val xfdf = entity.xfdf
        when (op) {
            OP_ADD -> {
                val annotation = Annotation(
                    authorId,
                    null,
                    xfdf
                )
                createAnnotation(annotId, annotation)
            }
            OP_MODIFY -> {
                val annotation = Annotation(
                    authorId,
                    null,
                    xfdf
                )
                changeAnnotation(annotId, annotation)
            }
            OP_REMOVE -> {
                removeAnnotation(annotId)
            }
        }
    }
}

linkDatabase Rules

Lastly, you should add server-side permission rules for writing data. Although client-side permission checking is supported in the SDK, every user has default access to each annotation's information (including authorId and authorName). Thus, data-write permissions should be regulated in the server as well. Add the following Database Rules to your Firebase console via the "Database" button on the left panel, and then click the "Rules" tab.

{
  "rules": {
    ".read": "auth != null",

    "annotations": {
      "$annotationId": {
        ".write": "auth.uid === newData.child('authorId').val() || auth.uid === data.child('authorId').val() || auth.uid === newData.child('parentAuthorId').val() || auth.uid === data.child('parentAuthorId').val()"
      }
    },

    "authors": {
      "$authorId": {
        ".write": "auth.uid === $authorId"
      }
    }
  }
}
This is the same setup as PDFTron's WebViewer collaboration sample and Android client will work out-of-box with the Web client. If you are interested in a web client, click here for more details.

linkStep 4 - Add the Viewer

You can now connect your server to the PDFTron SDK's out-of-box collaboration UI, in your MainActivity.kt:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    server = Server(this.application)

    val documentViewModel = ViewModelProviders.of(this).get(DocumentViewModel::class.java)
    documentViewModel.setCustomConnection(server)

    setUpSampleView()
}
private fun setUpSampleView() {
    mPdfViewCtrlTabHostFragment = createPdfViewerFragment()

    mPdfViewCtrlTabHostFragment!!.addCollabHostListener(object :
        CollabPdfViewCtrlTabHostFragment.CollabTabHostListener {
        override fun onNavButtonPressed() {
            finish()
        }

        override fun onTabDocumentLoaded(p0: String?) {
            server.signIn().subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                    if (it is ServerEvent.SignIn) {
                        if (it.response.isSuccessful) {
                            Log.d(TAG, "signInAnonymously:success")
                            // obtain user name and call server.updateAuthor
                        }
                    }
                }
        }
    })

    val ft = supportFragmentManager.beginTransaction()
    ft.replace(R.id.fragment_container, mPdfViewCtrlTabHostFragment!!, null)
    ft.commit()
}

Wrapping Up

That's it! Your real-time document collaboration app is ready to run. Moving on, head over to our UI customization guide, we will walk you through how to customize the collaboration UI.

If you have any questions about using PDFTron SDK in your project, please feel to contact us and we will be happy to help!

The full source code for this article can be found here.