How test code that depends on an Activity - java

I have written code to make screenshots on Android and I am trying to write a test for this. When running the test method, then I receive the following error message: Activity never becomes requested state "[STARTED, CREATED, DESTROYED, RESUMED]" (last lifecycle transition = "PRE_ON_CREATE"). I have a hard time figuring out what I am doing wrong.
The production code has been tested manually.
My test code in src/androidTest/java/org/app:
#RunWith (AndroidJUnit4.class)
public class ScreenshotsCaptorTest {
#Rule
public ActivityScenarioRule<TestActivity> rule =
new ActivityScenarioRule<>(TestActivity.class);
#Test
public void test() {
// This line is never reached
ActivityScenario<TestActivity> scenario = rule.getScenario();
scenario.onActivity(activity -> {
activity.setCallback(() -> {
try {
Bitmap screenshot = ScreenshotsCaptorBuilder.captor.takeScreenshot();
// Assert screenshot is non-null
} catch (CaptureScreenshotFailedException e) {
throw new RuntimeException(e);
}
});
Intent intent = ScreenshotsCaptorBuilder.intent(activity);
activity.startActivityForResult(intent, 100);
});
}
}
My helper Activity in src/androidTest/java/org/app:
import org.app.test.R;
public class TestActivity extends Activity {
private Callback callback;
#Override
public void onCreate(Bundle savedInstanceState) {
// This line is never reached
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
public void setCallback(Callback callback) {
// This line is never reached
this.callback = callback;
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
// This line is never reached
ScreenshotsCaptorBuilder.createCaptor(this, resultCode, intent);
callback.done();
}
public interface Callback {
// This line is never reached
void done();
}
}
The src/androidTest/AndroidManifest.xml file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.app">
<application>
<activity
android:name=".TestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
And src/androidTest/res/layout/activity_test.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
It would be really helpful if someone provide me some suggestions or tips to resolve my issue. Thanks in advance.

Related

Wrong activity displayed when implicit intent received from another app with Action.SEND

I have an app with two activities: MainActivity, which contains a URL entry field where the user can enter a YouTube video URL and press a submit button, to start the second activity, VideoActivity, which displays some information about this video (fetched from another web server).
The app also has a feature to receive intent via the Youtube application. When user presses the share button within the Youtube app, my app appears in the share list. Upon pressing share from the Youtube app, MainActivity should be brought to the front, and the URL should be posted within the MainActivity's URL field.
However, this only happens correctly on the first share. If the app is in the background when user shares from Youtube app, they are taken to whatever the last visible activity was, whether it is MainActivity or VideoActivity, (and even if it is MainActivity, the URL is not posted into the URL field, but the field is left in whatever state it was in when the app was last visible).
Here is my current AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.youcmt.youdmcapp">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
<activity
android:name=".VideoActivity"
android:parentActivityName=".MainActivity"/>
<service
android:name=".FetchVideoService"
android:exported="false"/>
</application>
</manifest>
Here is my MainActivity.java code:
public class MainActivity extends AppCompatActivity {
private ResponseReceiver mReceiver;
private EditText mUrlEditText;
private Button mSearchButton;
private ProgressBar mProgressBar;
#Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
super.onCreate(savedInstanceState);
mUrlEditText = findViewById(R.id.url_search_et);
Intent intent = getIntent();
if (intent.getType()!=null &&
intent.getType().equals("text/plain")) {
Bundle extras = getIntent().getExtras();
String value = extras.getString(Intent.EXTRA_TEXT);
if(value!=null)
{
mUrlEditText.setText(value);
}
}
mProgressBar = findViewById(R.id.progress_bar);
mSearchButton = findViewById(R.id.search_button);
mSearchButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
try {
askForVideo(mUrlEditText.getText().toString());
mSearchButton.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
} catch (Exception e) {
mUrlEditText.setText("");
mUrlEditText.setHint(e.getMessage());
e.printStackTrace();
}
}
});
}
#Override
protected void onResume() {
super.onResume();
//register the ResponseReceiver
mReceiver = new ResponseReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(FETCH_VIDEO_INFO);
registerReceiver(mReceiver, intentFilter);
}
private void askForVideo (String url) throws Exception {
try {
Intent intent = FetchVideoService.newIntent(this, url);
startService(intent);
} catch (Exception e) {
mUrlEditText.setText(e.getMessage());
}
}
public class ResponseReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
int status = intent.getIntExtra(EXTRA_VIDEO_STATUS, FAIL);
mProgressBar.setVisibility(View.INVISIBLE);
mSearchButton.setVisibility(View.VISIBLE);
if(status==FAIL)
{
mUrlEditText.setText("");
mUrlEditText.setHint("Error retrieving video!");
}
else if(status==SUCCESS) {
Video video = intent.getParcelableExtra(EXTRA_VIDEO);
Intent videoActivityIntent =
VideoActivity.newIntent(getApplicationContext(), video);
startActivity(videoActivityIntent);
}
}
}
#Override
protected void onPause() {
unregisterReceiver(mReceiver);
super.onPause();
}
}
I do not think any of the other files will be useful in understanding the problem. Although this seems like something many app creators should have to deal with, I can find no answers to this problem. Please comment if you feel I should add any additional information and thank you in advance for any help!
Update: testing demonstrates that after the first use of "Share" from YouTube (and considering app remains in the background), the MainActivity no longer receives any new intent on further shares. However, my app is still brought to the foreground somehow. This is very confusing to me.
When you share from another app, your MainActivity is brought to the front and onNewIntent() is called on it. You don't override onNewIntent() so you never see the share Intent.

IPC call to service causes NullPointerException when called from main thread but not on button event

I'm trying to create a service with an IPC connection defined in an .aidl file.
The service starts and the service connection can bind to it, but when the IPC API is called from the same thread it causes a NullPointerException.
When the call to the IPC API is put in an event on a button press it works.
How can this be fixed so that the call can be done without having a button event to start it.
My MainActivicty
public class MainActivity extends AppCompatActivity {
public final String TAG = "Main Activity";
IMyAidlInterface iMyAidlInterface;
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
}
public void onServiceDisconnected(ComponentName className) {
iMyAidlInterface = null;
}
};
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent interFaceServiceIntent = new Intent(this, IMyAidlService.class);
bindService(interFaceServiceIntent, mConnection, Context.BIND_AUTO_CREATE);
Intent sigGenIntent = new Intent(this, signalGenerator.class);
startService(sigGenIntent);
Button b = findViewById(R.id.button);
/*
* The following block is what I want to be able to execute. Currently it causes * a NullPointerException
* Caused by: java.lang.NullPointerException: Attempt to invoke interface method * 'int com.aidltest.IMyAidlInterface.getPid()' on a null object reference
*/
try {
iMyAidlInterface.getPid();
} catch (RemoteException e) {
e.printStackTrace();
}
/*
* The block below using the same interface from within a button event works
* without problem.
*/
b.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
iMyAidlInterface.getPid();
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
}
}
For reference my Aidl:
package com.aidltest;
interface IMyAidlInterface {
int getPid();
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
And my Manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aidltest">
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".IMyAidlService"
android:enabled="true"
android:exported="true">
</service>
</application>
</manifest>
This is happening because iMyAidlInterface is null in your onCreate call. It is not set until your connection listener calls you back via onServiceConnected. Binding is an async operation.

How to stop JobService Scheduled to when app is removed from background?

Currently i am working with an application and my app has a feature that the user will be able to click on a Navigate button and my app will start the Google Map. Till now it's fine and i have done it. But the fact where i am stuck is that i want my app to perform some tasks. To achieve that i have used JobService and scheduled it to run after every 5 seconds even when the app is in background.
When the user presses the back button then inside onDestroy method i have cancelled the scheduler. But when the app is removed from the background by sliding or pressing the cross icon the JobService keeps running as the onDestroy method can be called or not by the os when it is removed from the background. How can i stop the scheduled job when the app is removed from the background?
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="javarank.com.serviceinbackground">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyJobService" android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
</manifest>
MyJobService class
public class MyJobService extends JobService {
#Override
public boolean onStartJob(final JobParameters jobParameters) {
Toast.makeText(getApplicationContext(), "Doing job", Toast.LENGTH_SHORT).show();
jobFinished(jobParameters, true);
return false;
}
#Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
}
Here is my MainActivity
public class MainActivity extends AppCompatActivity {
private static final int JOB_ID = 1;
private JobInfo jobInfo;
private JobScheduler scheduler;
private Button navigateButton;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ComponentName componentName = new ComponentName(this, MyJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, componentName);
builder.setPeriodic(5000);
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
// if true this job exists even after a system reboot...
builder.setPersisted(false);
jobInfo = builder.build();
scheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
scheduler.schedule(jobInfo);
navigateButton = (Button) findViewById(R.id.navigate_button);
navigateButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
StringBuffer url = new StringBuffer("https://www.google.com/maps/dir/?api=1");
url.append("&origin=23.755736,90.374627");
url.append("&destination=23.754047,90.371682");
url.append("&travelmode=driving");
Uri gmmIntentUri = Uri.parse(url.toString());
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
startActivity(mapIntent);
}
});
}
#Override
protected void onDestroy() {
Toast.makeText(getApplicationContext(), "Destroy called.", Toast.LENGTH_SHORT).show();
scheduler.cancel(JOB_ID);
super.onDestroy();
}
}
I think you need to override following onStop() method and put stopService() command to stop the JobService.
#Override
protected void onStop() {
// A service can be "started" and/or "bound". In this case, it's "started" by this Activity
// and "bound" to the JobScheduler (also called "Scheduled" by the JobScheduler). This call
// to stopService() won't prevent scheduled jobs to be processed. However, failing
// to call stopService() would keep it alive indefinitely.
stopService(new Intent(this, MyJobService.class));
super.onStop();
}
You can create a new service like
MyService.java
public class MyService extends Service {
public MyService() {
}
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
//stop you jobservice from here
stopSelf();
}
}
and start it from MainActivity.java
startService(new Intent(MainActivity.this,MyService.class));
Android> 7 automatically saves battery power. You must turn on the application's battery saving stop feature.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent intent = new Intent();
String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + packageName));
startActivity(intent);
}
}
add this to AndroidManifest.xml
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
I faced this issue, but I found that after schedule job service, it can't be canceled (From view).
So I turned to stop it inside the job service by calling onStopJob(params)
and it worked.

NotificationListenerService sendBroadcast not working?

I'm trying for well over 10 hours now and I can't seem to think about anyhting else. I tried every possible example on the internet, but to no avail.
I have NotificationMonitor class extending NotificationListenerService and I wanted to send message from this service to main activity(and possible other activities and services in the future) using Intent mechanism. I post code below:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.testpackage.test">
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".NotificationMonitor"
android:label="#string/app_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
</manifest>
MainActivity.java
public class MainActivity extends Activity {
private TextView txtView;
private NotificationReceiver nReceiver;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txtView = (TextView) findViewById(R.id.textView);
//create receiver
nReceiver = new NotificationReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction("com.testpackage.test.NOTIFICATION_MONITOR");
registerReceiver(nReceiver,filter);
}
#Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(nReceiver);
}
public void buttonClicked(View v){
if(v.getId() == R.id.btnTestBroadcast){
//send test intent without category
Log.d("ActivityMain","Button clicked");
Intent i = new Intent("com.testpackage.test.NOTIFICATION_MONITOR");
sendBroadcast(i);
}
}
class NotificationReceiver extends BroadcastReceiver{
#Override
public void onReceive(Context context, Intent intent) {
Log.d("ActivityMain","Intent received: "+intent.getAction()+" has extra: "+intent.hasExtra("info"));
if (intent.hasCategory("com.testpackage.test.TEST_CATEGORY")) {
if (intent.hasExtra("info")) {
txtView.setText(intent.getStringExtra("info"));
}
}
}
}
}
NotificationMonitor.java
public class NotificationMonitor extends NotificationListenerService {
private NotificationMonitorReceiver receiver;
#Override
public void onCreate() {
super.onCreate();
receiver = new NotificationMonitorReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction("com.testpackage.test.NOTIFICATION_MONITOR");
registerReceiver(receiver,filter);
}
#Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}
#Override
public void onNotificationPosted(StatusBarNotification sbn) {
//do something
sendInfo("notification posted");
}
#Override
public void onNotificationRemoved(StatusBarNotification sbn) {
//do something
sendInfo("notification removed");
}
#Override
public void onListenerConnected() {
//service created and listener connected
Log.d("NM","Listener connected!");
sendInfo("listener connected");
}
private void sendInfo(String info) {
Log.d("NM", "sendInfo called!");
Intent i = new Intent("com.testpackage.test.NOTIFICATION_MONITOR");
i.addCategory("com.testpackage.test.TEST_CATEGORY");
i.putExtra("info", info);
sendBroadcast(i);
}
class NotificationMonitorReceiver extends BroadcastReceiver{
#Override
public void onReceive(Context context, Intent intent) {
//no categories intents get replied
Log.d("NM","Intent received: "+intent.getAction()+" has categories: "+(intent.getCategories()!=null));
if (intent.getCategories() == null) {
Intent i = new Intent("com.testpackage.test.NOTIFICATION_MONITOR");
i.addCategory("com.testpackage.test.TEST_CATEGORY");
sendBroadcast(i);
}
}
}
}
After running this app in debug mode of course I need to re-enable notification permissions, so when I do I see in logcat:
10-10 16:22:46.428 7330-7381/com.testpackage.test D/NM: Listener connected!
10-10 16:22:46.428 7330-7381/com.testpackage.test D/NM: sendInfo called!
well, I should receive broadcast in my application, shouldn't I?
After I click button:
10-10 16:22:57.607 7330-7330/com.testpackage.test D/ActivityMain: Button clicked
10-10 16:22:57.612 7330-7330/com.testpackage.test D/ActivityMain: Intent received: com.testpackage.test.NOTIFICATION_MONITOR has extra: false
10-10 16:22:57.619 7330-7330/com.testpackage.test D/NM: Intent received: com.testpackage.test.NOTIFICATION_MONITOR has categories: false
so the Intent is properly created and send from main activity, received back by the same activity and NotificationListenerService, has no categories so should get replied but nothing happens like when sendInfo method is called.
Please help, I have no other ideas about what might be wrong.
edit: I tested with regular services and of course Broadcasts are working just fine. Is there by chance any possibility that you just can't sendBroadcast from this particular extended service class?
Oficially, I'm a moron. Answer is: I didn't set up Category filter in IntentFilter and this is why I received zero properly sent intents from my class. So, long story short, to "fix" this mistake all one needs to do is add:
filter.addCategory("com.testpackage.test.TEST_CATEGORY");
and that's it. Thank you for your attention.

addAccount not called from AbstractAccountAuthenticator implementation from MainActivity

I am following a tutorial for adding a user account to the Android AccountManager.
In my main activity, I have the following method:
private void addNewAccount(String accountType, String authTokenType) {
Log.d(TAG,"addNewAccount called");
final AccountManagerFuture<Bundle> future = mAccountManager.addAccount(accountType, authTokenType, null, null, this, new AccountManagerCallback<Bundle>() {
#Override
public void run(AccountManagerFuture<Bundle> future) {
try {
Bundle bnd = future.getResult();
Log.d("ACME", "AddNewAccount Bundle is " + bnd);
} catch (Exception e) {
e.printStackTrace();
}
}
}, null);
}
This method is being called, as I see the log in the logcat. Now my AbstractAccountAuthenticator implementation is as follows:
public class AcmeAuthenticator extends AbstractAccountAuthenticator {
private String TAG = "AcmeAuthenticator";
private final Context mContext;
public AcmeAuthenticator(Context context) {
super(context);
this.mContext = context;
}
#Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Log.d("acme", TAG + "> addAccount");
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, accountType);
intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType);
intent.putExtra(AuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
The above method is never called. The following is the service I have created for it:
public class AcmeAuthenticatorService extends Service {
#Override
public IBinder onBind(Intent intent) {
AcmeAuthenticator authenticator = new AcmeAuthenticator(this);
return authenticator.getIBinder();
}
}
And my manifest definition is as follows:
<activity android:name="com.exercise.accountmanagerstudy.accountAuthenticator.AuthenticatorActivity" android:label="#string/login_label"/>
<service android:name=".accountAuthenticator.AcmeAuthenticatorService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data android:name="android.accounts.AccountAuthenticator"
android:resource="#xml/authenticator" />
</service>
<!-- client -->
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
<!-- Authenticator -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
I am not getting an compiler errors, the addAccount override in the AbstractAccountAuthenticator implementation is not called. from the main activity addNewAccount method. I have researched around for a few links here and here.
Any help will be appreciated.
Ok, so I finally did figure it out. Apparently, the authenticator.xml file for AcmeAuthenticator has a field called accountType:
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.exercise.accountmanagerstudy"
android:icon="#drawable/ic_launcher"
android:smallIcon="#drawable/ic_launcher"
android:label="#string/label"
android:accountPreferences="#xml/prefs"/>
When I was calling addNewAccount in my main activity, I was supposed to pass the exact value of the accountType in the above mentioned xml as the accountType argument. Phew, that took me quite sometime and hope it helps someone else :-).

Categories

Resources