Skip to content

Commit

Permalink
Merge pull request #40 from dpogue/android-tests
Browse files Browse the repository at this point in the history
Android cleanup and tests, CI updates
  • Loading branch information
isuda authored Jun 4, 2024
2 parents 2aa2aad + 1bc77ff commit f240f44
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 148 deletions.
65 changes: 57 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ on:
jobs:
test-ios:
name: "iOS Tests"
runs-on: macos-12
runs-on: macos-14
env:
DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 16
node-version: 20

- name: Set up example app
run: |
Expand All @@ -35,14 +35,63 @@ jobs:
-project OAuthPluginTests.xcodeproj \
-scheme OAuthPluginTests \
-testPlan UnitTests \
-destination "platform=iOS Simulator,name=iPhone 13"
-destination "platform=iOS Simulator,name=iPhone 15"
working-directory: ./tests/ios

- name: Run iOS UI Tests
run: |
xcodebuild test -quiet \
xcodebuild test \
-project OAuthPluginTests.xcodeproj \
-scheme OAuthPluginTests \
-testPlan DeviceTests \
-destination "platform=iOS Simulator,name=iPhone 13"
-destination "platform=iOS Simulator,name=iPhone 15" \
-destination-timeout 300
working-directory: ./tests/ios


test-android:
name: "Android Tests"
runs-on: ubuntu-latest

steps:
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Use Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- name: Set up example app
run: |
npm i
npx cordova prepare android
working-directory: ./example

- name: Set up gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: 8.4

- name: Run Android Unit Tests
run: |
gradle test
working-directory: ./tests/android

- name: Run Android UI Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: gradle connectedCheck
working-directory: ./tests/android
149 changes: 12 additions & 137 deletions src/android/OAuthPlugin.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2019 Ayogo Health Inc.
* Copyright 2019 - 2022 Ayogo Health Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,51 +16,26 @@

package com.ayogo.cordova.oauth;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import androidx.browser.customtabs.CustomTabsIntent;
import android.text.TextUtils;

import java.util.ArrayList;
import java.util.List;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginResult;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;


public class OAuthPlugin extends CordovaPlugin {
private final String TAG = "OAuthPlugin";

// Taken from Google's CustomTabsHelper
// https://github.com/GoogleChrome/custom-tabs-client/blob/da65829d7d4b80c00809c6c4aa7f61f88fc7ca26/shared/src/main/java/org/chromium/customtabsclient/shared/CustomTabsHelper.java
static final String STABLE_PACKAGE = "com.android.chrome";
static final String BETA_PACKAGE = "com.chrome.beta";
static final String DEV_PACKAGE = "com.chrome.dev";
static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE";
private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";


/**
* The name of the package to use for the custom tab service.
*/
private String tabProvider = null;


/**
* Executes the request.
*
Expand All @@ -76,7 +51,7 @@ public class OAuthPlugin extends CordovaPlugin {
* @return Whether the action was valid.
*/
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) {
if ("startOAuth".equals(action)) {
try {
String authEndpoint = args.getString(0);
Expand Down Expand Up @@ -121,15 +96,7 @@ public void onNewIntent(Intent intent) {
jsobj.put(queryKey, uri.getQueryParameter(queryKey));
}

final String msg = jsobj.toString();
CordovaWebViewEngine engine = this.webView.getEngine();
final String jsCode = "window.dispatchEvent(new MessageEvent('message', { data: 'oauth::" + msg + "' }));";
if (engine != null) {
engine.evaluateJavascript(jsCode, null);
} else {
this.webView.sendJavascript(jsCode);
}

dispatchOAuthMessage(jsobj.toString());
} catch (JSONException e) {
LOG.e(TAG, "JSON Serialization failed");
e.printStackTrace();
Expand All @@ -144,112 +111,20 @@ public void onNewIntent(Intent intent) {
* @param url The URL of the OAuth endpoint.
*/
private void startOAuth(String url) {
String customTabsBrowser = findCustomTabProvider();

CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
CustomTabsIntent customTabsIntent = builder.build();

String packageName = this.findCustomTabProvider();
if (packageName != null) {
customTabsIntent.intent.setPackage(packageName);
}

customTabsIntent.launchUrl(this.cordova.getActivity(), Uri.parse(url));
}

@SuppressWarnings("deprecation")
private void dispatchOAuthMessage(final String msg) {
final String jsCode = "window.dispatchEvent(new MessageEvent('message', { data: 'oauth::" + msg + "' }));";

/**
* Goes through all apps that handle VIEW intents and have a warmup service.
*
* Picks the one chosen by the user if there is one, otherwise makes a best
* effort to return a valid package name.
*
* This is <strong>not</strong> threadsafe.
*
* @return The package name recommended to use for connecting to custom
* tabs related components.
*/
private String findCustomTabProvider() {
if (this.tabProvider != null) {
return this.tabProvider;
}

PackageManager pm = this.cordova.getActivity().getPackageManager();

// Get default VIEW intent handler.
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
String defaultViewHandlerPackageName = null;

if (defaultViewHandlerInfo != null) {
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
CordovaWebViewEngine engine = this.webView.getEngine();
if (engine != null) {
engine.evaluateJavascript(jsCode, null);
} else {
this.webView.sendJavascript(jsCode);
}


// Get all apps that can handle VIEW intents.
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, PackageManager.MATCH_ALL);
List<String> packagesSupportingCustomTabs = new ArrayList<>();

for (ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = new Intent();
serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setPackage(info.activityInfo.packageName);

if (pm.resolveService(serviceIntent, 0) != null) {
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
}
}

// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
// and service calls.
if (packagesSupportingCustomTabs.isEmpty()) {
this.tabProvider = null;
} else if (packagesSupportingCustomTabs.size() == 1) {
this.tabProvider = packagesSupportingCustomTabs.get(0);
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) && !this.hasSpecializedHandlerIntents(activityIntent) && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
this.tabProvider = defaultViewHandlerPackageName;
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
this.tabProvider = STABLE_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
this.tabProvider = BETA_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
this.tabProvider = DEV_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
this.tabProvider = LOCAL_PACKAGE;
}

return this.tabProvider;
}


/**
* Used to check whether there is a specialized handler for a given intent.
*
* @param intent The intent to check with.
* @return Whether there is a specialized handler for the given intent.
*/
private boolean hasSpecializedHandlerIntents(Intent intent) {
try {
PackageManager pm = this.cordova.getActivity().getPackageManager();
List<ResolveInfo> handlers = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);

if (handlers == null || handlers.size() == 0) {
return false;
}

for (ResolveInfo resolveInfo : handlers) {
IntentFilter filter = resolveInfo.filter;

if (filter == null || filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0 || resolveInfo.activityInfo == null) {
continue;
}

return true;
}
} catch (RuntimeException e) {
LOG.e(TAG, "Runtime exception while getting specialized handlers");
}

return false;
}
}
5 changes: 5 additions & 0 deletions tests/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build/
.gradle/
src/main/java/com/ayogo/cordova/oauth/OAuthPlugin.java
src/main/assets/
src/main/res/
91 changes: 91 additions & 0 deletions tests/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Copyright 2022 Ayogo Health Inc.
*
* Licensed 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
*
* https://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.
*/

buildscript {
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:8.3.1'
}
}

apply plugin: 'com.android.application'

android {
compileSdkVersion 34

defaultConfig {
namespace 'com.ayogo.cordova.oauth.tests'
applicationId 'com.ayogo.cordova.oauth.tests'
minSdkVersion 24
targetSdkVersion 34
versionCode 1
versionName '1.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

buildTypes {
debug {
testCoverageEnabled true
}
}
}

repositories {
google()
mavenCentral()
}

dependencies {
// These should get pulled in by the Cordova package, but don't :(
implementation 'androidx.appcompat:appcompat:[1.4.2,)'
implementation 'androidx.core:core-splashscreen:[1.0.0,)'
implementation 'androidx.webkit:webkit:[1.4.0,)'

implementation 'org.apache.cordova:framework:[11.0.0,)'
implementation 'androidx.browser:browser:[1.3.0,)'

testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-inline:4.6.1'
testImplementation 'org.json:json:[20220924,)'

androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}

task copyPluginSource(type: Copy) {
from '../../src/android'
into 'src/main/java/com/ayogo/cordova/oauth'
}

task copyExampleAssets(type: Copy) {
from '../../example/platforms/android/app/src/main/assets'
into 'src/main/assets'
}

task copyExampleResources(type: Copy) {
from '../../example/platforms/android/app/src/main/res'
into 'src/main/res'
}

preBuild.dependsOn(copyPluginSource)
preBuild.dependsOn(copyExampleAssets)
preBuild.dependsOn(copyExampleResources)
1 change: 1 addition & 0 deletions tests/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
android.useAndroidX = true
Loading

0 comments on commit f240f44

Please sign in to comment.