GDPR - Consent SDK - Consent form translation - localization

The Consent SDK allows to show a consent form which, however, it is currently in English only (version 1.0.3 of the SDK). SDK page says:
To update consent text of the Google-rendered consent form, modify the consentform.html file included in the Consent SDK as required.
However, consentform.html is provided as an asset and I don't see a way to localize it, especially using gradle. What is the best way to handle localization in this case? And why this was not done in the first place? Europe is not just English.

Since Google's EU Consent dialog is not localizable, I created my own consent dialog which you can translate as usual with strings.xml. It's loosely based on what Google did. This is to be used without mediation:
You are free to use my code, however consult your legal advisor, if the text is appropriate for you. I cannot provide legal advice on the consent text that is appropriate for you.
Add to your gradle file:
implementation 'com.google.android.ads.consent:consent-library:1.0.3'
Add member variables:
public boolean mShowNonPersonalizedAdRequests = false;
private AlertDialog mEuDialog;
In onCreate() call checkConsentStatus():
#Override
protected void onCreate(Bundle savedInstanceState) {
// ...
checkConsentStatus();
// ...
}
Add checkConsentStatus() method which uses Google's Consent SDK:
// https://developers.google.com/admob/android/eu-consent
private void checkConsentStatus(){
ConsentInformation consentInformation = ConsentInformation.getInstance(this);
ConsentInformation.getInstance(this).addTestDevice("YOUR-DEVICE-ID"); // enter your device id, if you need it for testing
String[] publisherIds = {"pub-YOUR-ADMOB-PUB-ID"}; // enter your admob pub-id
consentInformation.requestConsentInfoUpdate(publisherIds, new ConsentInfoUpdateListener() {
#Override
public void onConsentInfoUpdated(ConsentStatus consentStatus) {
log("User's consent status successfully updated: " +consentStatus);
if (ConsentInformation.getInstance(MainActivity.this).isRequestLocationInEeaOrUnknown()){
log("User is from EU");
/////////////////////////////
// TESTING - reset the choice
//ConsentInformation.getInstance(MainActivity.this).setConsentStatus(ConsentStatus.UNKNOWN);
/////////////////////////////
// If the returned ConsentStatus is UNKNOWN, collect user's consent.
if (consentStatus == ConsentStatus.UNKNOWN) {
showMyConsentDialog(false);
}
// If the returned ConsentStatus is PERSONALIZED or NON_PERSONALIZED
// the user has already provided consent. Forward consent to the Google Mobile Ads SDK.
else if (consentStatus == ConsentStatus.NON_PERSONALIZED) {
mShowNonPersonalizedAdRequests = true;
// The default behavior of the Google Mobile Ads SDK is to serve personalized ads.
// If a user has consented to receive only non-personalized ads, you can configure
// an AdRequest object with the following code to specify that only non-personalized
// ads should be returned.
}
} else {
log("User is NOT from EU");
// we don't have to do anything
}
}
#Override
public void onFailedToUpdateConsentInfo(String errorDescription) {
log("User's consent status failed to update: " +errorDescription);
}
});
}
Add showMyConsentDialog() method:
public void showMyConsentDialog(boolean showCancel) {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(MainActivity.this, R.style.MyAlertDialogStyle);
LayoutInflater inflater = getLayoutInflater();
View eu_consent_dialog = inflater.inflate(R.layout.eu_consent, null);
alertDialog.setView(eu_consent_dialog)
.setCancelable(false);
if (showCancel) alertDialog.setPositiveButton(R.string.dialog_close, null);
mEuDialog = alertDialog.create();
mEuDialog.show();
Button btn_eu_consent_yes = eu_consent_dialog.findViewById(R.id.btn_eu_consent_yes);
Button btn_eu_consent_no = eu_consent_dialog.findViewById(R.id.btn_eu_consent_no);
Button btn_eu_consent_remove_ads = eu_consent_dialog.findViewById(R.id.btn_eu_consent_remove_ads);
btn_eu_consent_yes.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mEuDialog.cancel();
toast(getString(R.string.thank_you), MainActivity.this);
ConsentInformation.getInstance(MainActivity.this).setConsentStatus(ConsentStatus.PERSONALIZED);
mShowNonPersonalizedAdRequests = false;
}
});
btn_eu_consent_no.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mEuDialog.cancel();
toast(getString(R.string.thank_you), MainActivity.this);
ConsentInformation.getInstance(MainActivity.this).setConsentStatus(ConsentStatus.NON_PERSONALIZED);
mShowNonPersonalizedAdRequests = true;
}
});
btn_eu_consent_remove_ads.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mEuDialog.cancel();
IAP_buyAdsFree(); // YOUR REMOVE ADS METHOD
}
});
TextView tv_eu_learn_more = eu_consent_dialog.findViewById(R.id.tv_eu_learn_more);
tv_eu_learn_more.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
euMoreInfoDialog();
}
});
}
This is the consent layout, save to eu_consent.xml:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:id="#+id/ll_eu_consent"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="#dimen/activity_horizontal_margin"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/eu_consent_text"
android:textSize="14sp"
android:paddingBottom="6dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/eu_consent_question"
android:textSize="14sp"
android:paddingBottom="6dp"
android:textStyle="bold"
/>
<Button
android:id="#+id/btn_eu_consent_yes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/eu_consent_yes"
android:textSize="13sp"
/>
<Button
android:id="#+id/btn_eu_consent_no"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/eu_consent_no"
android:textSize="13sp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
/>
<Button
android:id="#+id/btn_eu_consent_remove_ads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/action_remove_ads"
android:textSize="13sp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/eu_consent_change_setting"
android:textSize="14sp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
/>
<TextView
android:id="#+id/tv_eu_learn_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/learn_more"
android:textSize="14sp"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:textColor="#color/blue"
style="#style/SelectableItem"
/>
</LinearLayout>
</ScrollView>
Add euMoreInfoDialog():
private void euMoreInfoDialog(){
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this, R.style.MyAlertDialogStyle);
ScrollView sv = new ScrollView(this);
LinearLayout ll = new LinearLayout(this);
ll.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(40, 20, 40, 20);
TextView tv_my_privacy_policy = new TextView(this);
String link = ""+getResources().getString(R.string.app_name)+"";
tv_my_privacy_policy.setText(Html.fromHtml(link));
tv_my_privacy_policy.setMovementMethod(LinkMovementMethod.getInstance());
ll.addView(tv_my_privacy_policy, params);
TextView tv_google_partners = new TextView(this);
tv_google_partners.setText(R.string.google_partners);
tv_google_partners.setPadding(40,40,40,20);
ll.addView(tv_google_partners);
List<AdProvider> adProviders = ConsentInformation.getInstance(this).getAdProviders();
for (AdProvider adProvider : adProviders) {
//log("adProvider: " +adProvider.getName()+ " " +adProvider.getPrivacyPolicyUrlString());
link = ""+adProvider.getName()+"";
TextView tv_adprovider = new TextView(this);
tv_adprovider.setText(Html.fromHtml(link));
tv_adprovider.setMovementMethod(LinkMovementMethod.getInstance());
ll.addView(tv_adprovider, params);
}
sv.addView(ll);
builder.setTitle(R.string.privacy_policy)
.setView(sv)
.setPositiveButton(R.string.dialog_close, null);
final AlertDialog createDialog = builder.create();
createDialog.show();
}
In your AdMob web interface select the ad technology providers you wish to use. I suggest you don't select more than 20 (or so), because I assume the euMoreInfoDialog() will become very slow if you select too many providers.
Add to onDestroy() to prevent errors on screen rotate:
#Override
public void onDestroy(){
// ...
if (mEuDialog != null && mEuDialog.isShowing()) mEuDialog.cancel();
// ...
super.onDestroy();
}
When you make an ad request, check the value of mShowNonPersonalizedAdRequests and add "npa" to the request if necessary:
Bundle extras = new Bundle();
if (mShowNonPersonalizedAdRequests)
extras.putString("npa", "1");
AdRequest adRequest = new AdRequest.Builder()
.addTestDevice(AdRequest.DEVICE_ID_EMULATOR)
.addTestDevice("YOUR-DEVICE-ID-GOES-HERE") // insert your device id
.addNetworkExtrasBundle(AdMobAdapter.class, extras)
.build();
And lastly, add strings for all your languages to strings.xml:
<!-- EU GDPR Consent texts -->
<string name="eu_consent_text">Dear user!\n\nWe use Google Admob to show ads. Ads support our work, and enable further development of this app. In line with the new European Data Protection Regulation (GDPR), we need your consent to serve ads tailored for you.</string>
<string name="eu_consent_question">Can your data be used to show ads tailored for you?</string>
<string name="learn_more">Learn how your data is used</string>
<string name="google_partners">Google and its partners:</string>
<string name="eu_consent_yes">Yes, continue to show relevant ads</string>
<string name="eu_consent_no">No, show ads that are irrelevant</string>
<string name="eu_consent_change_setting">You can change this setting anytime in the \"About\" window.</string>
<string name="thank_you">Thank you!</string>
That's it!
(Note: log() and toast() are my methods, replace them with your own. PRIVACY_URL is your String url to your privacy policy.)

In Android Studio select the Project files view, then go to External Libraries> then look for consent library, then click by right mouse button on classes.dex and choose Show in Explorer. Next go to upper folder and there search for assets folder and consetform.html, there are two folders for that library - maybe, for debug and release build? But I just found out it works.
edit: now it not works, due to android studio update, this solution works: https://stackoverflow.com/a/51310779/1555754

I took the ConsentFormClasses.jar and created an own MyConsentFormClasses.jar. In this jar-File I took the class 'ConsentForm.class' and added a new method load(String locale).
Difference to the original method load()is the call of the webiew.
this.webView.loadUrl("file:///android_asset/consentform.html")
changed to
this.webView.loadUrl("file:///android_asset/consentform_"+locale+".html")
The string variable locale I defined as string resource.
In the Assests folder I put the following files e.g consentform_en.html, consentform_fr.html, consentform_pl.html.
Building the new ConsentForm.Class und create MyConsentFormClasses.jar, copied the jar in the libsfolder of my project and configured AndroidStudio dependencies...
The disadvantage to have to make changes in the source code is bearable for me. I hope that Google will provide a proper solution here soon
GGK

Related

BroadcastReceiver Not Firing after boot

Unable to get my Xamarin.Android app to fire Toast after boot. I checked many accepted solutions but none seem to resolve my problem. I've also tried various "working" examples but haven't had any luck so clearly I'm missing something.
Device: Samsung Galaxy S3
API: 19
Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.novak" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
<uses-sdk android:minSdkVersion="16" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application android:label="ReceiverApp">
<receiver android:enabled="true"
android:exported="true"
android-permission="android.permission.RECEIVE_BOOT_COMPLETED"
android:name="com.novak.BootReceiver" >
<intent-filter >
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
</application>
</manifest>
BootReceiver.cs
using Android.App;
using Android.Widget;
using Android.Content;
namespace ReceiverApp
{
[BroadcastReceiver]
[IntentFilter(new[] { Intent.ActionBootCompleted })]
public class BootReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
Toast.MakeText(context, "Receiver", ToastLength.Long).Show();
}
}
}
It would appear that what I want to do is not possible. Once my app is installed the app sits in a stopped state and must be executed manually the first time before it can receive broadcasts
[How to start a Service when .apk is Installed for the first time
P.S: On real device it takes a while to fire the event, ie: 2 minutes after unlock the screen on my Samsung. On emulator it takes very short.
I wrote the code below to notice when it will fire:
Autorun.cs - Just add this file into your project, that's it, it will start after the boot. No manifest modification needed.
using Android;
using Android.App;
using Android.Content;
// we need to ask permission to be notified of these events
[assembly: UsesPermission (Manifest.Permission.ReceiveBootCompleted)]
namespace XamarinCookbook
{
// we want this to fire when the device boots
[BroadcastReceiver]
[IntentFilter (new []{ Intent.ActionBootCompleted })]
public class ServiceStarter : BroadcastReceiver
{
public override void OnReceive (Context context, Intent intent)
{
#region Start chrome
var mesaj = "Autorun started";
Android.Widget.Toast.MakeText(Android.App.Application.Context, mesaj, Android.Widget.ToastLength.Long).Show();
var uri = Android.Net.Uri.Parse("https://500px.com");
var intent1 = new Intent(Intent.ActionView, uri);
intent1.AddFlags(ActivityFlags.NewTask);
intent1.SetPackage("com.android.chrome");
try
{
context.StartActivity(intent1);
}
catch (ActivityNotFoundException ex)
{
//Chrome browser not installed
intent.SetPackage(null);
context.StartActivity(intent1);
}
#endregion
/*
#region Real code
// just start the service
var myIntent = new Intent (context, typeof(XamarinService));
context.StartService (myIntent);
#endregion
*/
}
}
}
VS Solution:
https://drive.google.com/open?id=1iYZQ2YCvBkyym9-2FvU7KXoBKUWsMdlT

How to allow only these orientations in my Cordova/Phonegap app?

I created an iOS (iPhone) App using Cordova and want to only allow the following orientations:
Portrait
Landscape Left
Landscape Right
This also means that "upside down" should not be allowed:
I know that I can set this in Xcode but when ever I start a new Cordova build this setting gets overwritten.
So I checked Cordova docs and found this: http://cordova.apache.org/docs/en/5.1.1/config_ref_index.md.html#The%20config.xml%20File
It says that I can set orientation in config.xml like this:
<preference name="Orientation" value="landscape" />
But I don't see how I can set a more fine granular setting as I described above. How can this be done?
Note: I am on Cordova 5.1.1
You can use config.xml'
<platform name="ios">
<preference name="Orientation" value="all" />
</platform>
along with shouldRotateToOrientation(degrees) callback as stated in the docs like this:
onDeviceReady: function() {
app.receivedEvent('deviceready');
window.shouldRotateToOrientation = function(degrees) {
return degrees !== 180;
};
},
You can use an after_prepare hook which will apply the settings after the cordova prepare and therefore avoid them getting overwritten on each cordova build. Place the following code in <your_project>/hooks/after_prepare/some_file.js:
#!/usr/bin/env node
// Set support for all orienations in iOS .plist - workaround for this cordova bug: https://issues.apache.org/jira/browse/CB-8953
var platforms = process.env.CORDOVA_PLATFORMS.split(',');
platforms.forEach(function(p) {
if (p == "ios") {
var fs = require('fs'),
plist = require('plist'),
xmlParser = new require('xml2js').Parser(),
plistPath = '',
configPath = 'config.xml';
// Construct plist path.
if (fs.existsSync(configPath)) {
var configContent = fs.readFileSync(configPath);
// Callback is synchronous.
xmlParser.parseString(configContent, function (err, result) {
var name = result.widget.name;
plistPath = 'platforms/ios/' + name + '/' + name + '-Info.plist';
});
}
// Change plist and write.
if (fs.existsSync(plistPath)) {
var pl = plist.parseFileSync(plistPath);
configure(pl);
fs.writeFileSync(plistPath, plist.build(pl).toString());
}
process.exit();
}
});
function configure(plist) {
var iPhoneOrientations = [
'UIInterfaceOrientationLandscapeLeft',
'UIInterfaceOrientationLandscapeRight',
'UIInterfaceOrientationPortrait'
];
var iPadOrientations = [
'UIInterfaceOrientationLandscapeLeft',
'UIInterfaceOrientationLandscapeRight',
'UIInterfaceOrientationPortrait'
];
plist["UISupportedInterfaceOrientations"] = iPhoneOrientations;
plist["UISupportedInterfaceOrientations~ipad"] = iPadOrientations;
}
Note: you'll need to install the plist and xml2js node modules if you don't already have them.

in sap fiori application, search bar is working for mock data, not for backend data

in sap fiori application, search bar is working for mock data, not for backend data(sap ABAP(odata))
Filename: Master.Controller.js
onSearch : function() {
this.oInitialLoadFinishedDeferred = jQuery.Deferred();
// Add search filter
var filters = [];
var searchString = this.getView().byId("searchField").getValue();
if (searchString && searchString.length > 0) {
filters = [ new sap.ui.model.Filter("QUARTER_ID", sap.ui.model.FilterOperator.Contains, searchString) ];
}
// Update list binding
this.getView().byId("list").getBinding("items").filter(filters);
//On phone devices, there is nothing to select from the list
if (sap.ui.Device.system.phone) {
return;
}
//Wait for the list to be reloaded
this.waitForInitialListLoading(function () {
//On the empty hash select the first item
this.selectFirstItem();
});
},
filename: Master.view.xml
<subHeader id="masterSubHeader">
<Bar id="searchBar">
<contentMiddle>
<SearchField
id="searchField"
livechange= "onSearch"
width="100%">
</SearchField>
</contentMiddle>
</Bar>
</subHeader>
<content>
<List
id="list"
select="onSelect"
mode="{device>/listMode}"
noDataText="{i18n>masterListNoDataText}"
growing="true"
growingScrollToLoad="true"
items="{/quarterviewSet}">
<items
id="masterList">
<ObjectListItem
id="mainListItem"
press="onSelect"
type="{device>/listItemType}"
counter="0"
title="{QUARTER_ID}"
number="{QTRTYPE_NAME}"
numberUnit="{QUARTER_CATEGORY}"
markFavorite="false"
markFlagged="false"
showMarkers="false">
<attributes>
<ObjectAttribute id="ATTR1" text="{LOCATION}" />
<ObjectAttribute id="ATTR2" text="{CITY}" />
</attributes>
<core:ExtensionPoint
name="extListItemInfo"/>
</ObjectListItem>
</items>
</List>
</content>
You will have to figure out if the data from the backend is same as the mock data. If not, the bindings might have gone wrong. Please check if the data is loaded into the list initially with all the backend data. After that, try to do the search.
Put a break point in the onSearch function and see if the data is still there and how if the filters are created.

Programmatically hide simple pref

I can't seem to figure out how to programmatically hide or show a setting.
I have tried this:
function onSwitchChange(prefName) {
var ms = require("sdk/simple-prefs").prefs.option1;
if(ms == "S"){
require("sdk/simple-prefs").prefs.option2.hidden = false;
}else{
require("sdk/simple-prefs").prefs.option2.hidden = true;
}
}
require("sdk/simple-prefs").on("option1", onSwitchChange);
you need to give your pref an oninputchanged attribute.
see here: MDN :: Inline Options - Setting element changed notifications
it looks like you're using firefox-addon-sdk so after you make your addon to xpi. rename the xpi to zip then extract it. then edit options.xul then re-zip the files, then re-rename it to .xpi.
the edit you need to make to options.xul is find the setting element of option2. then add to it this:
<setting title="option1" type="string" pref="blahBlahBlah" oninputchanged="if (this.value == 'S') { document.querySelector('setting[title=\"option1\"]').style.display='none'; } else { document.querySelector('setting[title=\"option1\"]').style.display=''; } ">
option2
</setting>

actionscript callback() function - URLRequest() causes "Security Sandbox Violation"

* Security Sandbox Violation *
SecurityDomain 'http://loadimage.my.com' tried to access incompatible context 'http://my.com/My.swf'
I am loading an jpg image file in actionscript.
In the callback function I want to addChild, but an "Security Sandbox Violation" is displayed.
public function preloadAll() {
...
// call preLoad with callback function
preLoad(function (slide:Slide):void{
//
// loading this url causes the error *** Security Sandbox Violation ***
//
var url:String = "http://my.com/My.swf";
var urlReq:URLRequest = new URLRequest(url);
var loader:Loader = new Loader()
loader.load(urlReq);
slide.image.addChild(loader);
});
...
}
public function preLoad(callback: Function = null) : void {
this.url = "http://image.my.com/cache/Picture_001.jpg"
var self:Slide = this;
this.image.addEventListener(Event.COMPLETE, function() : void {
// callback when image completes loading
callback(self);
});
this.image.load(this.url);
}
http://my.com/crossdomain.xml
<?xml version="1.0" ?>
<cross-domain-policy>
<site-control permitted-cross-domain-policies="master-only"/>
<allow-access-from domain="*"/>
<allow-access-from domain="" />
<allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>
I'm not sure exactly what you're trying to do inside the code for image, but my guess is that you're trying to access the loader's content, which is obviously a security violation since the SWF is being loaded from a different domain.
There are two ways to access the content of a SWF that has been loaded from a different domain:
Load the SWF in the "current" security domain
Use Security.allowDomain() in the loaded SWF
More details here:
https://stackoverflow.com/a/9547996/579230

Resources