diff --git a/apps/app/App_Resources/Android/AndroidManifest.xml b/apps/app/App_Resources/Android/AndroidManifest.xml index 72e4464c2..b73635900 100644 --- a/apps/app/App_Resources/Android/AndroidManifest.xml +++ b/apps/app/App_Resources/Android/AndroidManifest.xml @@ -27,6 +27,17 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true" > + + + + + + + + diff --git a/apps/app/ui-tests-app/intent/main-page.ts b/apps/app/ui-tests-app/intent/main-page.ts new file mode 100644 index 000000000..fba42084f --- /dev/null +++ b/apps/app/ui-tests-app/intent/main-page.ts @@ -0,0 +1,17 @@ +import { EventData } from "tns-core-modules/data/observable"; +import { SubMainPageViewModel } from "../sub-main-page-view-model"; +import { WrapLayout } from "tns-core-modules/ui/layouts/wrap-layout"; +import { Page } from "tns-core-modules/ui/page"; + +export function pageLoaded(args: EventData) { + const page = args.object; + const wrapLayout = page.getViewById("wrapLayoutWithExamples"); + page.bindingContext = new SubMainPageViewModel(wrapLayout, loadExamples()); +} + +export function loadExamples() { + const examples = new Map(); + examples.set("open-file", "intent/open-file"); + + return examples; +} \ No newline at end of file diff --git a/apps/app/ui-tests-app/intent/main-page.xml b/apps/app/ui-tests-app/intent/main-page.xml new file mode 100644 index 000000000..33306f0d0 --- /dev/null +++ b/apps/app/ui-tests-app/intent/main-page.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/app/ui-tests-app/intent/open-file.ts b/apps/app/ui-tests-app/intent/open-file.ts new file mode 100644 index 000000000..d95ccc75f --- /dev/null +++ b/apps/app/ui-tests-app/intent/open-file.ts @@ -0,0 +1,15 @@ +import { isIOS, isAndroid } from "tns-core-modules/platform"; +import * as fs from "tns-core-modules/file-system/file-system"; +import * as utils from "tns-core-modules/utils/utils"; + +export function openFile() { + let directory; + if (isIOS) { + directory = fs.knownFolders.ios.downloads(); + } else if (isAndroid) { + directory = android.os.Environment.getExternalStorageDirectory().getAbsolutePath().toString(); + } + + const filePath = fs.path.join(directory, "Test_File_Open.txt"); + utils.openFile(filePath); +} \ No newline at end of file diff --git a/apps/app/ui-tests-app/intent/open-file.xml b/apps/app/ui-tests-app/intent/open-file.xml new file mode 100644 index 000000000..aa41c4042 --- /dev/null +++ b/apps/app/ui-tests-app/intent/open-file.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/app/ui-tests-app/main-page.ts b/apps/app/ui-tests-app/main-page.ts index 2e6c4fd49..88d6d06d9 100644 --- a/apps/app/ui-tests-app/main-page.ts +++ b/apps/app/ui-tests-app/main-page.ts @@ -36,6 +36,7 @@ export function pageLoaded(args: EventData) { examples.set("progress-bar", "progress-bar/main-page"); examples.set("date-picker", "date-picker/date-picker"); examples.set("nested-frames", "nested-frames/main-page"); + examples.set("intent", "intent/main-page"); page.bindingContext = new MainPageViewModel(wrapLayout, examples); const parent = page.getViewById("parentLayout"); diff --git a/tests/app/App_Resources/Android/AndroidManifest.xml b/tests/app/App_Resources/Android/AndroidManifest.xml index dea9961e6..2c9be1067 100644 --- a/tests/app/App_Resources/Android/AndroidManifest.xml +++ b/tests/app/App_Resources/Android/AndroidManifest.xml @@ -27,6 +27,7 @@ android:label="@string/app_name" android:largeHeap="true" android:theme="@style/AppTheme" > + 10) { - allTests["SAFEAREALAYOUT"] = safeAreaLayoutTests; - allTests["SAFEAREA-LISTVIEW"] = safeAreaListViewtTests; - allTests["SAFEAREA-SCROLL-VIEW"] = scrollViewSafeAreaTests; - allTests["SAFEAREA-REPEATER"] = repeaterSafeAreaTests; - allTests["SAFEAREA-WEBVIEW"] = webViewSafeAreaTests; -} +// if (platform.isIOS && ios.MajorVersion > 10) { +// allTests["SAFEAREALAYOUT"] = safeAreaLayoutTests; +// allTests["SAFEAREA-LISTVIEW"] = safeAreaListViewtTests; +// allTests["SAFEAREA-SCROLL-VIEW"] = scrollViewSafeAreaTests; +// allTests["SAFEAREA-REPEATER"] = repeaterSafeAreaTests; +// allTests["SAFEAREA-WEBVIEW"] = webViewSafeAreaTests; +// } -import * as stylePropertiesTests from "./ui/styling/style-properties-tests"; -allTests["STYLE-PROPERTIES"] = stylePropertiesTests; +// import * as stylePropertiesTests from "./ui/styling/style-properties-tests"; +// allTests["STYLE-PROPERTIES"] = stylePropertiesTests; -import * as frameTests from "./ui/frame/frame-tests"; -allTests["FRAME"] = frameTests; +// import * as frameTests from "./ui/frame/frame-tests"; +// allTests["FRAME"] = frameTests; -import * as viewTests from "./ui/view/view-tests"; -allTests["VIEW"] = viewTests; +// import * as viewTests from "./ui/view/view-tests"; +// allTests["VIEW"] = viewTests; -import * as viewLayoutChangedEventTests from "./ui/view/view-tests-layout-event"; -allTests["VIEW-LAYOUT-EVENT"] = viewLayoutChangedEventTests; +// import * as viewLayoutChangedEventTests from "./ui/view/view-tests-layout-event"; +// allTests["VIEW-LAYOUT-EVENT"] = viewLayoutChangedEventTests; -import * as styleTests from "./ui/styling/style-tests"; -allTests["STYLE"] = styleTests; +// import * as styleTests from "./ui/styling/style-tests"; +// allTests["STYLE"] = styleTests; -import * as visualStateTests from "./ui/styling/visual-state-tests"; -allTests["VISUAL-STATE"] = visualStateTests; +// import * as visualStateTests from "./ui/styling/visual-state-tests"; +// allTests["VISUAL-STATE"] = visualStateTests; -import * as valueSourceTests from "./ui/styling/value-source-tests"; -allTests["VALUE-SOURCE"] = valueSourceTests; +// import * as valueSourceTests from "./ui/styling/value-source-tests"; +// allTests["VALUE-SOURCE"] = valueSourceTests; -import * as buttonTests from "./ui/button/button-tests"; -allTests["BUTTON"] = buttonTests; +// import * as buttonTests from "./ui/button/button-tests"; +// allTests["BUTTON"] = buttonTests; -import * as borderTests from "./ui/border/border-tests"; -allTests["BORDER"] = borderTests; +// import * as borderTests from "./ui/border/border-tests"; +// allTests["BORDER"] = borderTests; -import * as labelTests from "./ui/label/label-tests"; -allTests["LABEL"] = labelTests; +// import * as labelTests from "./ui/label/label-tests"; +// allTests["LABEL"] = labelTests; -import * as tabViewTests from "./ui/tab-view/tab-view-tests"; -allTests["TAB-VIEW"] = tabViewTests; +// import * as tabViewTests from "./ui/tab-view/tab-view-tests"; +// allTests["TAB-VIEW"] = tabViewTests; -import * as tabViewNavigationTests from "./ui/tab-view/tab-view-navigation-tests"; -allTests["TAB-VIEW-NAVIGATION"] = tabViewNavigationTests; +// import * as tabViewNavigationTests from "./ui/tab-view/tab-view-navigation-tests"; +// allTests["TAB-VIEW-NAVIGATION"] = tabViewNavigationTests; -import * as imageTests from "./ui/image/image-tests"; -allTests["IMAGE"] = imageTests; +// import * as imageTests from "./ui/image/image-tests"; +// allTests["IMAGE"] = imageTests; -import * as sliderTests from "./ui/slider/slider-tests"; -allTests["SLIDER"] = sliderTests; +// import * as sliderTests from "./ui/slider/slider-tests"; +// allTests["SLIDER"] = sliderTests; -import * as switchTests from "./ui/switch/switch-tests"; -allTests["SWITCH"] = switchTests; +// import * as switchTests from "./ui/switch/switch-tests"; +// allTests["SWITCH"] = switchTests; -import * as progressTests from "./ui/progress/progress-tests"; -allTests["PROGRESS"] = progressTests; +// import * as progressTests from "./ui/progress/progress-tests"; +// allTests["PROGRESS"] = progressTests; -import * as placeholderTests from "./ui/placeholder/placeholder-tests"; -allTests["PLACEHOLDER"] = placeholderTests; +// import * as placeholderTests from "./ui/placeholder/placeholder-tests"; +// allTests["PLACEHOLDER"] = placeholderTests; -import * as pageTests from "./ui/page/page-tests"; -allTests["PAGE"] = pageTests; +// import * as pageTests from "./ui/page/page-tests"; +// allTests["PAGE"] = pageTests; -import * as listViewTests from "./ui/list-view/list-view-tests"; -allTests["LISTVIEW"] = listViewTests; +// import * as listViewTests from "./ui/list-view/list-view-tests"; +// allTests["LISTVIEW"] = listViewTests; -import * as activityIndicatorTests from "./ui/activity-indicator/activity-indicator-tests"; -allTests["ACTIVITY-INDICATOR"] = activityIndicatorTests; +// import * as activityIndicatorTests from "./ui/activity-indicator/activity-indicator-tests"; +// allTests["ACTIVITY-INDICATOR"] = activityIndicatorTests; -import * as textFieldTests from "./ui/text-field/text-field-tests"; -allTests["TEXT-FIELD"] = textFieldTests; +// import * as textFieldTests from "./ui/text-field/text-field-tests"; +// allTests["TEXT-FIELD"] = textFieldTests; -import * as textViewTests from "./ui/text-view/text-view-tests"; -allTests["TEXT-VIEW"] = textViewTests; +// import * as textViewTests from "./ui/text-view/text-view-tests"; +// allTests["TEXT-VIEW"] = textViewTests; -import * as listPickerTests from "./ui/list-picker/list-picker-tests"; -allTests["LIST-PICKER"] = listPickerTests; +// import * as listPickerTests from "./ui/list-picker/list-picker-tests"; +// allTests["LIST-PICKER"] = listPickerTests; -import * as datePickerTests from "./ui/date-picker/date-picker-tests"; -allTests["DATE-PICKER"] = datePickerTests; +// import * as datePickerTests from "./ui/date-picker/date-picker-tests"; +// allTests["DATE-PICKER"] = datePickerTests; -import * as timePickerTests from "./ui/time-picker/time-picker-tests"; -allTests["TIME-PICKER"] = timePickerTests; +// import * as timePickerTests from "./ui/time-picker/time-picker-tests"; +// allTests["TIME-PICKER"] = timePickerTests; -import * as webViewTests from "./ui/web-view/web-view-tests"; -allTests["WEB-VIEW"] = webViewTests; +// import * as webViewTests from "./ui/web-view/web-view-tests"; +// allTests["WEB-VIEW"] = webViewTests; -import * as htmlViewTests from "./ui/html-view/html-view-tests"; -allTests["HTML-VIEW"] = htmlViewTests; +// import * as htmlViewTests from "./ui/html-view/html-view-tests"; +// allTests["HTML-VIEW"] = htmlViewTests; -import * as repeaterTests from "./ui/repeater/repeater-tests"; -allTests["REPEATER"] = repeaterTests; +// import * as repeaterTests from "./ui/repeater/repeater-tests"; +// allTests["REPEATER"] = repeaterTests; -import * as segmentedBarTests from "./ui/segmented-bar/segmented-bar-tests"; -allTests["SEGMENTED-BAR"] = segmentedBarTests; +// import * as segmentedBarTests from "./ui/segmented-bar/segmented-bar-tests"; +// allTests["SEGMENTED-BAR"] = segmentedBarTests; -import * as animationTests from "./ui/animation/animation-tests"; -allTests["ANIMATION"] = animationTests; +// import * as animationTests from "./ui/animation/animation-tests"; +// allTests["ANIMATION"] = animationTests; -import * as lifecycle from "./ui/lifecycle/lifecycle-tests"; -allTests["LIFECYCLE"] = lifecycle; +// import * as lifecycle from "./ui/lifecycle/lifecycle-tests"; +// allTests["LIFECYCLE"] = lifecycle; -import * as cssAnimationTests from "./ui/animation/css-animation-tests"; -allTests["CSS-ANIMATION"] = cssAnimationTests; +// import * as cssAnimationTests from "./ui/animation/css-animation-tests"; +// allTests["CSS-ANIMATION"] = cssAnimationTests; -import * as transitionTests from "./navigation/transition-tests"; -allTests["TRANSITIONS"] = transitionTests; +// import * as transitionTests from "./navigation/transition-tests"; +// allTests["TRANSITIONS"] = transitionTests; -import * as searchBarTests from "./ui/search-bar/search-bar-tests"; -allTests["SEARCH-BAR"] = searchBarTests; +// import * as searchBarTests from "./ui/search-bar/search-bar-tests"; +// allTests["SEARCH-BAR"] = searchBarTests; -import * as navigationTests from "./navigation/navigation-tests"; -allTests["NAVIGATION"] = navigationTests; +// import * as navigationTests from "./navigation/navigation-tests"; +// allTests["NAVIGATION"] = navigationTests; -import * as livesyncTests from "./livesync/livesync-tests"; -allTests["LIVESYNC"] = livesyncTests; +// import * as livesyncTests from "./livesync/livesync-tests"; +// allTests["LIVESYNC"] = livesyncTests; -import * as tabViewRootTests from "./ui/tab-view/tab-view-root-tests"; -allTests["TAB-VIEW-ROOT"] = tabViewRootTests; +// import * as tabViewRootTests from "./ui/tab-view/tab-view-root-tests"; +// allTests["TAB-VIEW-ROOT"] = tabViewRootTests; -import * as resetRootViewTests from "./ui/root-view/reset-root-view-tests"; -allTests["RESET-ROOT-VIEW"] = resetRootViewTests; +// import * as resetRootViewTests from "./ui/root-view/reset-root-view-tests"; +// allTests["RESET-ROOT-VIEW"] = resetRootViewTests; -import * as rootViewTests from "./ui/root-view/root-view-tests"; -allTests["ROOT-VIEW"] = rootViewTests; +// import * as rootViewTests from "./ui/root-view/root-view-tests"; +// allTests["ROOT-VIEW"] = rootViewTests; import * as utilsTests from "./utils/utils-tests"; allTests["UTILS"] = utilsTests; diff --git a/tests/package.json b/tests/package.json index 3f6cb8562..a36530bc3 100644 --- a/tests/package.json +++ b/tests/package.json @@ -28,4 +28,4 @@ "scripts": { "check-circular-deps": "node circular-check.js" } -} \ No newline at end of file +} diff --git a/tns-core-modules/utils/utils.android.ts b/tns-core-modules/utils/utils.android.ts index c2b9db852..2b3845652 100644 --- a/tns-core-modules/utils/utils.android.ts +++ b/tns-core-modules/utils/utils.android.ts @@ -1,10 +1,16 @@ import { - write as traceWrite, categories as traceCategories, messageType as traceMessageType + write as traceWrite, + categories as traceCategories, + messageType as traceMessageType, } from "../trace"; export * from "./utils-common"; import { getNativeApplication, android as androidApp } from "../application"; +import { device } from "../platform"; +import { FileSystemAccess } from "../file-system/file-system-access"; + +const MIN_URI_SHARE_RESTRICTED_APK_VERSION = 24; export module layout { let density: number; @@ -224,3 +230,145 @@ export function openUrl(location: string): boolean { } return true; } + +/** + * Check whether external storage is read only + * + * @returns {boolean} whether the external storage is read only + */ +function isExternalStorageReadOnly(): boolean { + const extStorageState = android.os.Environment.getExternalStorageState(); + if (android.os.Environment.MEDIA_MOUNTED_READ_ONLY === extStorageState) { + return true; + } + return false; +} + +/** + * Checks whether external storage is available + * + * @returns {boolean} whether external storage is available + */ +function isExternalStorageAvailable(): boolean { + const extStorageState = android.os.Environment.getExternalStorageState(); + if (android.os.Environment.MEDIA_MOUNTED === extStorageState) { + return true; + } + return false; +} + +/** + * Detect the mimetype of a file at a given path + * + * @param {string} filePath + * @returns {string} mimetype + */ +function getMimeTypeNameFromExtension(filePath: string): string { + const mimeTypeMap = android.webkit.MimeTypeMap.getSingleton(); + const extension = new FileSystemAccess() + .getFileExtension(filePath) + .replace(".", "") + .toLowerCase(); + + return mimeTypeMap.getMimeTypeFromExtension(extension); +} + +/** + * Open a file + * + * @param {string} filePath + * @returns {boolean} whether opening the file succeeded or not + */ +export function openFile(filePath: string): boolean { + const context = ad.getApplicationContext(); + try { + // Ensure external storage is available + if (!isExternalStorageAvailable()) { + traceWrite( + ` +External storage is unavailable (please check app permissions). +Applications cannot access internal storage of other application on Android (see: https://developer.android.com/guide/topics/data/data-storage). +`, + traceCategories.Error, + traceMessageType.error, + ); + + return false; + } + + // Ensure external storage is available + if (isExternalStorageReadOnly()) { + traceWrite("External storage is read only", traceCategories.Error, traceMessageType.error); + return false; + } + + // Determine file mimetype & start creating intent + const mimeType = getMimeTypeNameFromExtension(filePath); + const intent = new android.content.Intent(android.content.Intent.ACTION_VIEW); + const chooserIntent = android.content.Intent.createChooser(intent, "Open File..."); + + intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK); + chooserIntent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK); + + // Android SDK <28 only requires starting the chooser Intent straight forwardly + const sdkVersion = parseInt(device.sdkVersion, 10); + if (sdkVersion && sdkVersion < MIN_URI_SHARE_RESTRICTED_APK_VERSION) { + traceWrite( + `detected sdk version ${sdkVersion} (< ${MIN_URI_SHARE_RESTRICTED_APK_VERSION}), using simple openFile`, + traceCategories.Debug + ); + intent.setDataAndType(android.net.Uri.fromFile(new java.io.File(filePath)), mimeType); + context.startActivity(chooserIntent); + return true; + } + + traceWrite( + `detected sdk version ${sdkVersion} (>= ${MIN_URI_SHARE_RESTRICTED_APK_VERSION}), using URI openFile`, + traceCategories.Debug + ); + + // Android SDK 24+ introduced file system permissions changes that disallow + // exposing URIs between applications + // + // see: https://developer.android.com/reference/android/os/FileUriExposedException + // see: https://github.com/NativeScript/NativeScript/issues/5661#issuecomment-456405380 + const providerName = `${context.getPackageName()}.provider`; + traceWrite(`fully-qualified provider name [${providerName}]`, traceCategories.Debug); + + const apkURI = android.support.v4.content.FileProvider.getUriForFile( + context, + providerName, + new java.io.File(filePath), + ); + + // Set flags & URI as data type on the view action + intent.addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION); + chooserIntent.addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION); + + // Finish intent setup + intent.setDataAndType(apkURI, mimeType); + + context.startActivity(chooserIntent); + + return true; + } catch (err) { + const msg = err.message ? `: ${err.message}` : ""; + traceWrite(`Error in openFile${msg}`, traceCategories.Error, traceMessageType.error); + + if (msg && + msg.includes("Attempt to invoke virtual method") && + msg.includes("android.content.pm.ProviderInfo.loadXmlMetaData") && + msg.includes("on a null object reference")) { + // Alert user to possible fix + traceWrite( + ` +Please ensure you have your manifest correctly configured with the FileProvider. +(see: https://developer.android.com/reference/android/support/v4/content/FileProvider#ProviderDefinition) +`, + traceCategories.Error, + ); + } + + return false; + } +} diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 7ae9e8257..2e4e06fca 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -285,6 +285,12 @@ export function isDataURI(uri: string): boolean */ export function openUrl(url: string): boolean +/** + * Opens file. + * @param {string} filePath The file. + */ +export function openFile(filePath: string): boolean + /** * Escapes special regex symbols (., *, ^, $ and so on) in string in order to create a valid regex from it. * @param source The original value.