COMING SOON: Spring 2025 Release
By Shirley Gong | 2019 Apr 30
5 min
Tags
android
collaboration
In this tutorial, you will learn how to create a real-time document collaboration app using the Apryse 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 Apryse WebViewer server or your own server.
Users discussing the same document in real time via the Apryse DK and Firebase
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 Apryse 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 Apryse's customizable document annotation tools and extensible annotation functionality here.
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'
}
You will now add three Apryse 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 Apryse 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 Java 8
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
...
implementation "com.pdftron:pdftron:9.4.1"
implementation "com.pdftron:tools:9.4.1"
// Add PDFTron collaboration package
implementation "com.pdftron:collab:9.4.1"
}
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")
}
}
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. Apryse 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)
}
}
}
}
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"
}
}
}
}
You can now connect your server to the Apryse 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()
}
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 Apryse 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.
PRODUCTS
Enterprise
Small Business
Popular Content