Skip to main content

Getting Timeline

The Photo SDK provides functions for implementing a timeline. A timeline is a chronological photo feed. To implement a timeline, the first step is to get data from the backend.

Getting Photos From The Backend#

For getting photos from backend, use the following code example.

important

Since the download procedure takes a significant amount of time (and, on the other hand, does not require permissions from the user), it should be started as soon as the user is logged into the application.

var disposable: Disposable? = null
// If true, then a full reading of the list of photos and videos from the backend is // performed. This can take a long time. If false, then reading is performed incrementally, // only the last changes are read.var fullRefresh: Boolean = true
// ...
// Returns Completable, which completes when the operation ends.// You have to subscribe to the returned Completable to receive updates.disposable = PhotoManager.timeline.reloadBackend(fullRefresh)        .observeOn(AndroidSchedulers.mainThread())        .subscribeBy(                onComplete = {                    // Handle it                },                onError = { // it: Throwable                    // Handle it                }        )
// ...
// You must call dispose() on disposable that returned by the subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Scan Local Gallery#

Scanning the local gallery allows you to add local photos to the timeline.

To start scanning the local gallery, use the following code example.

important

Since reading the local gallery is possible only with the READ_EXTERNAL_STORAGE permission, this call should be performed only after user granted the permission. If this method is called without getting permission, the STORAGE_PERMISSION_NOT_GRANTED flag will appear in the auto-upload status (see Auto Upload Status). You can double-check the permissions and set/clear this flag using the method PhotoManager.timeline.recheckPermissions()

important

Starting with android version 10 (API level 29), in order to get the location from media files, you need to add ACCESS_MEDIA_LOCATION permission in Manifest file and request it at runtime. More info: https://developer.android.com/training/data-storage/shared/media#media-location-permission

important

Starting with Android 14 (API level 34), the user can grant partial access to files with READ_MEDIA_VISUAL_USER_SELECTED permission. In this case, the scanner processes only the files selected by the user. More info: https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_VISUAL_USER_SELECTED

var disposable: Disposable? = null
// ...
// Returns Completable, which completes when the operation ends.// You have to subscribe to the returned Completable to receive updates.disposable = PhotoManager.timeline.reloadLocal()        .observeOn(AndroidSchedulers.mainThread())        .subscribeBy(                onComplete = {                    // Handle it                },                onError = { // it: Throwable                    // Handle it                }        )
// ...
// You must call dispose() on disposable that returned by the subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Getting A List Of Photos/Videos#

The Cloudike Photos SDK provides media item information to the client application as a PagingData of PhotoItem objects.

To access the list of media items, use the following code example.

important

The createPagingObservable() function creates new Observable that emits a PagingData of PhotoItems every time there is a change to the local database. To display the paginated list in the RecyclerView, you must use PagingDataAdapter from the Android Paging Library.

The Cloudike Photos SDK uses the following Android Paging Library.

// Pagingimplementation 'androidx.paging:paging-runtime-ktx:3.1.1'implementation 'androidx.paging:paging-rxjava2-ktx:3.1.1'
// Declare view modelprivate class TimelineViewModel(    private val timeline: Timeline) : ViewModel() {
    // NOTE: Your app can use either Observable or Flow. Both of them are created here for demo purposes only.
    val contentObservable: Observable<PagingData<PhotoItem>> by lazy {        timeline.createPagingObservable()            .cachedIn(viewModelScope)    }
    val contentFlow: Flow<PagingData<PhotoItem>> by lazy {        timeline.createPagingFlow()            .cachedIn(viewModelScope)    }}
// Factory is required to pass timeline to the view modelprivate class TimelineViewModelFactory(    private val timeline: Timeline) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {        return TimelineViewModel(timeline) as T    }}
// Fragmentclass TimelineFrg : Fragment() {
    private val adapter: TimelineAdapter // derived from PagingDataAdapter    private val timeline = PhotoManager.timeline    private val viewModel: TimelineViewModel by viewModels { TimelineViewModelFactory(timeline) }    private var disposable: Disposable? = null
    override fun onStart() {        super.onStart()        // You have to subscribe to the returned Observable to receive updates.        disposable = viewModel.contentObservable            .subscribe { pagingData ->                // Update adapter                adapter.submitData(this.lifecycle, pagingData)            }    }
    override fun onStop() {        super.onStop()        // You must call dispose() on disposable that returned by the subscribe() method,        // when it is no longer needed, for example in your fragment’s onStop() or onPause().        disposable?.dispose()    }}

As an alternative to Observable, Flow is also available for getting media items of the timeline:

    override fun onStart() {        super.onStart()        lifecycleScope.launch {            viewModel.contentFlow                .collectLatest {                    adapter.submitData(it)                }        }    }
important

The list contains only photos/videos that are either already uploaded to the cloud, or located in a folder that is enabled for startup (see Autoupload Folders). If an element is not uploaded to the cloud and at the same time is in a folder that is disabled, then such element will be excluded from the timeline list.

Getting A Filtered List Of Media Items#

By default, Timeline provides full list of media items. If you want to filter this list, it can be done by setting Filter property of Timeline. The following code example shows how to display uploaded photos only (excluding videos and not uploaded media):


// Set filter to the timelinePhotoManager.timeline.filter = TimelineFilter(    byUploadStatus = TimelineFilter.ByUploadStatus.UPLOADED,    byMediaType = TimelineFilter.ByMediaType.PHOTOS,    byOther = TimelineFilter.ByOther.NOT_SET)

// View modelprivate class TimelineViewModel(    private val timeline: Timeline) : ViewModel() {
    // NOTE: Your app can use either Observable or Flow. Both of them are created here for demo purposes only.
    val contentObservable: Observable<PagingData<PhotoItem>> by lazy {        timeline.createFilteredPagingObservable() // Note: *filtered* version of createPagingObservable is used            .cachedIn(viewModelScope)    }
    val contentFlow: Flow<PagingData<PhotoItem>> by lazy {        timeline.createFilteredPagingFlow() // Note: *filtered* version of createPagingFlow is used            .cachedIn(viewModelScope)    }}

Filter object has three properties:

  • byUploadStatus - can be one of UPLOADED, NOT_UPLOADED, NOT_SET
  • byMediaType - can be one of PHOTOS, VIDEOS, NOT_SET
  • byOther - can be one of FAVORITES or NOT_SET

The latter constants NOT_SET and it can be used to clean filter setting in one or both properties. For example, if you want to display all the media items which are not uploaded, set filter as follows:

PhotoManager.timeline.filter = TimelineFilter(    byUploadStatus = TimelineFilter.ByUploadStatus.NOT_UPLOADED,    byMediaType = TimelineFilter.ByMediaType.NOT_SET,    byOther = TimelineFilter.ByOther.NOT_SET)

Monthly Separators#

Paging library 3 provides capability of inserting headers (separators) on the fly by means of method PagingData.insertSeparators. See Add list separators.

Links: PagingData\<T>, PhotoItem.

Building a Preview / Thumbnail#

The app usually displays the timeline as a list (or gallery) of media files. To load and display preview, the application can use its own implementation or a third-party one, such as Glide. To access the content of a media file, you can use the values of the following attributes of PhotoItem (in order of decreasing priority):

  • localUri - Uri provided by MediaStore for accessing the content of a local photo / video file;
  • localPath - (Deprecated) The direct path to the file in the local storage. It is strongly recommended to use localUri instead of this attribute;
  • thumbSmallUrl - URL to small thumbnail created by the cloud storage;
  • thumbMiddleUrl - URL to medium thumbnail created by the cloud storage;
  • previewUrl - URL to preview image created by the cloud storage;
important

Note that app must have READ_EXTERNAL_STORAGE permission to access media file via localUri.

To access a media resource by its URL, you need to enter an authorization token in the request header. Here's an example using the Glide media library. In order for Glide to automatically add an authorization token in a media upload request, the first step is to add a header builder.

// The `glideHeaders` object is initialized once with additional auth headersvar glideHeaders: LazyHeaders? = null    get() {        if (field == null) {            field = LazyHeaders.Builder()                    .addHeader("Mountbit-Auth", Prefs.token!!)                    .addHeader("User-Agent", Utils.appUserAgent())                    .build()        }        return field    }
important

When your app is logging out, it must set glideHeaders to null in order to reinitialize it later with new auth token.

Then, we create the Glide uri loader.

internal class HeaderLoader(urlLoader: ModelLoader<GlideUrl?, InputStream?>?): BaseGlideUrlLoader<String>(urlLoader) {    override fun handles(model: String) = true    override fun getUrl(model: String?, width: Int, height: Int, options: Options?) = model    override fun getHeaders(model: String?, width: Int, height: Int, options: Options?): Headers = glideHeaders!!}

Create a HeaderLoaderFactory model load factory and create a HeaderLoader instance in it.

internal class HeaderLoaderFactory : ModelLoaderFactory<String, InputStream> {
    override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, InputStream> {        return HeaderLoader(multiFactory.build(GlideUrl::class.java, InputStream::class.java))    }    override fun teardown() {}}

The last step is to create the Glide module and connect our factory to it.

@GlideModuleclass GlideModuleWithHeaders : AppGlideModule() {
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {        registry.replace(String::class.java, InputStream::class.java, HeaderLoaderFactory())    }}

After going through all the steps, Glide will automatically substitute a header with an authorization token.

Links: PhotoItem.

Get Section Photo Observable#

Photo SDK Timeline does not provide any information about sections (monthly headers) because they can be inserted on the fly by the app when submitting PagingData to the adapter (see Monthly Separators). Instead, Timeline can provide a list of photos in range of timestamps. The following example shows how to get list of photos in the section if sections separated by month:


// header item inserted by the app and tapped by the user in timelineval headerPhotoItem: PhotoItem = // ...
val range = with (Calendar.getInstance()) {    timeInMillis = headerPhotoItem.createdAt    set(Calendar.DAY_OF_MONTH, 1)    set(Calendar.HOUR_OF_DAY, 0)    set(Calendar.MINUTE, 0)    set(Calendar.SECOND, 0)    set(Calendar.MILLISECOND, 0)    LongRange(timeInMillis, headerPhotoItem.createdAt)}

disposable = PhotoManager.timeline.createSectionPhotosObservable(range)    .subscribe { sectionItems: List<PhotoItem> ->        // proceed with section items    }
// ...
// Dispose subscription when it is no longer neededdisposable?.dispose()

Note in the example above headerPhotoItem must have timestamp set to the value greater than topmost photo of the section (and lower than last photo of section above). Timeline always show photos in descending order of their createdAt field, and section separators must obey this rule. The following scheme explains the point:

section A (sectionA.createdAt > photo1.createdAt)photo1  photo2  photo3photo4  photo5  photo6section B (photo6.createdAt > sectionB.createdAt > photo7.createdAt) photo7  photo8  photo9...

If you need section items when displaying filtered timeline (see Getting A Filtered List Of Media Items) you have to call createFilteredSectionPhotosObservable():

disposable = PhotoManager.timeline.createFilteredSectionPhotosObservable(range)    .subscribe { sectionItems: List<PhotoItem> ->        // proceed with section items    }

Links: PagingData\<T>, PhotoItem.

Get Photo By Backend Id#

To get photo(s) by the backend id(s), use the following code example.

var disposable: Disposable? = null
// Backend photo id.var id: String = null
// ...
// You have to subscribe to the returned Single to receive updates.disposable = PhotoManager.timeline.fetchAndGetPhotosObservableByPhotoBackendIds(listOf(id))        .observeOn(AndroidSchedulers.mainThread())        .subscribeBy(                onSuccess = {                    // Handle it                },                onError = { // it: Throwable                    // Handle it                }        )
// ...
// You must call dispose() on disposable that returned by the subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Links: PhotoItem.

Create Photos Observable By List Of Backend Ids#

To create photos observable by the list of backend ids, use the following code example.

var disposable: Disposable? = null
// Backend photo id list.var idList: List<String> = null
// ...
// You have to subscribe to the returned Single<Observable<List<PhotoItem>>> to receive updates.disposable = PhotoManager.timeline.createObservableForPhotosWithBackendIds(idList)        .observeOn(AndroidSchedulers.mainThread())        .subscribeBy(            onSuccess = { photoList ->                 // Handle it            },            onError = { // it: Throwable                // Handle it            }        )
// ...
// You must call dispose() on disposable that returned by the subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Links: PhotoItem.

Faces#

For getting faces photos from backend, use the following code example.

val operationFetchFace = PhotosphotoManager.timeline.fetchFacePhotos(faceId)

To get photos by face id, use the following code example. It returns a PagingData of PhotoItem as Flow that contain the face with the specified id.

// View modelprivate class TimelineViewModel( private val timeline: Timeline) : ViewModel() { // ...    val contentFlow: Flow<PagingData<PhotoItem>> by lazy {        timeline.createFacePhotosPagingFlow(faceId)            .cachedIn(viewModelScope)    }// ...}

Timeline Summary#

The summary function allows you to get a summary about storage for your personal cloud. The summary provides the following information as an object TimelineSummary:

  • number of photos
  • number of videos
  • number of items in the trash bin
  • last uploaded photo

Get up-to-date summary#

When the library is initialized, a summary is generated based on the last loaded data. In order to update the summary, use the following code example.

var disposable: Disposable? = null
// ...
// Returns completable, which completes when the data is updated from backend.// You have to subscribe to returned observable in order to receive updates.disposable = PhotoManager.timeline.reloadSummary()        .observeOn(AndroidSchedulers.mainThread())        .subscribeBy(            onComplete = {                // Handle it            },            onError = { // it: Throwable                // Handle it            }        )
// ...
// You must call dispose() on disposable that returned by subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Getting summary#

For an up-to-date summary, use the following code example.

var disposable: Disposable? = null
// ...
// Returns observable, which emits TimelineSummary object.// You have to subscribe to returned observable in order to receive updates.disposable = PhotoManager.timeline.createSummaryObservable() .observeOn(AndroidSchedulers.mainThread()) .subscribe { // it: TimelineSummary  // Handle it }
// ...
// You must call dispose() on disposable that returned by subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Links - TimelineSummary.