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 BackendFor 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 GalleryScanning 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/VideosThe 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 ItemsBy 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 ofUPLOADED
,NOT_UPLOADED
,NOT_SET
byMediaType
- can be one ofPHOTOS
,VIDEOS
,NOT_SET
byOther
- can be one ofFAVORITES
orNOT_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 SeparatorsPaging 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 / ThumbnailThe 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 uselocalUri
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 ObservablePhoto 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 IdTo 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 IdsTo 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.
#
FacesFor 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 SummaryThe 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 summaryWhen 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 summaryFor 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.