import angular from 'angular' ; import { Injectable } from 'angular-ts-decorators' ; import autobind from 'autobind-decorator' ; import { AppEventType } from '../../../app/app.enum' ; import { AlertService } from '../../../shared/alert/alert.service' ; import { BookmarkChangeType } from '../../../shared/bookmark/bookmark.enum' ; import { Bookmark , BookmarkMetadata , ModifyBookmarkChangeData , RemoveBookmarkChangeData } from '../../../shared/bookmark/bookmark.interface' ; import { BookmarkHelperService } from '../../../shared/bookmark/bookmark-helper/bookmark-helper.service' ; import { FailedDownloadFileError , FailedGetPageMetadataError , I18nError , SyncUncommittedError } from '../../../shared/errors/errors' ; import { ExceptionHandler } from '../../../shared/errors/errors.interface' ; import Globals from '../../../shared/global-shared.constants' ; import { PlatformType } from '../../../shared/global-shared.enum' ; import { I18nObject , PlatformService , WebpageMetadata } from '../../../shared/global-shared.interface' ; import { LogService } from '../../../shared/log/log.service' ; import { MetadataService } from '../../../shared/metadata/metadata.service' ; import { NetworkService } from '../../../shared/network/network.service' ; import { StoreService } from '../../../shared/store/store.service' ; import { SyncType } from '../../../shared/sync/sync.enum' ; import { Sync } from '../../../shared/sync/sync.interface' ; import { SyncService } from '../../../shared/sync/sync.service' ; import { UtilityService } from '../../../shared/utility/utility.service' ; import { WorkingContext } from '../../../shared/working/working.enum' ; import { WorkingService } from '../../../shared/working/working.service' ; @ autobind @ Injectable ( 'PlatformService' ) export class AndroidPlatformService implements PlatformService { Strings = require ( '../../../../../res/strings/en.json' ) ; $exceptionHandler : ExceptionHandler ; $http : ng . IHttpService ; $injector : ng . auto . IInjectorService ; $interval : ng . IIntervalService ; $q : ng . IQService ; $timeout : ng . ITimeoutService ; alertSvc : AlertService ; bookmarkHelperSvc : BookmarkHelperService ; logSvc : LogService ; metadataSvc : MetadataService ; networkSvc : NetworkService ; storeSvc : StoreService ; _syncSvc : SyncService ; utilitySvc : UtilityService ; workingSvc : WorkingService ; backgroundSyncInterval : ng . IPromise < void > ; _currentPage : BookmarkMetadata ; cancelGetPageMetadata : ( ) => any ; i18nObjects : I18nObject [ ] ; loadingId : string ; platformName = PlatformType . Android ; static $inject = [ '$exceptionHandler' , '$http' , '$injector' , '$interval' , '$q' , '$timeout' , 'AlertService' , 'BookmarkHelperService' , 'LogService' , 'MetadataService' , 'NetworkService' , 'StoreService' , 'UtilityService' , 'WorkingService' ] ; constructor ( $exceptionHandler , $http , $injector , $interval , $q , $timeout , AlertSvc , BookmarkHelperSvc , LogSvc , MetadataSvc , NetworkSvc , StoreSvc , UtilitySvc , WorkingSvc ) { this . $exceptionHandler = $exceptionHandler ; this . $http = $http ; this . $injector = $injector ; this . $interval = $interval ; this . $q = $q ; this . $timeout = $timeout ; this . alertSvc = AlertSvc ; this . bookmarkHelperSvc = BookmarkHelperSvc ; this . logSvc = LogSvc ; this . metadataSvc = MetadataSvc ; this . networkSvc = NetworkSvc ; this . storeSvc = StoreSvc ; this . utilitySvc = UtilitySvc ; this . workingSvc = WorkingSvc ; this . i18nObjects = [ ] ; } get currentPage ( ) : BookmarkMetadata { return this . _currentPage ; } set currentPage ( value ) { this . _currentPage = value ; } get syncSvc ( ) : SyncService { if ( angular . isUndefined ( this . _syncSvc ) ) { this . _syncSvc = this . $injector . get ( 'SyncService' ) ; } return this . _syncSvc ; } checkOptionalNativePermissions ( ) { return this . methodNotApplicable ( ) ; } disableBackgroundSync ( ) { if ( ! this . backgroundSyncInterval ) { return ; } this . $interval . cancel ( this . backgroundSyncInterval ) ; this . backgroundSyncInterval = null ; window . cordova . plugins . backgroundMode . disable ( ) ; } disableSync ( ) { return this . syncSvc . disableSync ( ) ; } disableNativeEventListeners ( ) { return this . methodNotApplicable ( ) ; } downloadFile ( filename , textContents ) { if ( ! filename ) { throw new Error ( 'File name not supplied.' ) ; } const storageLocation = `${ window . cordova . file . externalRootDirectory }Download` ; return this . $q ( ( resolve , reject ) => { const onError = ( err ) => { return reject ( new FailedDownloadFileError ( undefined , err ) ) ; } ; this . logSvc . logInfo ( `Downloading file ${ filename }` ) ; window . resolveLocalFileSystemURL ( storageLocation , ( dirEntry ) => { dirEntry . getFile ( filename , { create : true } , ( fileEntry ) => { fileEntry . createWriter ( ( fileWriter ) => { fileWriter . write ( textContents ) ; fileWriter . onerror = onError ; fileWriter . onwriteend = ( ) => resolve ( filename ) ; } , onError ) ; } , onError ) ; } , onError ) ; } ) ; } enableBackgroundSync ( ) { if ( this . backgroundSyncInterval ) { return ; } window . cordova . plugins . backgroundMode . enable ( ) ; this . backgroundSyncInterval = this . $interval ( ( ) => { if ( ! window . cordova . plugins . backgroundMode . isActive ( ) ) { return ; } this . executeSync ( true ) . then ( this . disableBackgroundSync ) . catch ( ( err ) => { if ( err instanceof SyncUncommittedError || this . networkSvc . isNetworkConnectionError ( err ) ) { this . logSvc . logInfo ( 'Waiting for network connection...' ) ; return ; } this . disableBackgroundSync ( ) ; throw err ; } ) ; } , 120e3 ) ; } enableNativeEventListeners ( ) { return this . methodNotApplicable ( ) ; } executeSync ( isBackgroundSync = false , workingContext ? ) { if ( ! isBackgroundSync ) { this . workingSvc . show ( workingContext ) ; } return this . syncSvc . executeSync ( isBackgroundSync ) . finally ( ( ) => { if ( ! isBackgroundSync ) { this . workingSvc . hide ( ) ; } } ) ; } getAppVersion ( ) { return this . $q . when ( window . cordova . getAppVersion . getVersionNumber ( ) ) . then ( ( versionNumber ) => { return this . utilitySvc . getSemVerAlignedVersion ( versionNumber ) ; } ) ; } getAppVersionName ( ) { return this . $q . when ( window . cordova . getAppVersion . getVersionNumber ( ) ) ; } getCurrentLocale ( ) { let currentLocale = Globals . I18n . DefaultLocale ; return this . $q < any > ( ( resolve , reject ) => { navigator . globalization . getPreferredLanguage ( resolve , reject ) ; } ) . then ( ( language ) => { if ( ! angular . isUndefined ( language ?. value ) ) { currentLocale = language ?. value ; } return currentLocale ; } ) . catch ( ( err ) => { this . logSvc . logWarning ( `Couldn’t get current locale: ${ err . message }` ) ; return currentLocale ; } ) ; } getCurrentUrl ( ) { return this . $q . resolve ( this . currentPage ?. url ) ; } getI18nString ( i18nObj ) { const i18nStr = this . i18nObjects [ i18nObj . key ] ; if ( angular . isUndefined ( i18nStr ?? undefined ) ) { throw new I18nError ( 'I18n string has no value' ) ; } return i18nStr ; } getPageMetadata ( getFullMetadata = true , pageUrl ? ) { let inAppBrowser ; let loadUrlTimeout ; let metadataUrl = pageUrl ?? this . currentPage ?. url ; if ( metadataUrl && ! new RegExp ( Globals . URL . ProtocolRegex ) . test ( metadataUrl ) ) { metadataUrl = `https://${ metadataUrl }` ; } const metadata = { title : this . currentPage ?. title , url : metadataUrl } ; const promise = this . $q < WebpageMetadata > ( ( resolve , reject ) => { if ( ! metadata . url ) { return resolve ( ) ; } if ( ! this . networkSvc . isNetworkConnected ( ) ) { return reject ( new FailedGetPageMetadataError ( ) ) ; } const handleResponse = ( pageContent ? , err ? ) : void => { this . workingSvc . hide ( ) ; if ( loadUrlTimeout ) { this . $timeout . cancel ( loadUrlTimeout ) ; loadUrlTimeout = null ; } if ( err || ! pageContent ) { return reject ( new FailedGetPageMetadataError ( undefined , err ) ) ; } const pageMetadata = this . metadataSvc . getMetadata ( metadata . url , pageContent ) ; if ( pageMetadata ) { return resolve ( pageMetadata ) ; } resolve ( metadata ) ; } ; if ( ! this . networkSvc . isNetworkConnected ( ) ) { return handleResponse ( ) ; } this . cancelGetPageMetadata = ( ) => { resolve ( metadata ) ; this . cancelGetPageMetadata = undefined ; } ; this . workingSvc . show ( WorkingContext . RetrievingMetadata ) ; inAppBrowser = window . cordova . InAppBrowser . open ( metadata . url , '_blank' , 'hidden=yes' ) ; inAppBrowser . addEventListener ( 'loaderror' , ( event ) => { const errMessage = event ?. message ?? 'Failed to load webpage' ; handleResponse ( null , new Error ( errMessage ) ) ; } ) ; inAppBrowser . addEventListener ( 'loadstop' , ( ) => { if ( ! inAppBrowser ) { return ; } inAppBrowser . executeScript ( { code : "(function() { var elements = document.querySelectorAll('video,script'); for (var i = 0; i < elements.length; i++) { elements[i].parentNode.removeChild(elements[i]); } })();" + "document.querySelector('html').outerHTML;" } , handleResponse ) ; } ) ; loadUrlTimeout = this . $timeout ( ( ) => { if ( ( promise as any ) . $$state ?. status === 0 ) { handleResponse ( null , new Error ( 'Timed out loading URL' ) ) ; } } , 10e3 ) ; } ) . finally ( ( ) => { if ( inAppBrowser ) { inAppBrowser . close ( ) ; inAppBrowser = null ; } } ) ; return promise ; } getSupportedUrl ( url ) { return url ; } initI18n ( ) { return this . getCurrentLocale ( ) . then ( ( currentLocale ) => { const i18nCode = currentLocale . split ( '-' ) [ 0 ] ; return this . $http . get < I18nObject [ ] > ( `./assets/strings_${ i18nCode }.json` ) . then ( ( response ) => { this . i18nObjects = response . data ; } ) ; } ) . catch ( ( err ) => { this . logSvc . logWarning ( `Couldn’t load i18n strings` ) ; throw err ; } ) ; } methodNotApplicable ( ) { return this . $q . resolve ( ) ; } openUrl ( url ) { window . open ( url , '_system' , '' ) ; } queueLocalResync ( ) { return this . queueSync ( { type : SyncType . Local } ) . then ( ( ) => { this . logSvc . logInfo ( 'Local sync data refreshed' ) ; } ) ; } queueSync ( sync ? ) { let resyncRequired = false ; return this . $q < boolean > ( ( resolve , reject ) => { if ( angular . isUndefined ( sync ) ) { return this . executeSync ( ) ; } if ( sync . type !== SyncType . LocalAndRemote && sync . type !== SyncType . Remote ) { return resolve ( true ) ; } this . syncSvc . checkForUpdates ( ) . then ( ( updatesAvailable ) => { if ( ! updatesAvailable ) { return resolve ( true ) ; } resyncRequired = true ; return this . queueSync ( { type : SyncType . Local } ) . then ( ( ) => { if ( sync . changeInfo . type === BookmarkChangeType . Add ) { return resolve ( true ) ; } return this . bookmarkHelperSvc . getCachedBookmarks ( ) . then ( ( bookmarks ) => { const changedBookmarkId = ( sync . changeInfo . changeData as RemoveBookmarkChangeData ) ?. id ?? ( sync . changeInfo . changeData as ModifyBookmarkChangeData ) ?. bookmark ?. id ; const changedBookmark = this . bookmarkHelperSvc . findBookmarkById ( changedBookmarkId , bookmarks ) as Bookmark ; if ( angular . isUndefined ( changedBookmark ) ) { this . logSvc . logInfo ( 'Changed bookmark could not be found, cancelling sync' ) ; return resolve ( false ) ; } resolve ( true ) ; } ) ; } ) ; } ) . catch ( reject ) ; } ) . catch ( ( ) => true ) . then ( ( proceedWithSync ) => { return ( proceedWithSync ? this . syncSvc . queueSync ( sync ) : this . $q . resolve ( ) ) . then ( ( ) => { if ( resyncRequired ) { this . utilitySvc . broadcastEvent ( AppEventType . RefreshBookmarkSearchResults ) ; } } ) . catch ( ( err ) => { if ( err instanceof SyncUncommittedError ) { this . enableBackgroundSync ( ) ; } throw err ; } ) ; } ) . finally ( this . workingSvc . hide ) ; } refreshNativeInterface ( ) { return this . methodNotApplicable ( ) ; } startSyncUpdateChecks ( ) { return this . methodNotApplicable ( ) ; } stopSyncUpdateChecks ( ) { return this . methodNotApplicable ( ) ; } urlIsSupported ( ) { return true ; } }