Skip to content

Commit

Permalink
Merge pull request #3784 from dpalou/MOBILE-4410
Browse files Browse the repository at this point in the history
Mobile 4410
  • Loading branch information
NoelDeMartin authored Sep 27, 2023
2 parents a666a07 + 47b1798 commit 3ddd53c
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 19 deletions.
5 changes: 5 additions & 0 deletions cordova-plugin-moodleapp/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
<clobbers target="cordova.MoodleApp" />
</js-module>
<platform name="android">
<framework src="com.android.installreferrer:installreferrer:2.2" />
<config-file target="res/xml/config.xml" parent="/*">
<feature name="SecureStorage">
<param name="android-package" value="com.moodle.moodlemobile.SecureStorage"/>
</feature>
<feature name="InstallReferrer">
<param name="android-package" value="com.moodle.moodlemobile.InstallReferrer"/>
</feature>
</config-file>
<source-file src="src/android/SecureStorage.java" target-dir="src/com/moodle/moodlemobile" />
<source-file src="src/android/InstallReferrer.java" target-dir="src/com/moodle/moodlemobile" />
</platform>
<platform name="ios">
<config-file target="config.xml" parent="/*">
Expand Down
148 changes: 148 additions & 0 deletions cordova-plugin-moodleapp/src/android/InstallReferrer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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
//
// 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 com.moodle.moodlemobile;

import android.util.Log;
import android.os.RemoteException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.PluginResult;

import com.android.installreferrer.api.InstallReferrerClient;
import com.android.installreferrer.api.InstallReferrerStateListener;
import com.android.installreferrer.api.ReferrerDetails;

public class InstallReferrer extends CordovaPlugin implements InstallReferrerStateListener {

private static final String TAG = "InstallReferrer";
private static final int UNKNOWN_ERROR = 1;
private static final int FEATURE_NOT_SUPPORTED = 2;
private static final int SERVICE_UNAVAILABLE = 3;

private InstallReferrerClient referrerClient;
private CallbackContext callbackContext;
private JSONObject referrerResult;

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
try {
switch (action) {
case "getReferrer":
this.getReferrer(callbackContext);

return true;
}
} catch (Throwable e) {
Log.e(TAG, "Failed executing action: " + action, e);
callbackContext.error(e.getMessage());
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, UNKNOWN_ERROR));
}

return false;
}

/**
* Connect to the referrer client and obtain the referrer data when connected.
*
* @param callbackContext The callback context used when calling back into JavaScript.
*/
private void getReferrer(CallbackContext callbackContext) {
if (this.referrerResult != null) {
callbackContext.success(this.referrerResult);

return;
}

this.callbackContext = callbackContext;

try {
if (this.referrerClient == null) {
this.referrerClient = InstallReferrerClient.newBuilder(this.cordova.getActivity().getApplicationContext()).build();
}

this.referrerClient.startConnection(this);
} catch (Exception exception) {
Log.e(TAG, "startConnection error: " + exception.getMessage());
callbackContext.error(exception.getMessage());
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, UNKNOWN_ERROR));
}
}

/**
* Get referral data from an already established connection and pass it to current callback context.
*/
private void getReferralData() {
try {
ReferrerDetails response = referrerClient.getInstallReferrer();
JSONObject referrerResult = new JSONObject();

referrerResult.put("referrer", response.getInstallReferrer());
referrerResult.put("clickTime", response.getReferrerClickTimestampSeconds());
referrerResult.put("appInstallTime", response.getInstallBeginTimestampSeconds());
referrerResult.put("instantExperienceLaunched", response.getGooglePlayInstantParam());
this.referrerResult = referrerResult;

if (this.callbackContext != null) {
this.callbackContext.success(this.referrerResult);
}
} catch (Exception exception) {
Log.e(TAG, "getReferralData error: " + exception.getMessage());
if (this.callbackContext != null) {
this.callbackContext.error(exception.getMessage());
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, UNKNOWN_ERROR));
}

this.referrerClient.endConnection();
}

try {
this.referrerClient.endConnection();
} catch (Exception exception) {
// Ignore errors.
}
}

@Override
public void onInstallReferrerSetupFinished(int responseCode) {
switch (responseCode) {
case InstallReferrerClient.InstallReferrerResponse.OK:
// Connection established.
this.getReferralData();
break;
case InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED:
// API not available on the current Play Store app.
if (this.callbackContext != null) {
this.callbackContext.error("Referrer feature not supported.");
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FEATURE_NOT_SUPPORTED));
}
break;
case InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE:
// Connection couldn't be established.
if (this.callbackContext != null) {
this.callbackContext.error("Referrer service unavailable.");
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, SERVICE_UNAVAILABLE));
}
break;
}
}

@Override
public void onInstallReferrerServiceDisconnected() {
// Nothing to do.
}

}
2 changes: 2 additions & 0 deletions cordova-plugin-moodleapp/src/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { InstallReferrer } from './plugins/InstallReferrer';
import { SecureStorage } from './plugins/SecureStorage';

const api: MoodleAppPlugins = {
secureStorage: new SecureStorage(),
installReferrer: new InstallReferrer(),
};

// This is necessary to work around the default transpilation behavior,
Expand Down
39 changes: 39 additions & 0 deletions cordova-plugin-moodleapp/src/ts/plugins/InstallReferrer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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
//
// 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.

/**
* Allows retrieving install referrer data.
* https://developer.android.com/google/play/installreferrer
*/
export class InstallReferrer {

/**
* Get referrer data.
*
* @returns Referrer data.
*/
async getReferrer(): Promise<InstallReferrerResult> {
return new Promise((resolve, reject) => {
cordova.exec(resolve, reject, 'InstallReferrer', 'getReferrer', []);
});
}

}

export type InstallReferrerResult = {
referrer: string;
clickTime: number;
appInstallTime: number;
instantExperienceLaunched: boolean;
};
2 changes: 2 additions & 0 deletions cordova-plugin-moodleapp/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { InstallReferrer } from '../src/ts/plugins/InstallReferrer';
import { SecureStorage as SecureStorageImpl } from '../src/ts/plugins/SecureStorage';

declare global {

interface MoodleAppPlugins {
secureStorage: SecureStorageImpl;
installReferrer: InstallReferrer;
}

interface Cordova {
Expand Down
6 changes: 3 additions & 3 deletions src/core/features/login/pages/site/site.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h1>{{ 'core.login.connecttomoodle' | translate }}</h1>
<div class="ion-text-center ion-padding ion-margin-bottom core-login-site-logo" [class.hidden]="hasSites || enteredSiteUrl">
<img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation" alt="">
</div>
<form [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites && siteForm" #siteFormEl>
<form [formGroup]="siteForm" (ngSubmit)="connect(siteForm.value.siteUrl, $event)" *ngIf="!fixedSites && siteForm" #siteFormEl>
<!-- Form to input the site URL if there are no fixed sites. -->
<ng-container *ngIf=" siteSelector=='url'">
<ion-item>
Expand Down Expand Up @@ -48,7 +48,7 @@ <h2>{{ 'core.login.siteaddress' | translate }}</h2>
<h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item button *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)"
<ion-item button *ngIf="enteredSiteUrl" (click)="connect(enteredSiteUrl.url, $event)"
[attr.aria-label]="'core.login.connect' | translate" detail="true" class="core-login-entered-site">
<ion-thumbnail slot="start" aria-hidden="true">
<ion-icon name="fas-pen" aria-hidden="true"></ion-icon>
Expand Down Expand Up @@ -120,7 +120,7 @@ <h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2>

<!-- Template site selector. -->
<ng-template #sitelisting let-site="site">
<ion-item button (click)="connect($event, site.url, site)" [ngClass]="site.className" [attr.aria-label]="site.name" detail="true">
<ion-item button (click)="connect(site.url, $event, site)" [ngClass]="site.className" [attr.aria-label]="site.name" detail="true">
<ion-thumbnail *ngIf="siteFinderSettings.displayimage" slot="start">
<img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'" alt="" role="presentation">
<img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon" alt="" role="presentation">
Expand Down
48 changes: 41 additions & 7 deletions src/core/features/login/pages/site/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { CoreUserSupportConfig } from '@features/user/classes/support/support-co
import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config';
import { CoreLoginError } from '@classes/errors/loginerror';
import { CorePlatform } from '@services/platform';
import { CoreReferrer } from '@services/referrer';

/**
* Site (url) chooser when adding a new site.
Expand Down Expand Up @@ -98,8 +99,21 @@ export class CoreLoginSitePage implements OnInit {

if (sites.length) {
url = await this.initSiteSelector();
} else if (CoreConstants.CONFIG.enableonboarding && !CorePlatform.isIOS()) {
this.initOnboarding();
} else {
url = await this.consumeInstallReferrerUrl() ?? '';

const showOnboarding = CoreConstants.CONFIG.enableonboarding && !CorePlatform.isIOS();

if (url) {
this.connect(url);

if (showOnboarding) {
// Don't display onboarding in this case, and don't display it again later.
CoreConfig.set(CoreLoginHelperProvider.ONBOARDING_DONE, 1);
}
} else if (showOnboarding) {
this.initOnboarding();
}
}

this.showScanQR = CoreLoginHelper.displayQRInSiteScreen();
Expand Down Expand Up @@ -150,6 +164,26 @@ export class CoreLoginSitePage implements OnInit {
return this.fixedSites[0].url;
}

/**
* Consume install referrer URL.
*
* @returns Referrer URL, undefined if no URL to use.
*/
protected async consumeInstallReferrerUrl(): Promise<string | undefined> {
const url = await CoreUtils.ignoreErrors(CoreUtils.timeoutPromise(CoreReferrer.consumeInstallReferrerUrl(), 1000));
if (!url) {
return;
}

const hasSites = (await CoreUtils.ignoreErrors(CoreSites.getSites(), [])).length > 0;
if (hasSites) {
// There are sites stored already, don't use the referrer URL since it's an update or a backup was restored.
return;
}

return url;
}

/**
* Initialize and show onboarding if needed.
*
Expand Down Expand Up @@ -241,14 +275,14 @@ export class CoreLoginSitePage implements OnInit {
/**
* Try to connect to a site.
*
* @param e Event.
* @param url The URL to connect to.
* @param e Event (if any).
* @param foundSite The site clicked, if any, from the found sites list.
* @returns Promise resolved when done.
*/
async connect(e: Event, url: string, foundSite?: CoreLoginSiteInfoExtended): Promise<void> {
e.preventDefault();
e.stopPropagation();
async connect(url: string, e?: Event, foundSite?: CoreLoginSiteInfoExtended): Promise<void> {
e?.preventDefault();
e?.stopPropagation();

CoreApp.closeKeyboard();

Expand Down Expand Up @@ -540,7 +574,7 @@ export class CoreLoginSitePage implements OnInit {
// Put the text in the field (if present).
this.siteForm.controls.siteUrl.setValue(text);

this.connect(new Event('click'), text);
this.connect(text);
} else {
CoreDomUtils.showErrorModal('core.errorurlschemeinvalidsite', true);
}
Expand Down
8 changes: 6 additions & 2 deletions src/core/features/native/services/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ export class CoreNativeService {
* Get a native plugin instance.
*
* @param plugin Plugin name.
* @returns Plugin instance.
* @returns Plugin instance, null if plugin is not supported for current platform.
*/
plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> {
plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> | null {
if (plugin === 'installReferrer' && !CorePlatform.isAndroid()) {
return null;
}

if (!(plugin in this.plugins)) {
this.plugins[plugin] = asyncInstance(async () => {
await CorePlatform.ready();
Expand Down
Loading

0 comments on commit 3ddd53c

Please sign in to comment.