From 9a5b5e2e4fa388870385ab4c9436b95fe8bbc343 Mon Sep 17 00:00:00 2001 From: Benoit Lavenier <benoit.lavenier@e-is.pro> Date: Thu, 13 Aug 2020 19:45:10 +0200 Subject: [PATCH] [enh] Handle URI used to launch the app --- config.xml | 6 +- gradle/wrapper/gradle-wrapper.properties | 5 - gulpfile.js | 5 + package.json | 8 +- .../android/build/app/build-extras.gradle | 9 -- .../java/fr/duniter/cesium/MainActivity.java | 130 ------------------ www/js/controllers/home-controllers.js | 93 +++---------- www/js/platform.js | 116 +++++++++++++--- www/js/services/device-services.js | 44 +++++- www/js/services/http-services.js | 4 +- 10 files changed, 177 insertions(+), 243 deletions(-) delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100644 resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java diff --git a/config.xml b/config.xml index 5900c5d31..15fe32f64 100644 --- a/config.xml +++ b/config.xml @@ -55,9 +55,9 @@ </feature> <platform name="android"> <preference name="AndroidXEnabled" value="true" /> - <preference name="cdvMinSdkVersion" value="16" /> - <preference name="cdvCompileSdkVersion" value="29" /> - <preference name="cdvBuildToolsVersion" value="29.0.2" /> + <preference name="AndroidLaunchMode" value="singleTask" /> + <preference name="android-minSdkVersion" value="16" /> + <preference name="android-targetSdkVersion" value="29" /> <hook src="scripts/hooks/before_prepare.js" type="before_prepare" /> <hook src="scripts/hooks/after_prepare.js" type="after_prepare" /> <icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" /> diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ee93d6819..000000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-6.5.1-all.zip diff --git a/gulpfile.js b/gulpfile.js index be0a2d265..672c8fe31 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1264,6 +1264,11 @@ function cdvAndroidManifest() { // add <uses-sdk> (tools:overrideLibrary) .pipe(replace(/(<\/manifest>)/, ' <uses-sdk tools:overrideLibrary="org.kaliumjni.lib,org.apache.cordova" />\n$1')) + // Add URI scheme web+june + // Patch invalid intent-filter (should be a bug of cordova-plugin-customurlschema) + // FIXME : this cause too many intent-filter are appended, on each build + //.pipe(replace('<data android:host=" " android:pathPrefix="/" android:scheme=" " />', '<data android:scheme="web+june" />')) + .pipe(gulp.dest(srcMainPath)); } diff --git a/package.json b/package.json index 4b72ac018..d5feb51e5 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "lint": "node scripts/node/jshint.js", "install-platforms": "ionic cordova prepare", "start": "ionic serve", - "start:firefox": "gulp webExtCompile && web-ext run --source-dir ./dist/web/ext/", + "start:webExt": "gulp webExtCompile && web-ext run --source-dir ./dist/web/ext/", "start:android": "ionic cordova run android --color", "docker:build": "sudo docker build . -t cesium/release", "docker:run": "sudo docker run -ti --rm -p 8100:8100 -p 35729:35729 -v .:/cesium:rw cesium/release", @@ -203,9 +203,9 @@ "cordova-plugin-androidx-adapter": {}, "cordova-plugin-customurlscheme": { "URL_SCHEME": "june", - "ANDROID_SCHEME": " ", - "ANDROID_HOST": " ", - "ANDROID_PATHPREFIX": "/" + "ANDROID_SCHEME": "http", + "ANDROID_HOST": "g1.duniter.org", + "ANDROID_PATHPREFIX": "/wallet" }, "cordova-plugin-secure-storage-android10": {}, "cordova-plugin-minisodium": {} diff --git a/resources/android/build/app/build-extras.gradle b/resources/android/build/app/build-extras.gradle index 00875ab7b..63ce8dcd9 100644 --- a/resources/android/build/app/build-extras.gradle +++ b/resources/android/build/app/build-extras.gradle @@ -9,12 +9,3 @@ dependencies { exclude group: 'com.android.support', module:'support-v4' } } - -// Overrides the value of minSdkVersion set in AndroidManifest.xml. Useful when creating multiple APKs based on SDK version -ext.cdvMinSdkVersion=16 - - // Overrides the automatically detected android.compileSdkVersion value -ext.cdvCompileSdkVersion=29 - - // Overrides the automatically detected android.buildToolsVersion value -ext.cdvBuildToolsVersion='29.0.2' diff --git a/resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java b/resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java deleted file mode 100644 index 3287e5747..000000000 --- a/resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. - */ - -package fr.duniter.cesium; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import org.apache.cordova.*; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import java.net.URLEncoder; -import java.util.Locale; - -public class MainActivity extends CordovaActivity -{ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - // enable Cordova apps to be started in the background - Bundle extras = getIntent().getExtras(); - if (extras != null && extras.getBoolean("cdvStartInBackground", false)) { - moveTaskToBack(true); - } - - // Set by <content src="index.html" /> in config.xml - loadUrl(launchUrl); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - String action = intent.getAction(); - if ("android.intent.action.VIEW".equals(action)) { - Uri data = intent.getData(); - loadFromUri(data); - setResult(Activity.RESULT_OK); - } - } - - protected void loadFromUri(Uri uri) { - List<String> pathSegments; - final String scheme = uri.getScheme(); - if (scheme == null) return; // Skip if no scheme - - if ("http".equals(scheme) || "https".equals(scheme)) { - pathSegments = uri.getPathSegments(); - } else if ("web+june".equals(scheme) || "june".equals(scheme)) { - pathSegments = new ArrayList<String>(); - // Use the host as first path segment, if any - if (uri.getHost() != null) { - pathSegments.add(uri.getHost()); - } - // Or use - else if (uri.getEncodedSchemeSpecificPart() != null) { - pathSegments.add(uri.getEncodedSchemeSpecificPart()); - } - if (uri.getPathSegments() != null) pathSegments.addAll(uri.getPathSegments()); - } else { - return; // Skip - } - - if (pathSegments.size() == 0) return; // Skip - - // Create the URI expected by Cesium - String fixedUri = "june://" + join(pathSegments, "/"); - if (uri.getQuery() != null) { - fixedUri += uri.getQuery(); - } - String url = getLaunchUrlNoHash() + "#/app/home?uri=" + URLEncoder.encode(fixedUri); - - if (appView == null) { - init(); - } - runOnUiThread(new Runnable() { - public void run() { - MainActivity.this.appView.loadUrlIntoView(url, false); - } - }); - } - - protected String getLaunchUrlNoHash() { - String url = "http://localhost/";//this.launchUrl; - // Remove hash path - int hashIndex = url.indexOf('#'); - if (hashIndex != -1) { - url = url.substring(0, hashIndex); - } - return url; - } - - - protected String join(List<String> items, String separator) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i<items.size(); i++) { - sb.append(items.get(i)); - sb.append(separator); - } - // Remove last separator - sb.setLength(sb.length() - separator.length()); - - return sb.toString(); - } -} diff --git a/www/js/controllers/home-controllers.js b/www/js/controllers/home-controllers.js index 8c0bd8980..9fd026656 100644 --- a/www/js/controllers/home-controllers.js +++ b/www/js/controllers/home-controllers.js @@ -26,7 +26,7 @@ angular.module('cesium.home.controllers', ['cesium.platform', 'cesium.services'] .controller('HomeCtrl', HomeController) ; -function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $http, UIUtils, BMA, +function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $http, $q, UIUtils, BMA, csConfig, csCache, csPlatform, csCurrency, csSettings, csHttp) { 'ngInject'; @@ -46,21 +46,17 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht } if (state && state.stateParams && state.stateParams.uri) { - return $scope.redirectFromUri(state.stateParams.uri); + + return csPlatform.uri.open(state.stateParams.uri) + .then(function() { + $scope.loading = false; + }); } else if (state && state.stateParams && state.stateParams.error) { // Error query parameter $scope.error = state.stateParams.error; $scope.node = csCurrency.data.node; $scope.loading = false; - $ionicHistory.nextViewOptions({ - disableAnimate: true, - disableBack: true, - historyRoot: true - }); - $state.go('app.home', {error: undefined}, { - reload: false, - inherit: true, - notify: false}); + $scope.cleanLocationHref(state); } else { // Wait platform to be ready @@ -176,69 +172,24 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht } }; - /** - * Parse an URI (see g1lien) - * @param uri - * @returns {*} - */ - $scope.redirectFromUri = function(uri) { - console.debug("[home] Detecting external uri: ", uri); - var parts = csHttp.uri.parse(uri); - - var state,stateParams; - - if (parts.protocol === 'g1:' || parts.protocol === 'june:') { - console.debug("[home] Applying uri...", parts); - - // Pubkey - if (parts.hostname && BMA.regexp.PUBKEY.test(parts.hostname)) { - state = 'app.wot_identity'; - stateParams = {pubkey: parts.hostname}; - } - else if ((parts.hostname === 'wallet' || parts.hostname === 'pubkey') && BMA.regexp.PUBKEY.test(parts.pathSegments[0])) { - state = 'app.wot_identity'; - stateParams = {pubkey: parts.pathSegments[0]}; - } - - // Block - else if (parts.hostname === 'block') { - state = 'app.view_block'; - stateParams = {number: parts.pathSegments[0]}; - } - - // Otherwise, try to a wot lookup - else if (parts.hostname && BMA.regexp.USER_ID.test(parts.hostname)) { - state = 'app.wot_lookup.tab_search'; - stateParams = {q: parts.hostname}; - } - } - - if (state === 'app.wot_identity' && parts.searchParams && (angular.isDefined(parts.searchParams.action) || angular.isDefined(parts.searchParams.amount))) { - stateParams = angular.merge(stateParams, - // Add default actions - { action: 'transfer'}, - // Add path params - parts.searchParams); - } + // remove '?uri&error' from the location URI, and inside history + $scope.cleanLocationHref = function(state) { + if (state && state.stateParams) { + var stateParams = angular.copy(state.stateParams); + delete stateParams.uri; + delete stateParams.error; - // Open the state - if (state) { - $ionicHistory.clearHistory(); + // Update location href $ionicHistory.nextViewOptions({ - disableAnimate: false, - disableBack: true, - historyRoot: true - }); - return $state.go(state, stateParams, { - reload: true, - inherit: false, - notify: true + disableAnimate: true, + disableBack: false, + historyRoot: false }); - } - else { - console.error("[home] Unknown URI format: " + uri); - this.loading = false; - return UIUtils.alert.error("ERROR.UNKNOWN_URI_FORMAT", uri); + return $state.go(state.stateName, stateParams, { + reload: false, + inherit: true, + notify: false + }); } }; diff --git a/www/js/platform.js b/www/js/platform.js index b0fffc3e6..24be8e508 100644 --- a/www/js/platform.js +++ b/www/js/platform.js @@ -100,7 +100,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] }) - .factory('csPlatform', function (ionicReady, $rootScope, $q, $state, $translate, $timeout, UIUtils, + .factory('csPlatform', function (ionicReady, $rootScope, $q, $state, $translate, $timeout, $ionicHistory, UIUtils, BMA, Device, csHttp, csConfig, csCache, csSettings, csCurrency, csWallet) { 'ngInject'; @@ -113,8 +113,8 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] removeChangeStateListener; // Fix csConfig values - csConfig.demo = csConfig.demo === true || csConfig.demo === 'true' ||Â false; - csConfig.readonly = csConfig.readonly === true || csConfig.readonly === 'true' ||Â false; + csConfig.demo = csConfig.demo === true || csConfig.demo === 'true' || false; + csConfig.readonly = csConfig.readonly === true || csConfig.readonly === 'true' || false; function disableChangeState() { if (removeChangeStateListener) return; // make sure to call this once @@ -123,11 +123,10 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] if (!event.defaultPrevented && next.name !== 'app.home' && next.name !== 'app.settings') { event.preventDefault(); if (startPromise) { - startPromise.then(function() { + startPromise.then(function () { $state.go(next.name, nextParams); }); - } - else { + } else { UIUtils.loading.hide(); } } @@ -162,12 +161,12 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] } // Try to get summary - return csHttp.get(fallbackNode.host, fallbackNode.port, '/node/summary', fallbackNode.port==443 || BMA.node.forceUseSsl)() - .catch(function(err) { + return csHttp.get(fallbackNode.host, fallbackNode.port, '/node/summary', fallbackNode.port == 443 || BMA.node.forceUseSsl)() + .catch(function (err) { console.error('[platform] Could not reach fallback node [{0}]: skipping'.format(newServer)); // silent, but return no result (will loop to the next fallback node) }) - .then(function(res) { + .then(function (res) { if (!res) return checkBmaNodeAlive(); // Loop // Force to show port/ssl, if this is the only difference @@ -175,14 +174,13 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] if (messageParam.old === messageParam.new) { if (BMA.port != fallbackNode.port) { messageParam.new += ':' + fallbackNode.port; - } - else if (BMA.useSsl == false && (fallbackNode.useSsl || fallbackNode.port==443)) { + } else if (BMA.useSsl == false && (fallbackNode.useSsl || fallbackNode.port == 443)) { messageParam.new += ' (SSL)'; } } return $translate('CONFIRM.USE_FALLBACK_NODE', messageParam) - .then(function(msg) { + .then(function (msg) { return UIUtils.alert.confirm(msg); }) .then(function (confirm) { @@ -220,7 +218,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] }; } }) - .catch(function(err) { + .catch(function (err) { // silent (just log it) console.error('[platform] Failed to get Cesium latest version', err); }) @@ -229,7 +227,71 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] return $q.when(); } - function registerProtocols() { + /** + * Parse an external URI (see g1lien), and open the expected state + * @param uri + * @returns {*} + */ + function openUri(uri) { + if (!uri) return $q.when(); // Skip + + console.info('[platform] Detecting external uri: ', uri); + + var parts = csHttp.uri.parse(uri), + state, stateParams; + + // G1 lien + if (parts.protocol === 'june:') { + console.debug("[home] Applying uri...", parts); + + // Pubkey + if (parts.hostname && BMA.regexp.PUBKEY.test(parts.hostname)) { + state = 'app.wot_identity'; + stateParams = {pubkey: parts.hostname}; + } else if ((parts.hostname === 'wallet' || parts.hostname === 'pubkey') && BMA.regexp.PUBKEY.test(parts.pathSegments[0])) { + state = 'app.wot_identity'; + stateParams = {pubkey: parts.pathSegments[0]}; + } + + // Block + else if (parts.hostname === 'block') { + state = 'app.view_block'; + stateParams = {number: parts.pathSegments[0]}; + } + + // Otherwise, try to a wot lookup + else if (parts.hostname && BMA.regexp.USER_ID.test(parts.hostname)) { + state = 'app.wot_lookup.tab_search'; + stateParams = {q: parts.hostname}; + } + } + + if (state === 'app.wot_identity' && parts.searchParams && (angular.isDefined(parts.searchParams.action) || angular.isDefined(parts.searchParams.amount))) { + stateParams = angular.merge(stateParams, + // Add default actions + {action: 'transfer'}, + // Add path params + parts.searchParams); + } + + if (state) { + // Open the state, after cleaning current location URI + return $state.go(state, stateParams, { + reload: true + }) + .then(function () { + // This is need to make back button working again + return $timeout(function () { + if ($ionicHistory.backView()) $ionicHistory.removeBackView(); + }, 400); + }); + } else { + console.error("[home] Unknown URI format: " + uri); + return UIUtils.alert.error("ERROR.UNKNOWN_URI_FORMAT", uri); + } + } + + function registerProtocolHandlers() { var protocols = ['web+june'/*, 'web+g1', 'g1'*/]; _.each(protocols, function(protocol) { @@ -246,7 +308,10 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] function addListeners() { listeners = [ // Listen if node changed - BMA.api.node.on.restart($rootScope, restart, this) + BMA.api.node.on.restart($rootScope, restart, this), + + // Listen for new intent + Device.api.intent.on.new($rootScope, handleOpenUri, this) ]; } @@ -275,7 +340,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] // Avoid change state disableChangeState(); - registerProtocols(); + registerProtocolHandlers(); // We use 'ionicReady()' instead of '$ionicPlatform.ready()', because this one is callable many times startPromise = ionicReady() @@ -302,6 +367,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] .then(function(){ enableChangeState(); addListeners(); + processLaunchUri(); startPromise = null; started = true; }) @@ -332,6 +398,20 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] }, 500); } + /** + * Process launch intent, as it could have been triggered BEFORE addListeners() + * @returns {*} + */ + function processLaunchUri() { + return Device.intent.last() + .then(function(intent) { + if (intent) { + Device.intent.clear(); + return openUri(intent); + } + }); + } + return { disableChangeState: disableChangeState, isStarted: isStarted, @@ -341,6 +421,9 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] stop: stop, version: { latest: getLatestRelease + }, + uri: { + open: openUri } }; }) @@ -423,6 +506,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] if ($ionicHistory.backView()) { return $ionicHistory.goBack(); } + event.preventDefault(); return UIUtils.alert.confirm('CONFIRM.EXIT_APP') .then(function (confirm) { diff --git a/www/js/services/device-services.js b/www/js/services/device-services.js index 478842435..5479c3b19 100644 --- a/www/js/services/device-services.js +++ b/www/js/services/device-services.js @@ -2,7 +2,7 @@ var App; angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.settings.services']) - .factory('Device', function($rootScope, $translate, $ionicPopup, $q, + .factory('Device', function($rootScope, $translate, $ionicPopup, $q, Api, // removeIf(no-device) $cordovaClipboard, $cordovaBarcodeScanner, $cordovaCamera, // endRemoveIf(no-device) @@ -14,6 +14,8 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti MAX_HEIGHT: 400, MAX_WIDTH: 400 }, + that = this, + api = new Api(this, "Device"), exports = { // workaround to quickly no is device or not (even before the ready() event) enable: true @@ -143,6 +145,35 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti } }; + function getLastIntent() { + var deferred = $q.defer(); + window.plugins.launchmyapp.getLastIntent( + deferred.resolve, + deferred.reject); + return deferred.promise; + } + + function handleOpenUri(intent) { + if (intent) { + console.info('[platform] Received new intent: ', intent); + cache.lastIntent = intent; // Remember, for last() + api.intent.raise.new(intent); + } + } + + exports.intent = { + enable: false, + last: function() { + return $q.when(cache.lastIntent); + }, + handle: handleOpenUri, + clear: function() { + cache.lastIntent = undefined; + } + }; + + window.handleOpenURL = handleOpenUri; // Need by cordova-plugin-customurlscheme + // Numerical keyboard - fix #30 exports.keyboard.digit = { settings: { @@ -242,16 +273,18 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti exports.keyboard.enable = cordova && cordova.plugins && !!cordova.plugins.Keyboard; exports.barcode.enable = cordova && cordova.plugins && !!cordova.plugins.barcodeScanner && !exports.isOSX(); exports.clipboard.enable = cordova && cordova.plugins && !!cordova.plugins.clipboard; + exports.intent.enable = window && !!window.plugins.launchmyapp; if (exports.keyboard.enable) { angular.extend(exports.keyboard, cordova.plugins.Keyboard); } - console.debug('[device] Ionic platform ready, with [camera: {0}] [barcode scanner: {1}] [keyboard: {2}] [clipboard: {3}]' - .format(exports.camera.enable, exports.barcode.enable, exports.keyboard.enable, exports.clipboard.enable)); + console.debug('[device] Ionic platform ready, with {camera: {0}, barcode: {1}, keyboard: {2}, clipboard: {3}, intent: {4}}' + .format(exports.camera.enable, exports.barcode.enable, exports.keyboard.enable, exports.clipboard.enable, exports.intent.enable)); if (cordova.InAppBrowser) { console.debug('[device] Enabling InAppBrowser'); + window.open = cordova.InAppBrowser.open; } } else { @@ -265,6 +298,11 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti return startPromise; }; + api.registerEvent('intent', 'new'); + + // Export the event api (see ngApi) + exports.api = api; + return exports; }) diff --git a/www/js/services/http-services.js b/www/js/services/http-services.js index dc6f6b173..0d4e08b39 100644 --- a/www/js/services/http-services.js +++ b/www/js/services/http-services.js @@ -320,9 +320,9 @@ angular.module('cesium.http.services', ['cesium.cache.services']) var protocol, hostname; // G1 URI (see G1lien) - if (uri.startsWith('g1:') || uri.startsWith('june:') || uri.startsWith('web+june:')) { + if (uri.startsWith('june:') || uri.startsWith('web+june:')) { protocol = 'june:'; - var path = uri.replace(/^(g1|web\+june|june):(\/\/)?/, ''); + var path = uri.replace(/^(web\+june|june):(\/\/)?/, ''); // Store hostname here, because parse will apply a lowercase hostname = path; -- GitLab