From 15922bf95105983afff5f417677443f7b6e2fee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geoffrey=20M=C3=A9tais?= Date: Thu, 19 Nov 2015 10:57:26 +0100 Subject: [PATCH] Base extensions API for Android --- api/.gitignore | 1 + api/build.gradle | 48 ++++ api/proguard-rules.pro | 17 ++ .../vlc/plugin/api/ApplicationTest.java | 36 +++ api/src/main/AndroidManifest.xml | 32 +++ .../vlc/plugin/api/IExtensionHost.aidl | 10 + .../vlc/plugin/api/IExtensionService.aidl | 10 + .../vlc/plugin/api/VLCExtensionItem.aidl | 3 + .../vlc/plugin/api/VLCExtensionItem.java | 95 +++++++ .../vlc/plugin/api/VLCExtensionService.java | 82 ++++++ api/src/main/res/values/strings.xml | 26 ++ .../vlc/plugin/api/ExampleUnitTest.java | 38 +++ settings.gradle | 2 +- vlc-android/AndroidManifest.xml | 12 + vlc-android/build.gradle | 1 + .../videolan/vlc/plugin/ExtensionListing.java | 234 ++++++++++++++++++ .../videolan/vlc/plugin/PluginService.java | 206 +++++++++++++++ 17 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 api/.gitignore create mode 100644 api/build.gradle create mode 100644 api/proguard-rules.pro create mode 100644 api/src/androidTest/java/org/videolan/vlc/plugin/api/ApplicationTest.java create mode 100644 api/src/main/AndroidManifest.xml create mode 100644 api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionHost.aidl create mode 100644 api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionService.aidl create mode 100644 api/src/main/aidl/org/videolan/vlc/plugin/api/VLCExtensionItem.aidl create mode 100644 api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionItem.java create mode 100644 api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionService.java create mode 100644 api/src/main/res/values/strings.xml create mode 100644 api/src/test/java/org/videolan/vlc/plugin/api/ExampleUnitTest.java create mode 100644 vlc-android/src/org/videolan/vlc/plugin/ExtensionListing.java create mode 100644 vlc-android/src/org/videolan/vlc/plugin/PluginService.java diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 000000000..765a9fb62 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,48 @@ +/* + * ************************************************************************* + * 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' +} diff --git a/api/proguard-rules.pro b/api/proguard-rules.pro new file mode 100644 index 000000000..ae5f54db3 --- /dev/null +++ b/api/proguard-rules.pro @@ -0,0 +1,17 @@ +# 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 *; +#} diff --git a/api/src/androidTest/java/org/videolan/vlc/plugin/api/ApplicationTest.java b/api/src/androidTest/java/org/videolan/vlc/plugin/api/ApplicationTest.java new file mode 100644 index 000000000..eab5681a0 --- /dev/null +++ b/api/src/androidTest/java/org/videolan/vlc/plugin/api/ApplicationTest.java @@ -0,0 +1,36 @@ +/* + * ************************************************************************* + * 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; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/api/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml new file mode 100644 index 000000000..887ccdf2f --- /dev/null +++ b/api/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionHost.aidl b/api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionHost.aidl new file mode 100644 index 000000000..229d4cf16 --- /dev/null +++ b/api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionHost.aidl @@ -0,0 +1,10 @@ +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 items); + oneway void playUri(in Uri uri, String title); +} diff --git a/api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionService.aidl b/api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionService.aidl new file mode 100644 index 000000000..2e4699d8a --- /dev/null +++ b/api/src/main/aidl/org/videolan/vlc/plugin/api/IExtensionService.aidl @@ -0,0 +1,10 @@ +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); +} diff --git a/api/src/main/aidl/org/videolan/vlc/plugin/api/VLCExtensionItem.aidl b/api/src/main/aidl/org/videolan/vlc/plugin/api/VLCExtensionItem.aidl new file mode 100644 index 000000000..d8e58c302 --- /dev/null +++ b/api/src/main/aidl/org/videolan/vlc/plugin/api/VLCExtensionItem.aidl @@ -0,0 +1,3 @@ +package org.videolan.vlc.plugin.api; + +parcelable VLCExtensionItem; diff --git a/api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionItem.java b/api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionItem.java new file mode 100644 index 000000000..24b7c6065 --- /dev/null +++ b/api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionItem.java @@ -0,0 +1,95 @@ +/* + * ************************************************************************* + * 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 CREATOR = new + Parcelable.Creator() { + 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(); + } +} diff --git a/api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionService.java b/api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionService.java new file mode 100644 index 000000000..7bd127252 --- /dev/null +++ b/api/src/main/java/org/videolan/vlc/plugin/api/VLCExtensionService.java @@ -0,0 +1,82 @@ +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 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); + } + }); + } + }; +} diff --git a/api/src/main/res/values/strings.xml b/api/src/main/res/values/strings.xml new file mode 100644 index 000000000..491820cd1 --- /dev/null +++ b/api/src/main/res/values/strings.xml @@ -0,0 +1,26 @@ + + + + Plugin API + diff --git a/api/src/test/java/org/videolan/vlc/plugin/api/ExampleUnitTest.java b/api/src/test/java/org/videolan/vlc/plugin/api/ExampleUnitTest.java new file mode 100644 index 000000000..2e9d028d6 --- /dev/null +++ b/api/src/test/java/org/videolan/vlc/plugin/api/ExampleUnitTest.java @@ -0,0 +1,38 @@ +/* + * ************************************************************************* + * 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 diff --git a/settings.gradle b/settings.gradle index 1e0edf890..0509c2f55 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':libvlc' +include ':libvlc', ':api' include ':vlc-android' \ No newline at end of file diff --git a/vlc-android/AndroidManifest.xml b/vlc-android/AndroidManifest.xml index ba6f06f17..c872a9bf6 100644 --- a/vlc-android/AndroidManifest.xml +++ b/vlc-android/AndroidManifest.xml @@ -42,6 +42,17 @@ + + + + + + + + CREATOR + = new Creator() { + 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); + } + } +} diff --git a/vlc-android/src/org/videolan/vlc/plugin/PluginService.java b/vlc-android/src/org/videolan/vlc/plugin/PluginService.java new file mode 100644 index 000000000..d36d01bc1 --- /dev/null +++ b/vlc-android/src/org/videolan/vlc/plugin/PluginService.java @@ -0,0 +1,206 @@ +/* + * ************************************************************************* + * 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 getAvailableExtensions() { + PackageManager pm = VLCApplication.getAppContext().getPackageManager(); + List 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 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> deferredOps +// = new LinkedList>(); + } + + 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; + } + } + }; +}