Skip to content
Snippets Groups Projects
Commit 15922bf9 authored by Geoffrey Métais's avatar Geoffrey Métais
Browse files

Base extensions API for Android

parent fe55ba65
No related branches found
No related tags found
No related merge requests found
Showing
with 852 additions and 1 deletion
/build
/*
* *************************************************************************
* build.gradle.java
* **************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* ***************************************************************************
*/
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion '23.0.2'
defaultConfig {
minSdkVersion 8
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
}
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/dekans/SDK/android-sdk-linux/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
/*
* *************************************************************************
* ApplicationTest.java
* **************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* ***************************************************************************
*/
package org.videolan.vlc.plugin.api;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
\ No newline at end of file
<!--
~ **************************************************************************
~ AndroidManifest.xml.java
~ ***************************************************************************
~ Copyright © 2015 VLC authors and VideoLAN
~ Author: Geoffrey Métais
~
~ This program is free software; you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation; either version 2 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program; if not, write to the Free Software
~ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
~ ***************************************************************************
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.videolan.vlc.plugin.api">
<application android:allowBackup="true" android:label="@string/app_name"
android:supportsRtl="true">
</application>
</manifest>
package org.videolan.vlc.plugin.api;
import org.videolan.vlc.plugin.api.VLCExtensionItem;
import android.net.Uri;
interface IExtensionHost {
// Protocol version 1
oneway void updateList(in List<VLCExtensionItem> items);
oneway void playUri(in Uri uri, String title);
}
package org.videolan.vlc.plugin.api;
import org.videolan.vlc.plugin.api.IExtensionHost;
import org.videolan.vlc.plugin.api.VLCExtensionItem;
interface IExtensionService {
// Protocol version 1
oneway void onInitialize(in IExtensionHost host);
oneway void browse(int intId, String stringId);
}
package org.videolan.vlc.plugin.api;
parcelable VLCExtensionItem;
/*
* *************************************************************************
* VLCExtensionItem.java
* **************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* ***************************************************************************
*/
package org.videolan.vlc.plugin.api;
import android.os.Parcel;
import android.os.Parcelable;
public class VLCExtensionItem implements Parcelable {
public static final int TYPE_DIRECTORY = 0;
public static final int TYPE_VIDEO = 1;
public static final int TYPE_AUDIO = 2;
public static final int TYPE_PLAYLIST = 3;
public static final int TYPE_SUBTITLE = 4;
public static final int TYPE_OTHER_FILE = 5;
String id;
String path;
String title;
String subTitle;
//TODO choose how to deal with icons
String iconUri; // for content provider
int iconType; // Using VLC icons. maybe with iconRes?
public VLCExtensionItem(String id, String path, String title, String subTitle, String mimeType, int iconType) {
this.id = id;
this.path = path;
this.title = title;
this.subTitle = subTitle;
this.iconType = iconType;
}
public VLCExtensionItem() {
}
private VLCExtensionItem(Parcel in) {
readFromParcel(in);
}
public static final Parcelable.Creator<VLCExtensionItem> CREATOR = new
Parcelable.Creator<VLCExtensionItem>() {
public VLCExtensionItem createFromParcel(Parcel in) {
return new VLCExtensionItem(in);
}
public VLCExtensionItem[] newArray(int size) {
return new VLCExtensionItem[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(path);
dest.writeString(title);
dest.writeString(subTitle);
dest.writeString(iconUri);
dest.writeInt(iconType);
}
public void readFromParcel(Parcel in) {
id = in.readString();
path = in.readString();
title = in.readString();
subTitle = in.readString();
iconUri = in.readString();
iconType = in.readInt();
}
}
package org.videolan.vlc.plugin.api;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import java.util.List;
public abstract class VLCExtensionService extends Service{
private static final String TAG = "VLC/ExtensionService";
private static final ComponentName VLC_HOST_SERVICE =
new ComponentName("org.videolan.vlc",
"org.videolan.vlc.plugin.PluginService");
IExtensionHost mHost;
Context mContext = this;
private volatile Looper mServiceLooper;
private volatile Handler mServiceHandler;
protected abstract void browse(int intId, String stringId);
protected abstract void updateList(List<VLCExtensionItem> items);
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread(
"VLCExtension:" + getClass().getSimpleName());
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new Handler(mServiceLooper);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onDestroy() {
super.onDestroy();
mServiceHandler.removeCallbacksAndMessages(null); // remove all callbacks
mServiceLooper.quit();
}
public void playUri(Uri uri, String title) {
try {
mHost.playUri(uri, title);
} catch (RemoteException e) {
e.printStackTrace();
}
}
private final IExtensionService.Stub mBinder = new IExtensionService.Stub() {
@Override
public void onInitialize(IExtensionHost host) throws RemoteException {
mHost = host;
}
@Override
public void browse(final int id, final String text) throws RemoteException {
mServiceHandler.post(new Runnable() {
@Override
public void run() {
VLCExtensionService.this.browse(id, text);
}
});
}
};
}
<!--
~ **************************************************************************
~ strings.xml.java
~ ***************************************************************************
~ Copyright © 2015 VLC authors and VideoLAN
~ Author: Geoffrey Métais
~
~ This program is free software; you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation; either version 2 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program; if not, write to the Free Software
~ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
~ ***************************************************************************
-->
<resources>
<string name="app_name">Plugin API</string>
</resources>
/*
* *************************************************************************
* ExampleUnitTest.java
* **************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* ***************************************************************************
*/
package org.videolan.vlc.plugin.api;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* To work on unit tests, switch the Test Artifact in the Build Variants view.
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
\ No newline at end of file
include ':libvlc'
include ':libvlc', ':api'
include ':vlc-android'
\ No newline at end of file
......@@ -42,6 +42,17 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<permission
android:name="org.videolan.vlc.permission.READ_EXTENSION_DATA"
android:protectionLevel="normal" />
<permission
android:name="org.videolan.vlc.permission.BIND_DATA_CONSUMER"
android:protectionLevel="normal" />
<uses-permission android:name="org.videolan.vlc.permission.READ_EXTENSION_DATA" />
<uses-permission android:name="org.videolan.vlc.permission.BIND_DATA_CONSUMER" />
<application
android:name="org.videolan.vlc.VLCApplication"
android:hardwareAccelerated="true"
......@@ -418,6 +429,7 @@
</intent-filter>
</activity>
<service android:name=".plugin.PluginService" />
<service android:name=".PlaybackService" />
<receiver
android:name=".widget.VLCAppWidgetProviderWhite"
......
......@@ -202,6 +202,7 @@ android {
dependencies {
compile project(':libvlc')
compile project(':api')
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:cardview-v7:23.1.1'
compile 'com.android.support:recyclerview-v7:23.1.1'
......
/*
* *************************************************************************
* pluginListing.java
* **************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* ***************************************************************************
*/
package org.videolan.vlc.plugin;
import android.content.ComponentName;
import android.os.Parcel;
import android.os.Parcelable;
public class ExtensionListing implements Parcelable {
public static final int PARCELABLE_VERSION = 1;
private ComponentName mComponentName;
private int mProtocolVersion;
private boolean mCompatible;
private boolean mWorldReadable;
private String mTitle;
private String mDescription;
private int mIcon;
private ComponentName mSettingsActivity;
public ExtensionListing(){}
@Override
public int describeContents() {
return 0;
}
/**
* Returns the full qualified component name of the extension.
*/
public ComponentName componentName() {
return mComponentName;
}
/**
* Sets the full qualified component name of the extension.
*/
public ExtensionListing componentName(ComponentName componentName) {
mComponentName = componentName;
return this;
}
/**
* Returns the version of the {@link com.google.android.apps.dashclock.api.DashClockExtension}
* protocol used by the extension.
*/
public int protocolVersion() {
return mProtocolVersion;
}
/**
* Sets the version of the {@link com.google.android.apps.dashclock.api.DashClockExtension}
* protocol used by the extension.
*/
public ExtensionListing protocolVersion(int protocolVersion) {
mProtocolVersion = protocolVersion;
return this;
}
/**
* Returns whether this extension is compatible to the host application; that is whether
* the version of the {@link com.google.android.apps.dashclock.api.DashClockExtension}
* protocol used by the extension matches what is used by the host application.
*/
public boolean compatible() {
return mCompatible;
}
/**
* Sets whether this extension is considered compatible to the host application.
*/
public ExtensionListing compatible(boolean compatible) {
mCompatible = compatible;
return this;
}
/**
* Returns if the data of the ExtensionInfo is available to all hosts or only for the
* DashClock app.
*/
public boolean worldReadable() {
return mWorldReadable;
}
/**
* Sets if the data of the ExtensionInfo is available to all hosts or only for the
* DashClock app.
*/
public ExtensionListing worldReadable(boolean worldReadable) {
mWorldReadable = worldReadable;
return this;
}
/**
* Returns the label of the extension.
*/
public String title() {
return mTitle;
}
/**
* Sets the label of the extension.
*/
public ExtensionListing title(String title) {
mTitle = title;
return this;
}
/**
* Returns a description of the extension.
*/
public String description() {
return mDescription;
}
/**
* Sets a description of the extension.
*/
public ExtensionListing description(String description) {
mDescription = description;
return this;
}
/**
* Returns the ID of the drawable resource within the extension's package that represents this
* data. Default 0.
*/
public int icon() {
return mIcon;
}
/**
* Sets the ID of the drawable resource within the extension's package that represents this
* data. Default 0.
*/
public ExtensionListing icon(int icon) {
mIcon = icon;
return this;
}
/**
* Returns the full qualified component name of the settings class to configure
* the extension.
*/
public ComponentName settingsActivity() {
return mSettingsActivity;
}
/**
* Sets the full qualified component name of the settings class to configure
* the extension.
*/
public ExtensionListing settingsActivity(ComponentName settingsActivity) {
this.mSettingsActivity = settingsActivity;
return this;
}
/**
* @see android.os.Parcelable
*/
public static final Creator<ExtensionListing> CREATOR
= new Creator<ExtensionListing>() {
public ExtensionListing createFromParcel(Parcel in) {
return new ExtensionListing(in);
}
public ExtensionListing[] newArray(int size) {
return new ExtensionListing[size];
}
};
private ExtensionListing(Parcel in) {
int parcelableVersion = in.readInt();
// Version 1 below
if (parcelableVersion >= 1) {
mComponentName = ComponentName.readFromParcel(in);
mProtocolVersion = in.readInt();
mCompatible = in.readInt() == 1;
mWorldReadable = in.readInt() == 1;
mTitle = in.readString();
mDescription = in.readString();
mIcon = in.readInt();
boolean hasSettings = in.readInt() == 1;
if (hasSettings) {
mSettingsActivity = ComponentName.readFromParcel(in);
}
}
}
@Override
public void writeToParcel(Parcel parcel, int i) {
/**
* NOTE: When adding fields in the process of updating this API, make sure to bump
* {@link #PARCELABLE_VERSION}.
*/
parcel.writeInt(PARCELABLE_VERSION);
// Version 1 below
mComponentName.writeToParcel(parcel, 0);
parcel.writeInt(mProtocolVersion);
parcel.writeInt(mCompatible ? 1 : 0);
parcel.writeInt(mWorldReadable ? 1 : 0);
parcel.writeString(mTitle);
parcel.writeString(mDescription);
parcel.writeInt(mIcon);
parcel.writeInt(mSettingsActivity != null ? 1 : 0);
if (mSettingsActivity != null) {
mSettingsActivity.writeToParcel(parcel, 0);
}
}
}
/*
* *************************************************************************
* PluginService.java
* **************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* ***************************************************************************
*/
package org.videolan.vlc.plugin;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.videolan.vlc.VLCApplication;
import org.videolan.vlc.media.MediaUtils;
import org.videolan.vlc.media.MediaWrapper;
import org.videolan.vlc.plugin.api.IExtensionHost;
import org.videolan.vlc.plugin.api.IExtensionService;
import org.videolan.vlc.plugin.api.VLCExtensionItem;
import java.util.List;
public class PluginService extends Service {
private static final String TAG = "VLC/PluginService";
public static final String ACTION_EXTENSION = "org.videolan.vlc.Extension";
public static final int PROTOCOLE_VERSION = 1;
private static final int PLAY_MEDIA = 42;
private final IBinder mBinder = new LocalBinder();
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "service onCreate");
getAvailableExtensions();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("LocalService", "Received start id " + startId + ": " + intent);
return START_NOT_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "service onBind");
return mBinder;
}
public class LocalBinder extends Binder {
public PluginService getService() {
return PluginService.this;
}
}
public List<String> getAvailableExtensions() {
PackageManager pm = VLCApplication.getAppContext().getPackageManager();
List<ResolveInfo> resolveInfos = pm.queryIntentServices(
new Intent(ACTION_EXTENSION), PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : resolveInfos) {
ExtensionListing info = new ExtensionListing();
info.componentName(new ComponentName(resolveInfo.serviceInfo.packageName,
resolveInfo.serviceInfo.name));
info.title(resolveInfo.loadLabel(pm).toString());
Bundle metaData = resolveInfo.serviceInfo.metaData;
if (metaData != null) {
info.compatible(metaData.getInt("protocolVersion") == PROTOCOLE_VERSION);
info.worldReadable(metaData.getBoolean("worldReadable", false));
info.description(metaData.getString("description"));
String settingsActivity = metaData.getString("settingsActivity");
if (!TextUtils.isEmpty(settingsActivity)) {
info.settingsActivity(ComponentName.unflattenFromString(
resolveInfo.serviceInfo.packageName + "/" + settingsActivity));
}
}
info.icon(resolveInfo.getIconResource());
//availableExtensions.add(info); TODO
Log.d(TAG, "componentName "+info.componentName().toString());
Log.d(TAG, " - title "+info.title());
Log.d(TAG, " - protocolVersion "+info.protocolVersion());
Log.d(TAG, " - settingsActivity "+info.settingsActivity());
connectService(info);
}
return null;
}
private void connectService(ExtensionListing info) {
final Connection conn = new Connection();
ComponentName cn = info.componentName();
conn.componentName = cn;
conn.hostInterface = makeHostInterface(conn);
conn.serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
conn.ready = true;
conn.binder = IExtensionService.Stub.asInterface(service);
try {
conn.binder.onInitialize(conn.hostInterface);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
unbindService(conn.serviceConnection);
}
};
try {
if (!bindService(new Intent().setComponent(cn), conn.serviceConnection,
Context.BIND_AUTO_CREATE)) {
Log.e(TAG, "Error binding to extension " + cn.flattenToShortString());
// return null;
}
} catch (SecurityException e) {
Log.e(TAG, "Error binding to extension " + cn.flattenToShortString(), e);
// return null;
}
}
private IExtensionHost makeHostInterface(Connection conn) {
return new IExtensionHost.Stub(){
@Override
public void updateList(List<VLCExtensionItem> items) throws RemoteException {
//TODO
}
@Override
public void playUri(Uri uri, String title) throws RemoteException {
Log.d(TAG, "play media "+title);
Log.d(TAG, " - uri is: "+uri);
MediaWrapper media = new MediaWrapper(uri);
media.setTitle(title);
mHandler.obtainMessage(PLAY_MEDIA, media).sendToTarget();
}
};
}
private static class Connection {
boolean ready = false;
ComponentName componentName;
ServiceConnection serviceConnection;
IExtensionService binder;
IExtensionHost hostInterface;
ContentObserver contentObserver;
/**
* Only access on the async thread. The pair is (collapse token, operation)
*/
// final Queue<Pair<Object, Operation>> deferredOps
// = new LinkedList<Pair<Object, Operation>>();
}
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case PLAY_MEDIA:
MediaWrapper media = (MediaWrapper) msg.obj;
MediaUtils.openMediaNoUi(PluginService.this, media);
break;
}
}
};
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment