Merge "Add the print service recommendation service" into nyc-dev

This commit is contained in:
Philip P. Moltmann
2016-03-31 01:33:15 +00:00
committed by Android (Google) Code Review
36 changed files with 3098 additions and 93 deletions

View File

@@ -251,10 +251,13 @@ LOCAL_SRC_FILES += \
core/java/android/print/IPrintDocumentAdapterObserver.aidl \
core/java/android/print/IPrintJobStateChangeListener.aidl \
core/java/android/print/IPrintServicesChangeListener.aidl \
core/java/android/printservice/recommendation/IRecommendationsChangeListener.aidl \
core/java/android/print/IPrintManager.aidl \
core/java/android/print/IPrintSpooler.aidl \
core/java/android/print/IPrintSpoolerCallbacks.aidl \
core/java/android/print/IPrintSpoolerClient.aidl \
core/java/android/printservice/recommendation/IRecommendationServiceCallbacks.aidl \
core/java/android/printservice/recommendation/IRecommendationService.aidl \
core/java/android/print/IWriteResultCallback.aidl \
core/java/android/printservice/IPrintService.aidl \
core/java/android/printservice/IPrintServiceClient.aidl \
@@ -565,6 +568,7 @@ aidl_files := \
frameworks/base/core/java/android/print/PrintJobInfo.aidl \
frameworks/base/core/java/android/print/PrinterInfo.aidl \
frameworks/base/core/java/android/print/PrintJobId.aidl \
frameworks/base/core/java/android/printservice/recommendation/RecommendationInfo.aidl \
frameworks/base/core/java/android/hardware/usb/UsbDevice.aidl \
frameworks/base/core/java/android/hardware/usb/UsbInterface.aidl \
frameworks/base/core/java/android/hardware/usb/UsbEndpoint.aidl \

View File

@@ -42,6 +42,7 @@ package android {
field public static final java.lang.String BIND_MIDI_DEVICE_SERVICE = "android.permission.BIND_MIDI_DEVICE_SERVICE";
field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE";
field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
field public static final java.lang.String BIND_PRINT_RECOMMENDATION_SERVICE = "android.permission.BIND_PRINT_RECOMMENDATION_SERVICE";
field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE";
field public static final java.lang.String BIND_QUICK_SETTINGS_TILE = "android.permission.BIND_QUICK_SETTINGS_TILE";
field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS";
@@ -32702,6 +32703,30 @@ package android.printservice {
}
package android.printservice.recommendation {
public final class RecommendationInfo implements android.os.Parcelable {
ctor public RecommendationInfo(java.lang.CharSequence, java.lang.CharSequence, int, boolean);
method public int describeContents();
method public java.lang.CharSequence getName();
method public int getNumDiscoveredPrinters();
method public java.lang.CharSequence getPackageName();
method public boolean recommendsMultiVendorService();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.printservice.recommendation.RecommendationInfo> CREATOR;
}
public abstract class RecommendationService extends android.app.Service {
ctor public RecommendationService();
method public final android.os.IBinder onBind(android.content.Intent);
method public abstract void onConnected();
method public abstract void onDisconnected();
method public final boolean onUnbind(android.content.Intent);
method public final void updateRecommendations(java.util.List<android.printservice.recommendation.RecommendationInfo>);
}
}
package android.provider {
public final class AlarmClock {

View File

@@ -24,9 +24,11 @@ import android.print.IPrintDocumentAdapter;
import android.print.PrintJobId;
import android.print.IPrintJobStateChangeListener;
import android.print.IPrintServicesChangeListener;
import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.PrinterId;
import android.print.PrintJobInfo;
import android.print.PrintAttributes;
import android.printservice.recommendation.RecommendationInfo;
import android.printservice.PrintServiceInfo;
/**
@@ -73,7 +75,6 @@ interface IPrintManager {
* Get the print services.
*
* @param selectionFlags flags selecting which services to get
* @param selectedService if not null, the id of the print service to get
* @param userId the id of the user requesting the services
*
* @return the list of selected print services.
@@ -89,6 +90,37 @@ interface IPrintManager {
*/
void setPrintServiceEnabled(in ComponentName service, boolean isEnabled, int userId);
/**
* Listen for changes to the print service recommendations.
*
* @param listener the listener to add
* @param userId the id of the user listening
*
* @see android.print.PrintManager#getPrintServiceRecommendations
*/
void addPrintServiceRecommendationsChangeListener(in IRecommendationsChangeListener listener,
int userId);
/**
* Stop listening for changes to the print service recommendations.
*
* @param listener the listener to remove
* @param userId the id of the user requesting the removal
*
* @see android.print.PrintManager#getPrintServiceRecommendations
*/
void removePrintServiceRecommendationsChangeListener(in IRecommendationsChangeListener listener,
int userId);
/**
* Get the print service recommendations.
*
* @param userId the id of the user requesting the recommendations
*
* @return the list of selected print services.
*/
List<RecommendationInfo> getPrintServiceRecommendations(int userId);
void createPrinterDiscoverySession(in IPrinterDiscoveryObserver observer, int userId);
void startPrinterDiscovery(in IPrinterDiscoveryObserver observer,
in List<PrinterId> priorityList, int userId);

View File

@@ -36,12 +36,15 @@ import android.os.RemoteException;
import android.print.PrintDocumentAdapter.LayoutResultCallback;
import android.print.PrintDocumentAdapter.WriteResultCallback;
import android.printservice.PrintServiceInfo;
import android.printservice.recommendation.IRecommendationsChangeListener;
import android.printservice.recommendation.RecommendationInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
import libcore.io.IoUtils;
import java.lang.ref.WeakReference;
@@ -113,6 +116,7 @@ public final class PrintManager {
private static final int MSG_NOTIFY_PRINT_JOB_STATE_CHANGED = 1;
private static final int MSG_NOTIFY_PRINT_SERVICES_CHANGED = 2;
private static final int MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED = 3;
/**
* Package name of print spooler.
@@ -202,6 +206,9 @@ public final class PrintManager {
mPrintJobStateChangeListeners;
private Map<PrintServicesChangeListener, PrintServicesChangeListenerWrapper>
mPrintServicesChangeListeners;
private Map<PrintServiceRecommendationsChangeListener,
PrintServiceRecommendationsChangeListenerWrapper>
mPrintServiceRecommendationsChangeListeners;
/** @hide */
public interface PrintJobStateChangeListener {
@@ -223,6 +230,15 @@ public final class PrintManager {
public void onPrintServicesChanged();
}
/** @hide */
public interface PrintServiceRecommendationsChangeListener {
/**
* Callback notifying that the print service recommendations changed.
*/
void onPrintServiceRecommendationsChanged();
}
/**
* Creates a new instance.
*
@@ -260,7 +276,14 @@ public final class PrintManager {
listener.onPrintServicesChanged();
}
} break;
case MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED: {
PrintServiceRecommendationsChangeListenerWrapper wrapper =
(PrintServiceRecommendationsChangeListenerWrapper) message.obj;
PrintServiceRecommendationsChangeListener listener = wrapper.getListener();
if (listener != null) {
listener.onPrintServiceRecommendationsChanged();
}
} break;
}
}
};
@@ -539,13 +562,14 @@ public final class PrintManager {
* @see android.print.PrintManager#getPrintServices
*/
void addPrintServicesChangeListener(@NonNull PrintServicesChangeListener listener) {
Preconditions.checkNotNull(listener);
if (mService == null) {
Log.w(LOG_TAG, "Feature android.software.print not available");
return;
}
if (mPrintServicesChangeListeners == null) {
mPrintServicesChangeListeners = new ArrayMap<PrintServicesChangeListener,
PrintServicesChangeListenerWrapper>();
mPrintServicesChangeListeners = new ArrayMap<>();
}
PrintServicesChangeListenerWrapper wrappedListener =
new PrintServicesChangeListenerWrapper(listener, mHandler);
@@ -565,6 +589,8 @@ public final class PrintManager {
* @see android.print.PrintManager#getPrintServices
*/
void removePrintServicesChangeListener(@NonNull PrintServicesChangeListener listener) {
Preconditions.checkNotNull(listener);
if (mService == null) {
Log.w(LOG_TAG, "Feature android.software.print not available");
return;
@@ -588,7 +614,6 @@ public final class PrintManager {
}
}
/**
* Gets the list of print services, but does not register for updates. The user has to register
* for updates by itself, or use {@link PrintServicesLoader}.
@@ -596,7 +621,7 @@ public final class PrintManager {
* @param selectionFlags flags selecting which services to get. Either
* {@link #ENABLED_SERVICES},{@link #DISABLED_SERVICES}, or both.
*
* @return The enabled service list or an empty list.
* @return The print service list or an empty list.
*
* @see #addPrintServicesChangeListener(PrintServicesChangeListener)
* @see #removePrintServicesChangeListener(PrintServicesChangeListener)
@@ -604,6 +629,8 @@ public final class PrintManager {
* @hide
*/
public @NonNull List<PrintServiceInfo> getPrintServices(int selectionFlags) {
Preconditions.checkFlagsArgument(selectionFlags, ALL_SERVICES);
try {
List<PrintServiceInfo> services = mService.getPrintServices(selectionFlags, mUserId);
if (services != null) {
@@ -615,6 +642,92 @@ public final class PrintManager {
return Collections.emptyList();
}
/**
* Listen for changes to the print service recommendations.
*
* @param listener the listener to add
*
* @see android.print.PrintManager#getPrintServiceRecommendations
*/
void addPrintServiceRecommendationsChangeListener(
@NonNull PrintServiceRecommendationsChangeListener listener) {
Preconditions.checkNotNull(listener);
if (mService == null) {
Log.w(LOG_TAG, "Feature android.software.print not available");
return;
}
if (mPrintServiceRecommendationsChangeListeners == null) {
mPrintServiceRecommendationsChangeListeners = new ArrayMap<>();
}
PrintServiceRecommendationsChangeListenerWrapper wrappedListener =
new PrintServiceRecommendationsChangeListenerWrapper(listener, mHandler);
try {
mService.addPrintServiceRecommendationsChangeListener(wrappedListener, mUserId);
mPrintServiceRecommendationsChangeListeners.put(listener, wrappedListener);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
* Stop listening for changes to the print service recommendations.
*
* @param listener the listener to remove
*
* @see android.print.PrintManager#getPrintServiceRecommendations
*/
void removePrintServiceRecommendationsChangeListener(
@NonNull PrintServiceRecommendationsChangeListener listener) {
Preconditions.checkNotNull(listener);
if (mService == null) {
Log.w(LOG_TAG, "Feature android.software.print not available");
return;
}
if (mPrintServiceRecommendationsChangeListeners == null) {
return;
}
PrintServiceRecommendationsChangeListenerWrapper wrappedListener =
mPrintServiceRecommendationsChangeListeners.remove(listener);
if (wrappedListener == null) {
return;
}
if (mPrintServiceRecommendationsChangeListeners.isEmpty()) {
mPrintServiceRecommendationsChangeListeners = null;
}
wrappedListener.destroy();
try {
mService.removePrintServiceRecommendationsChangeListener(wrappedListener, mUserId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
* Gets the list of print service recommendations, but does not register for updates. The user
* has to register for updates by itself, or use {@link PrintServiceRecommendationsLoader}.
*
* @return The print service recommendations list or an empty list.
*
* @see #addPrintServiceRecommendationsChangeListener
* @see #removePrintServiceRecommendationsChangeListener
*
* @hide
*/
public @NonNull List<RecommendationInfo> getPrintServiceRecommendations() {
try {
List<RecommendationInfo> recommendations =
mService.getPrintServiceRecommendations(mUserId);
if (recommendations != null) {
return recommendations;
}
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
return Collections.emptyList();
}
/**
* @hide
*/
@@ -1242,4 +1355,37 @@ public final class PrintManager {
return mWeakListener.get();
}
}
/**
* @hide
*/
public static final class PrintServiceRecommendationsChangeListenerWrapper extends
IRecommendationsChangeListener.Stub {
private final WeakReference<PrintServiceRecommendationsChangeListener> mWeakListener;
private final WeakReference<Handler> mWeakHandler;
public PrintServiceRecommendationsChangeListenerWrapper(
PrintServiceRecommendationsChangeListener listener, Handler handler) {
mWeakListener = new WeakReference<>(listener);
mWeakHandler = new WeakReference<>(handler);
}
@Override
public void onRecommendationsChanged() {
Handler handler = mWeakHandler.get();
PrintServiceRecommendationsChangeListener listener = mWeakListener.get();
if (handler != null && listener != null) {
handler.obtainMessage(MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED,
this).sendToTarget();
}
}
public void destroy() {
mWeakListener.clear();
}
public PrintServiceRecommendationsChangeListener getListener() {
return mWeakListener.get();
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.print;
import android.annotation.NonNull;
import android.content.Context;
import android.content.Loader;
import android.os.Handler;
import android.os.Message;
import android.printservice.recommendation.RecommendationInfo;
import com.android.internal.util.Preconditions;
import java.util.List;
/**
* Loader for the list of print service recommendations.
*
* @hide
*/
public class PrintServiceRecommendationsLoader extends Loader<List<RecommendationInfo>> {
/** The print manager to be used by this object */
private final @NonNull PrintManager mPrintManager;
/** Handler to sequentialize the delivery of the results to the main thread */
private final Handler mHandler;
/** Listens for updates to the data from the platform */
private PrintManager.PrintServiceRecommendationsChangeListener mListener;
/**
* Create a new PrintServicesLoader.
*
* @param printManager The print manager supplying the data
* @param context Context of the using object
*/
public PrintServiceRecommendationsLoader(@NonNull PrintManager printManager,
@NonNull Context context) {
super(Preconditions.checkNotNull(context));
mHandler = new MyHandler();
mPrintManager = Preconditions.checkNotNull(printManager);
}
@Override
protected void onForceLoad() {
queueNewResult();
}
/**
* Read the print service recommendations and queue it to be delivered on the main thread.
*/
private void queueNewResult() {
Message m = mHandler.obtainMessage(0);
m.obj = mPrintManager.getPrintServiceRecommendations();
mHandler.sendMessage(m);
}
@Override
protected void onStartLoading() {
mListener = new PrintManager.PrintServiceRecommendationsChangeListener() {
@Override
public void onPrintServiceRecommendationsChanged() {
queueNewResult();
}
};
mPrintManager.addPrintServiceRecommendationsChangeListener(mListener);
// Immediately deliver a result
deliverResult(mPrintManager.getPrintServiceRecommendations());
}
@Override
protected void onStopLoading() {
if (mListener != null) {
mPrintManager.removePrintServiceRecommendationsChangeListener(mListener);
mListener = null;
}
if (mHandler != null) {
mHandler.removeMessages(0);
}
}
@Override
protected void onReset() {
onStopLoading();
}
/**
* Handler to sequentialize all the updates to the main thread.
*/
private class MyHandler extends Handler {
/**
* Create a new handler on the main thread.
*/
public MyHandler() {
super(getContext().getMainLooper());
}
@Override
public void handleMessage(Message msg) {
if (isStarted()) {
deliverResult((List<RecommendationInfo>) msg.obj);
}
}
}
}

View File

@@ -22,6 +22,7 @@ import android.content.Loader;
import android.os.Handler;
import android.os.Message;
import android.printservice.PrintServiceInfo;
import com.android.internal.util.Preconditions;
import java.util.List;
@@ -46,13 +47,16 @@ public class PrintServicesLoader extends Loader<List<PrintServiceInfo>> {
/**
* Create a new PrintServicesLoader.
*
* @param printManager The print manager supplying the data
* @param context Context of the using object
* @param selectionFlags What type of services to load.
*/
public PrintServicesLoader(@NonNull PrintManager printManager, @NonNull Context context,
int selectionFlags) {
super(context);
mPrintManager = printManager;
mSelectionFlags = selectionFlags;
super(Preconditions.checkNotNull(context));
mPrintManager = Preconditions.checkNotNull(printManager);
mSelectionFlags = Preconditions.checkFlagsArgument(selectionFlags,
PrintManager.ALL_SERVICES);
}
@Override

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.printservice.recommendation;
import android.printservice.recommendation.IRecommendationServiceCallbacks;
/**
* Interface for communication with the print service recommendation service.
*
* @see android.print.IPrintServiceRecommendationServiceCallbacks
*
* @hide
*/
oneway interface IRecommendationService {
void registerCallbacks(in IRecommendationServiceCallbacks callbacks);
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.printservice.recommendation;
import android.printservice.recommendation.RecommendationInfo;
/**
* Callbacks for communication with the print service recommendation service.
*
* @see android.print.IPrintServiceRecommendationService
*
* @hide
*/
oneway interface IRecommendationServiceCallbacks {
/**
* Update the print service recommendations.
*
* @param recommendations the new print service recommendations
*/
void onRecommendationsUpdated(in List<RecommendationInfo> recommendations);
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.printservice.recommendation;
/**
* Interface for observing changes of the print service recommendations.
*
* @hide
*/
oneway interface IRecommendationsChangeListener {
void onRecommendationsChanged();
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) 2016, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.printservice.recommendation;
/**
* @hide
*/
parcelable RecommendationInfo;

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.printservice.recommendation;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.printservice.PrintService;
import com.android.internal.util.Preconditions;
/**
* A recommendation to install a {@link PrintService print service}.
*
* @hide
*/
@SystemApi
public final class RecommendationInfo implements Parcelable {
/** Package name of the print service. */
private @NonNull final CharSequence mPackageName;
/** Display name of the print service. */
private @NonNull final CharSequence mName;
/** Number of printers the print service would discover if installed. */
private @IntRange(from = 0) final int mNumDiscoveredPrinters;
/** If the service detects printer from multiple vendors. */
private final boolean mRecommendsMultiVendorService;
/**
* Create a new recommendation.
*
* @param packageName Package name of the print service
* @param name Display name of the print service
* @param numDiscoveredPrinters Number of printers the print service would discover if
* installed
* @param recommendsMultiVendorService If the service detects printer from multiple vendor
*/
public RecommendationInfo(@NonNull CharSequence packageName, @NonNull CharSequence name,
@IntRange(from = 0) int numDiscoveredPrinters, boolean recommendsMultiVendorService) {
mPackageName = Preconditions.checkStringNotEmpty(packageName);
mName = Preconditions.checkStringNotEmpty(name);
mNumDiscoveredPrinters = Preconditions.checkArgumentNonnegative(numDiscoveredPrinters);
mRecommendsMultiVendorService = recommendsMultiVendorService;
}
/**
* Create a new recommendation from a parcel.
*
* @param parcel The parcel containing the data
*
* @see #CREATOR
*/
private RecommendationInfo(@NonNull Parcel parcel) {
this(parcel.readCharSequence(), parcel.readCharSequence(), parcel.readInt(),
parcel.readByte() != 0);
}
/**
* @return The package name the recommendations recommends.
*/
public CharSequence getPackageName() {
return mPackageName;
}
/**
* @return Whether the recommended print service detects printers of more than one vendor.
*/
public boolean recommendsMultiVendorService() {
return mRecommendsMultiVendorService;
}
/**
* @return The number of printer the print service would detect.
*/
public int getNumDiscoveredPrinters() {
return mNumDiscoveredPrinters;
}
/**
* @return The name of the recommended print service.
*/
public CharSequence getName() {
return mName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeCharSequence(mPackageName);
dest.writeCharSequence(mName);
dest.writeInt(mNumDiscoveredPrinters);
dest.writeByte((byte) (mRecommendsMultiVendorService ? 1 : 0));
}
/**
* Utility class used to create new print service recommendation objects from parcels.
*
* @see #RecommendationInfo(Parcel)
*/
public static final Creator<RecommendationInfo> CREATOR =
new Creator<RecommendationInfo>() {
@Override
public RecommendationInfo createFromParcel(Parcel in) {
return new RecommendationInfo(in);
}
@Override
public RecommendationInfo[] newArray(int size) {
return new RecommendationInfo[size];
}
};
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.printservice.recommendation;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import java.util.List;
/**
* Base class for the print service recommendation services.
*
* @hide
*/
@SystemApi
public abstract class RecommendationService extends Service {
private static final String LOG_TAG = "PrintServiceRecS";
/** Used to push onConnect and onDisconnect on the main thread */
private Handler mHandler;
/**
* The {@link Intent} action that must be declared as handled by a service in its manifest for
* the system to recognize it as a print service recommendation service.
*
* @hide
*/
public static final String SERVICE_INTERFACE =
"android.printservice.recommendation.RecommendationService";
/** Registered callbacks, only modified on main thread */
private IRecommendationServiceCallbacks mCallbacks;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
mHandler = new MyHandler();
}
/**
* Update the print service recommendations.
*
* @param recommendations The new set of recommendations
*/
public final void updateRecommendations(@Nullable List<RecommendationInfo> recommendations) {
mHandler.obtainMessage(MyHandler.MSG_UPDATE, recommendations).sendToTarget();
}
@Override
public final IBinder onBind(Intent intent) {
return new IRecommendationService.Stub() {
@Override
public void registerCallbacks(IRecommendationServiceCallbacks callbacks) {
// The callbacks come in order of the caller on oneway calls. Hence while the caller
// cannot know at what time the connection is made, he can know the ordering of
// connection and disconnection.
//
// Similar he cannot know when the disconnection is processed, hence he has to
// handle callbacks after calling disconnect.
if (callbacks != null) {
mHandler.obtainMessage(MyHandler.MSG_CONNECT, callbacks).sendToTarget();
} else {
mHandler.obtainMessage(MyHandler.MSG_DISCONNECT).sendToTarget();
}
}
};
}
/**
* Called when the client connects to the recommendation service.
*/
public abstract void onConnected();
/**
* Called when the client disconnects from the recommendation service.
*/
public abstract void onDisconnected();
private class MyHandler extends Handler {
static final int MSG_CONNECT = 1;
static final int MSG_DISCONNECT = 2;
static final int MSG_UPDATE = 3;
MyHandler() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CONNECT:
mCallbacks = (IRecommendationServiceCallbacks) msg.obj;
onConnected();
break;
case MSG_DISCONNECT:
onDisconnected();
mCallbacks = null;
break;
case MSG_UPDATE:
// Note that there might be a connection change in progress. In this case the
// message is handled as before the change. This is acceptable as the caller of
// the connection change has not guarantee when the connection change binder
// transaction is actually processed.
try {
mCallbacks.onRecommendationsUpdated((List<RecommendationInfo>) msg.obj);
} catch (RemoteException | NullPointerException e) {
Log.e(LOG_TAG, "Could not update recommended services", e);
}
break;
}
}
}
}

View File

@@ -56,7 +56,7 @@ public class Preconditions {
* @return the string reference that was validated
* @throws IllegalArgumentException if {@code string} is empty
*/
public static @NonNull String checkStringNotEmpty(final String string) {
public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string) {
if (TextUtils.isEmpty(string)) {
throw new IllegalArgumentException();
}
@@ -73,7 +73,7 @@ public class Preconditions {
* @return the string reference that was validated
* @throws IllegalArgumentException if {@code string} is empty
*/
public static @NonNull String checkStringNotEmpty(final String string,
public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string,
final Object errorMessage) {
if (TextUtils.isEmpty(string)) {
throw new IllegalArgumentException(String.valueOf(errorMessage));
@@ -141,13 +141,17 @@ public class Preconditions {
/**
* Check the requested flags, throwing if any requested flags are outside
* the allowed set.
*
* @return the validated requested flags.
*/
public static void checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
public static int checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
if ((requestedFlags & allowedFlags) != requestedFlags) {
throw new IllegalArgumentException("Requested flags 0x"
+ Integer.toHexString(requestedFlags) + ", but only 0x"
+ Integer.toHexString(allowedFlags) + " are allowed");
}
return requestedFlags;
}
/**
@@ -167,6 +171,22 @@ public class Preconditions {
return value;
}
/**
* Ensures that that the argument numeric value is non-negative.
*
* @param value a numeric int value
*
* @return the validated numeric value
* @throws IllegalArgumentException if {@code value} was negative
*/
public static @IntRange(from = 0) int checkArgumentNonnegative(final int value) {
if (value < 0) {
throw new IllegalArgumentException();
}
return value;
}
/**
* Ensures that that the argument numeric value is non-negative.
*

View File

@@ -2172,6 +2172,15 @@
<permission android:name="android.permission.BIND_PRINT_SERVICE"
android:protectionLevel="signature" />
<!-- Must be required by a {@link android.printservice.recommendation.RecommendationService},
to ensure that only the system can bind to it.
@hide
@SystemApi
<p>Protection level: signature
-->
<permission android:name="android.permission.BIND_PRINT_RECOMMENDATION_SERVICE"
android:protectionLevel="signature" />
<!-- Must be required by a {@link android.nfc.cardemulation.HostApduService}
or {@link android.nfc.cardemulation.OffHostApduService} to ensure that only
the system can bind to it.

View File

@@ -31,6 +31,7 @@ import android.print.PrintAttributes.Margins;
import android.print.PrintAttributes.MediaSize;
import android.print.PrintAttributes.Resolution;
import android.printservice.PrintServiceInfo;
import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.mockservice.MockPrintService;
import android.print.mockservice.PrintServiceCallbacks;
@@ -181,6 +182,17 @@ public class IPrintManagerParametersTest extends BasePrintTest {
new Handler(Looper.getMainLooper()));
}
/**
* Create a IPrintServiceRecommendationsChangeListener object.
*
* @return the object
* @throws Exception if the object could not be created.
*/
private IRecommendationsChangeListener
createMockIPrintServiceRecommendationsChangeListener() throws Exception {
return new PrintManager.PrintServiceRecommendationsChangeListenerWrapper(null,
new Handler(Looper.getMainLooper()));
}
/**
* Create a IPrinterDiscoveryObserver object.
@@ -558,6 +570,61 @@ public class IPrintManagerParametersTest extends BasePrintTest {
// Cannot test bad user Id as these tests are allowed to call across users
}
/**
* test IPrintManager.addPrintServiceRecommendationsChangeListener
*/
@MediumTest
public void testAddPrintServiceRecommendationsChangeListener() throws Exception {
final IRecommendationsChangeListener listener =
createMockIPrintServiceRecommendationsChangeListener();
mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId);
assertException(new Invokable() {
@Override
public void run() throws Exception {
mIPrintManager.addPrintServiceRecommendationsChangeListener(null, mUserId);
}
}, NullPointerException.class);
// Cannot test bad user Id as these tests are allowed to call across users
}
/**
* test IPrintManager.removePrintServicesChangeListener
*/
@MediumTest
public void testRemovePrintServiceRecommendationsChangeListener() throws Exception {
final IRecommendationsChangeListener listener =
createMockIPrintServiceRecommendationsChangeListener();
mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId);
mIPrintManager.removePrintServiceRecommendationsChangeListener(listener, mUserId);
// Removing unknown listeners is a no-op
mIPrintManager.removePrintServiceRecommendationsChangeListener(listener, mUserId);
mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId);
assertException(new Invokable() {
@Override
public void run() throws Exception {
mIPrintManager.removePrintServiceRecommendationsChangeListener(null, mUserId);
}
}, NullPointerException.class);
// Cannot test bad user Id as these tests are allowed to call across users
}
/**
* test IPrintManager.getPrintServiceRecommendations
*/
@MediumTest
public void testGetPrintServiceRecommendations() throws Exception {
mIPrintManager.getPrintServiceRecommendations(mUserId);
// Cannot test bad user Id as these tests are allowed to call across users
}
/**
* test IPrintManager.createPrinterDiscoverySession
*/

View File

@@ -0,0 +1,29 @@
# Copyright (C) 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := PrintRecommendationService
include $(BUILD_PACKAGE)
LOCAL_SDK_VERSION := system_current
include $(call all-makefiles-under, $(LOCAL_PATH))

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (c) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.printservice.recommendation">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowClearUserData="false"
android:label="@string/app_label"
android:allowBackup= "false">
<service
android:name=".RecommendationServiceImpl"
android:permission="android.permission.BIND_PRINT_RECOMMENDATION_SERVICE">
<intent-filter>
<action android:name="android.printservice.recommendation.RecommendationService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,190 @@
Copyright (c) 2005-2008, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_label">Print Service Recommendation Service</string>
</resources>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
(c) Copyright 2016 Mopria Alliance, Inc.
(c) Copyright 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="plugin_vendor_hp">HP</string>
<string name="plugin_vendor_lexmark">Lexmark</string>
<string name="plugin_vendor_brother">Brother</string>
<string name="plugin_vendor_canon">Canon</string>
<string name="plugin_vendor_xerox">Xerox</string>
<string name="plugin_vendor_samsung">Samsung Electorics</string>
<string name="plugin_vendor_epson">Epson</string>
<string name="plugin_vendor_konika_minolta">Konika Minolta</string>
<string name="plugin_vendor_fuji">Fuji</string>
</resources>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
(c) Copyright 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vendors>
<vendor>
<name>@string/plugin_vendor_hp</name>
<package>com.hp.android.printservice</package>
<mdns-names>
<mdns-name>HP</mdns-name>
<mdns-name>Hewlett-Packard</mdns-name>
<mdns-name>Hewlett Packard</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_lexmark</name>
<package>com.lexmark.print.plugin</package>
<mdns-names>
<mdns-name>Lexmark</mdns-name>
<mdns-name>Lexmark International</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_brother</name>
<package>com.brother.printservice</package>
<mdns-names>
<mdns-name>Brother</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_canon</name>
<package>com.xerox.printservice</package>
<mdns-names>
<mdns-name>Canon</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_xerox</name>
<package>jp.co.canon.android.printservice.plugin</package>
<mdns-names>
<mdns-name>Xerox</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_samsung</name>
<package>com.sec.app.samsungprintservice</package>
<mdns-names>
<mdns-name>Samsung</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_epson</name>
<package>com.epson.mobilephone.android.epsonprintserviceplugin</package>
<mdns-names>
<mdns-name>Epson</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_konika_minolta</name>
<package>com.kmbt.printservice</package>
<mdns-names>
<mdns-name>kmkmkm</mdns-name>
<mdns-name>Konica Minolta</mdns-name>
<mdns-name>Minolta</mdns-name>
</mdns-names>
</vendor>
<vendor>
<name>@string/plugin_vendor_fuji</name>
<package>jp.co.fujixerox.prt.PrintUtil.PCL</package>
<mdns-names>
<mdns-name>FUJI XEROX</mdns-name>
</mdns-names>
</vendor>
</vendors>

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.printservice.recommendation;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.StringRes;
/**
* Interface to be implemented by each print service plugin.
* <p/>
* A print service plugin is a minimal version of a real {@link android.printservice.PrintService
* print service}. You cannot print using the plugin. The only functionality in the plugin is to
* report the number of printers that the real service would discover.
*/
public interface PrintServicePlugin {
/**
* Call back used by the print service plugins.
*/
interface PrinterDiscoveryCallback {
/**
* Announce that something changed and the UI for this plugin should be updated.
*
* @param numDiscoveredPrinters The number of printers discovered.
*/
void onChanged(@IntRange(from = 0) int numDiscoveredPrinters);
}
/**
* Get the name (a string reference) of the {@link android.printservice.PrintService print
* service} with the {@link #getPackageName specified package name}. This is read once, hence
* returning different data at different times is not allowed.
*
* @return The name of the print service as a string reference. The localization is handled
* outside of the plugin.
*/
@StringRes int getName();
/**
* The package name of the full print service.
*
* @return The package name
*/
@NonNull CharSequence getPackageName();
/**
* Start the discovery plugin.
*
* @param callback Callbacks used by this plugin.
*
* @throws Exception If anything went wrong when starting the plugin
*/
void start(@NonNull PrinterDiscoveryCallback callback) throws Exception;
/**
* Stop the plugin. This can only return once the plugin is completely finished and cleaned up.
*
* @throws Exception If anything went wrong while stopping plugin
*/
void stop() throws Exception;
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.printservice.recommendation;
import android.content.res.Configuration;
import android.printservice.recommendation.RecommendationInfo;
import android.printservice.recommendation.RecommendationService;
import android.printservice.PrintService;
import android.util.Log;
import com.android.printservice.recommendation.plugin.mdnsFilter.MDNSFilterPlugin;
import com.android.printservice.recommendation.plugin.mdnsFilter.VendorConfig;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
/**
* Service that recommends {@link PrintService print services} that might be a good idea to install.
*/
public class RecommendationServiceImpl extends RecommendationService
implements RemotePrintServicePlugin.OnChangedListener {
private static final String LOG_TAG = "PrintServiceRecService";
/** All registered plugins */
private ArrayList<RemotePrintServicePlugin> mPlugins;
@Override
public void onConnected() {
mPlugins = new ArrayList<>();
try {
for (VendorConfig config : VendorConfig.getAllConfigs(this)) {
try {
mPlugins.add(new RemotePrintServicePlugin(new MDNSFilterPlugin(this,
config.name, config.packageName, config.mDNSNames), this, false));
} catch (Exception e) {
Log.e(LOG_TAG, "Could not initiate simple MDNS plugin for " +
config.packageName, e);
}
}
} catch (IOException | XmlPullParserException e) {
new RuntimeException("Could not parse vendorconfig", e);
}
final int numPlugins = mPlugins.size();
for (int i = 0; i < numPlugins; i++) {
try {
mPlugins.get(i).start();
} catch (RemotePrintServicePlugin.PluginException e) {
Log.e(LOG_TAG, "Could not start plugin", e);
}
}
}
@Override
public void onDisconnected() {
final int numPlugins = mPlugins.size();
for (int i = 0; i < numPlugins; i++) {
try {
mPlugins.get(i).stop();
} catch (RemotePrintServicePlugin.PluginException e) {
Log.e(LOG_TAG, "Could not stop plugin", e);
}
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
// Need to update plugin names as they might be localized
onChanged();
}
@Override
public void onChanged() {
ArrayList<RecommendationInfo> recommendations = new ArrayList<>();
final int numPlugins = mPlugins.size();
for (int i = 0; i < numPlugins; i++) {
RemotePrintServicePlugin plugin = mPlugins.get(i);
try {
int numPrinters = plugin.getNumPrinters();
if (numPrinters > 0) {
recommendations.add(new RecommendationInfo(plugin.packageName,
getString(plugin.name), numPrinters,
plugin.recommendsMultiVendorService));
}
} catch (Exception e) {
Log.e(LOG_TAG, "Could not read state of plugin for " + plugin.packageName, e);
}
}
updateRecommendations(recommendations);
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.printservice.recommendation;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.StringRes;
import com.android.internal.util.Preconditions;
/**
* Wrapper for a {@link PrintServicePlugin}, isolating issues with the plugin as good as possible
* from the {@link RecommendationServiceImpl service}.
*/
class RemotePrintServicePlugin implements PrintServicePlugin.PrinterDiscoveryCallback {
/** Lock for this object */
private final Object mLock = new Object();
/** The name of the print service. */
public final @StringRes int name;
/** If the print service if for more than a single vendor */
public final boolean recommendsMultiVendorService;
/** The package name of the full print service */
public final @NonNull CharSequence packageName;
/** Wrapped plugin */
private final @NonNull PrintServicePlugin mPlugin;
/** The number of printers discovered by the plugin */
private @IntRange(from = 0) int mNumPrinters;
/** If the plugin is started by not yet stopped */
private boolean isRunning;
/** Listener for changes to {@link #mNumPrinters}. */
private @NonNull OnChangedListener mListener;
/**
* Create a new remote for a {@link PrintServicePlugin plugin}.
*
* @param plugin The plugin to be wrapped
* @param listener The listener to be notified about changes in this plugin
* @param recommendsMultiVendorService If the plugin detects printers of more than a single
* vendor
*
* @throws PluginException If the plugin has issues while caching basic stub properties
*/
public RemotePrintServicePlugin(@NonNull PrintServicePlugin plugin,
@NonNull OnChangedListener listener, boolean recommendsMultiVendorService)
throws PluginException {
mListener = listener;
mPlugin = plugin;
this.recommendsMultiVendorService = recommendsMultiVendorService;
// We handle any throwable to isolate our self from bugs in the plugin code.
// Cache simple properties to avoid having to deal with exceptions later in the code.
try {
name = Preconditions.checkArgumentPositive(mPlugin.getName(), "name");
packageName = Preconditions.checkStringNotEmpty(mPlugin.getPackageName(),
"packageName");
} catch (Throwable e) {
throw new PluginException(mPlugin, "Cannot cache simple properties ", e);
}
isRunning = false;
}
/**
* Start the plugin. From now on there might be callbacks to the registered listener.
*/
public void start()
throws PluginException {
// We handle any throwable to isolate our self from bugs in the stub code
try {
synchronized (mLock) {
isRunning = true;
mPlugin.start(this);
}
} catch (Throwable e) {
throw new PluginException(mPlugin, "Cannot start", e);
}
}
/**
* Stop the plugin. From this call on there will not be any more callbacks.
*/
public void stop() throws PluginException {
// We handle any throwable to isolate our self from bugs in the stub code
try {
synchronized (mLock) {
mPlugin.stop();
isRunning = false;
}
} catch (Throwable e) {
throw new PluginException(mPlugin, "Cannot stop", e);
}
}
/**
* Get the current number of printers reported by the stub.
*
* @return The number of printers reported by the stub.
*/
public @IntRange(from = 0) int getNumPrinters() {
return mNumPrinters;
}
@Override
public void onChanged(@IntRange(from = 0) int numDiscoveredPrinters) {
synchronized (mLock) {
Preconditions.checkState(isRunning);
mNumPrinters = Preconditions.checkArgumentNonnegative(numDiscoveredPrinters,
"numDiscoveredPrinters");
if (mNumPrinters > 0) {
mListener.onChanged();
}
}
}
/**
* Listener to listen for changes to {@link #getNumPrinters}
*/
public interface OnChangedListener {
void onChanged();
}
/**
* Exception thrown if the stub has any issues.
*/
public class PluginException extends Exception {
private PluginException(PrintServicePlugin plugin, String message, Throwable e) {
super(plugin + ": " + message, e);
}
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.printservice.recommendation.plugin.mdnsFilter;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.printservice.recommendation.PrintServicePlugin;
import com.android.printservice.recommendation.util.MDNSUtils;
import com.android.printservice.recommendation.util.NsdResolveQueue;
import java.util.HashSet;
import java.util.List;
/**
* A plugin listening for mDNS results and only adding the ones that {@link
* MDNSUtils#isVendorPrinter match} configured list
*/
public class MDNSFilterPlugin implements PrintServicePlugin, NsdManager.DiscoveryListener {
private static final String LOG_TAG = "MDNSFilterPlugin";
private static final String PRINTER_SERVICE_TYPE = "_ipp._tcp";
/** Name of the print service this plugin is for */
private final @StringRes int mName;
/** Package name of the print service this plugin is for */
private final @NonNull CharSequence mPackageName;
/** mDNS names handled by the print service this plugin is for */
private final @NonNull HashSet<String> mMDNSNames;
/** Printer identifiers of the mPrinters found. */
@GuardedBy("mLock")
private final @NonNull HashSet<String> mPrinters;
/** Context of the user of this plugin */
private final @NonNull Context mContext;
/**
* Call back to report the number of mPrinters found.
*
* We assume that {@link #start} and {@link #stop} are never called in parallel, hence it is
* safe to not synchronize access to this field.
*/
private @Nullable PrinterDiscoveryCallback mCallback;
/** Queue used to resolve nsd infos */
private final @NonNull NsdResolveQueue mResolveQueue;
/**
* Create new stub that assumes that a print service can be used to print on all mPrinters
* matching some mDNS names.
*
* @param context The context the plugin runs in
* @param name The user friendly name of the print service
* @param packageName The package name of the print service
* @param mDNSNames The mDNS names of the printer.
*/
public MDNSFilterPlugin(@NonNull Context context, @NonNull String name,
@NonNull CharSequence packageName, @NonNull List<String> mDNSNames) {
mContext = Preconditions.checkNotNull(context, "context");
mName = mContext.getResources().getIdentifier(Preconditions.checkStringNotEmpty(name,
"name"), null, mContext.getPackageName());
mPackageName = Preconditions.checkStringNotEmpty(packageName);
mMDNSNames = new HashSet<>(Preconditions
.checkCollectionNotEmpty(Preconditions.checkCollectionElementsNotNull(mDNSNames,
"mDNSNames"), "mDNSNames"));
mResolveQueue = NsdResolveQueue.getInstance();
mPrinters = new HashSet<>();
}
@Override
public @NonNull CharSequence getPackageName() {
return mPackageName;
}
/**
* @return The NDS manager
*/
private NsdManager getNDSManager() {
return (NsdManager) mContext.getSystemService(Context.NSD_SERVICE);
}
@Override
public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
mCallback = callback;
getNDSManager().discoverServices(PRINTER_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
this);
}
@Override
public @StringRes int getName() {
return mName;
}
@Override
public void stop() throws Exception {
mCallback.onChanged(0);
mCallback = null;
getNDSManager().stopServiceDiscovery(this);
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
Log.w(LOG_TAG, "Failed to start network discovery for type " + serviceType + ": "
+ errorCode);
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
Log.w(LOG_TAG, "Failed to stop network discovery for type " + serviceType + ": "
+ errorCode);
}
@Override
public void onDiscoveryStarted(String serviceType) {
// empty
}
@Override
public void onDiscoveryStopped(String serviceType) {
mPrinters.clear();
}
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
mResolveQueue.resolve(getNDSManager(), serviceInfo,
new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
Log.w(LOG_TAG, "Service found: could not resolve " + serviceInfo + ": " +
errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
if (mCallback != null) {
boolean added = mPrinters.add(serviceInfo.getHost().getHostAddress());
if (added) {
mCallback.onChanged(mPrinters.size());
}
}
}
}
});
}
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
mResolveQueue.resolve(getNDSManager(), serviceInfo,
new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
Log.w(LOG_TAG, "Service lost: Could not resolve " + serviceInfo + ": "
+ errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
if (mCallback != null) {
boolean removed = mPrinters
.remove(serviceInfo.getHost().getHostAddress());
if (removed) {
mCallback.onChanged(mPrinters.size());
}
}
}
}
});
}
}

View File

@@ -0,0 +1,325 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.printservice.recommendation.plugin.mdnsFilter;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.XmlResourceParser;
import android.util.ArrayMap;
import com.android.internal.annotations.Immutable;
import com.android.internal.util.Preconditions;
import com.android.printservice.recommendation.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Vendor configuration as read from {@link R.xml#vendorconfigs vendorconfigs.xml}. Configuration
* can be read via {@link #getConfig(Context, String)}.
*/
@Immutable
public class VendorConfig {
/** Lock for {@link #sConfigs} */
private static final Object sLock = new Object();
/** Strings used as XML tags */
private static final String VENDORS_TAG = "vendors";
private static final String VENDOR_TAG = "vendor";
private static final String NAME_TAG = "name";
private static final String PACKAGE_TAG = "package";
private static final String MDNSNAMES_TAG = "mdns-names";
private static final String MDNSNAME_TAG = "mdns-name";
/** Map from vendor name to config. Initialized on first {@link #getConfig use}. */
private static @Nullable ArrayMap<String, VendorConfig> sConfigs;
/** Localized vendor name */
public final @NonNull String name;
/** Package name containing the print service for this vendor */
public final @NonNull String packageName;
/** mDNS names used by this vendor */
public final @NonNull List<String> mDNSNames;
/**
* Create an immutable configuration.
*/
private VendorConfig(@NonNull String name, @NonNull String packageName,
@NonNull List<String> mDNSNames) {
this.name = Preconditions.checkStringNotEmpty(name);
this.packageName = Preconditions.checkStringNotEmpty(packageName);
this.mDNSNames = Preconditions.checkCollectionElementsNotNull(mDNSNames, "mDNSName");
}
/**
* Get the configuration for a vendor.
*
* @param context Calling context
* @param name The name of the config to read
*
* @return the config for the vendor or null if not found
*
* @throws IOException
* @throws XmlPullParserException
*/
public static @Nullable VendorConfig getConfig(@NonNull Context context, @NonNull String name)
throws IOException, XmlPullParserException {
synchronized (sLock) {
if (sConfigs == null) {
sConfigs = readVendorConfigs(context);
}
return sConfigs.get(name);
}
}
/**
* Get all known vendor configurations.
*
* @param context Calling context
*
* @return The known configurations
*
* @throws IOException
* @throws XmlPullParserException
*/
public static @NonNull Collection<VendorConfig> getAllConfigs(@NonNull Context context)
throws IOException, XmlPullParserException {
synchronized (sLock) {
if (sConfigs == null) {
sConfigs = readVendorConfigs(context);
}
return sConfigs.values();
}
}
/**
* Read the text from a XML tag.
*
* @param parser XML parser to read from
*
* @return The text or "" if no text was found
*
* @throws IOException
* @throws XmlPullParserException
*/
private static @NonNull String readText(XmlPullParser parser)
throws IOException, XmlPullParserException {
String result = "";
if (parser.next() == XmlPullParser.TEXT) {
result = parser.getText();
parser.nextTag();
}
return result;
}
/**
* Read a tag with a text content from the parser.
*
* @param parser XML parser to read from
* @param tagName The name of the tag to read
*
* @return The text content of the tag
*
* @throws IOException
* @throws XmlPullParserException
*/
private static @NonNull String readSimpleTag(@NonNull Context context,
@NonNull XmlPullParser parser, @NonNull String tagName, boolean resolveReferences)
throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, tagName);
String text = readText(parser);
parser.require(XmlPullParser.END_TAG, null, tagName);
if (resolveReferences && text.startsWith("@")) {
return context.getResources().getString(
context.getResources().getIdentifier(text, null, context.getPackageName()));
} else {
return text;
}
}
/**
* Read content of a list of tags.
*
* @param parser XML parser to read from
* @param tagName The name of the list tag
* @param subTagName The name of the list-element tags
* @param tagReader The {@link TagReader reader} to use to read the tag content
* @param <T> The type of the parsed tag content
*
* @return A list of {@link T}
*
* @throws XmlPullParserException
* @throws IOException
*/
private static @NonNull <T> ArrayList<T> readTagList(@NonNull XmlPullParser parser,
@NonNull String tagName, @NonNull String subTagName, @NonNull TagReader<T> tagReader)
throws XmlPullParserException, IOException {
ArrayList<T> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, tagName);
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (parser.getName().equals(subTagName)) {
entries.add(tagReader.readTag(parser, subTagName));
} else {
throw new XmlPullParserException(
"Unexpected subtag of " + tagName + ": " + parser.getName());
}
}
return entries;
}
/**
* Read the vendor configuration file.
*
* @param context The content issuing the read
*
* @return An map pointing from vendor name to config
*
* @throws IOException
* @throws XmlPullParserException
*/
private static @NonNull ArrayMap<String, VendorConfig> readVendorConfigs(
@NonNull final Context context) throws IOException, XmlPullParserException {
try (XmlResourceParser parser = context.getResources().getXml(R.xml.vendorconfigs)) {
// Skip header
int parsingEvent;
do {
parsingEvent = parser.next();
} while (parsingEvent != XmlResourceParser.START_TAG);
ArrayList<VendorConfig> configs = readTagList(parser, VENDORS_TAG, VENDOR_TAG,
new TagReader<VendorConfig>() {
public VendorConfig readTag(XmlPullParser parser, String tagName)
throws XmlPullParserException, IOException {
return readVendorConfig(context, parser, tagName);
}
});
ArrayMap<String, VendorConfig> configMap = new ArrayMap<>(configs.size());
final int numConfigs = configs.size();
for (int i = 0; i < numConfigs; i++) {
VendorConfig config = configs.get(i);
configMap.put(config.name, config);
}
return configMap;
}
}
/**
* Read a single vendor configuration.
*
* @param parser XML parser to read from
* @param tagName The vendor tag
* @param context Calling context
*
* @return A config
*
* @throws XmlPullParserException
* @throws IOException
*/
private static VendorConfig readVendorConfig(@NonNull final Context context,
@NonNull XmlPullParser parser, @NonNull String tagName) throws XmlPullParserException,
IOException {
parser.require(XmlPullParser.START_TAG, null, tagName);
String name = null;
String packageName = null;
List<String> mDNSNames = null;
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String subTagName = parser.getName();
switch (subTagName) {
case NAME_TAG:
name = readSimpleTag(context, parser, NAME_TAG, false);
break;
case PACKAGE_TAG:
packageName = readSimpleTag(context, parser, PACKAGE_TAG, true);
break;
case MDNSNAMES_TAG:
mDNSNames = readTagList(parser, MDNSNAMES_TAG, MDNSNAME_TAG,
new TagReader<String>() {
public String readTag(XmlPullParser parser, String tagName)
throws XmlPullParserException, IOException {
return readSimpleTag(context, parser, tagName, true);
}
}
);
break;
default:
throw new XmlPullParserException("Unexpected subtag of " + tagName + ": "
+ subTagName);
}
}
if (name == null) {
throw new XmlPullParserException("name is required");
}
if (packageName == null) {
throw new XmlPullParserException("package is required");
}
if (mDNSNames == null) {
mDNSNames = Collections.emptyList();
}
// A vendor config should be immutable
mDNSNames = Collections.unmodifiableList(mDNSNames);
return new VendorConfig(name, packageName, mDNSNames);
}
@Override
public String toString() {
return name + " -> " + packageName + ", " + mDNSNames;
}
/**
* Used a a "function pointer" when reading a tag in {@link #readTagList(XmlPullParser, String,
* String, TagReader)}.
*
* @param <T> The type of content to read
*/
private interface TagReader<T> {
T readTag(XmlPullParser parser, String tagName) throws XmlPullParserException, IOException;
}
}

View File

@@ -0,0 +1,98 @@
/*
* (c) Copyright 2016 Mopria Alliance, Inc.
* (c) Copyright 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.printservice.recommendation.util;
import android.annotation.NonNull;
import android.net.nsd.NsdServiceInfo;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
/**
* Utils for dealing with mDNS attributes
*/
public class MDNSUtils {
public static final String ATTRIBUTE_TY = "ty";
public static final String ATTRIBUTE_PRODUCT = "product";
public static final String ATTRIBUTE_USB_MFG = "usb_mfg";
public static final String ATTRIBUTE_MFG = "mfg";
/**
* Check if the service has any of a set of vendor names.
*
* @param serviceInfo The service
* @param vendorNames The vendors
*
* @return true iff the has any of the set of vendor names
*/
public static boolean isVendorPrinter(@NonNull NsdServiceInfo serviceInfo,
@NonNull Set<String> vendorNames) {
for (Map.Entry<String, byte[]> entry : serviceInfo.getAttributes().entrySet()) {
// keys are case insensitive
String key = entry.getKey().toLowerCase();
switch (key) {
case ATTRIBUTE_TY:
case ATTRIBUTE_PRODUCT:
case ATTRIBUTE_USB_MFG:
case ATTRIBUTE_MFG:
if (entry.getValue() != null) {
if (containsVendor(new String(entry.getValue(), StandardCharsets.UTF_8),
vendorNames)) {
return true;
}
}
break;
default:
break;
}
}
return false;
}
/**
* Check if the attribute matches any of the vendor names, ignoring capitalization.
*
* @param attr The attribute
* @param vendorNames The vendor names
*
* @return true iff the attribute matches any of the vendor names
*/
private static boolean containsVendor(@NonNull String attr, @NonNull Set<String> vendorNames) {
for (String name : vendorNames) {
if (containsString(attr.toLowerCase(), name.toLowerCase())) {
return true;
}
}
return false;
}
/**
* Check if a string in another string.
*
* @param container The string that contains the string
* @param contained The string that is contained
*
* @return true if the string is contained in the other
*/
private static boolean containsString(@NonNull String container, @NonNull String contained) {
return container.equalsIgnoreCase(contained) || container.contains(contained + " ");
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.printservice.recommendation.util;
import android.annotation.NonNull;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import com.android.internal.annotations.GuardedBy;
import java.util.LinkedList;
/**
* Nsd resolve requests for the same info cancel each other. Hence this class synchronizes the
* resolutions to hide this effect.
*/
public class NsdResolveQueue {
/** Lock for {@link #sInstance} */
private static final Object sLock = new Object();
/** Instance of this singleton */
@GuardedBy("sLock")
private static NsdResolveQueue sInstance;
/** Lock for {@link #mResolveRequests} */
private final Object mLock = new Object();
/** Current set of registered service info resolve attempts */
@GuardedBy("mLock")
private final LinkedList<NsdResolveRequest> mResolveRequests = new LinkedList<>();
public static NsdResolveQueue getInstance() {
synchronized (sLock) {
if (sInstance == null) {
sInstance = new NsdResolveQueue();
}
return sInstance;
}
}
/**
* Container for a request to resolve a serviceInfo.
*/
private static class NsdResolveRequest {
final @NonNull NsdManager nsdManager;
final @NonNull NsdServiceInfo serviceInfo;
final @NonNull NsdManager.ResolveListener listener;
private NsdResolveRequest(@NonNull NsdManager nsdManager,
@NonNull NsdServiceInfo serviceInfo, @NonNull NsdManager.ResolveListener listener) {
this.nsdManager = nsdManager;
this.serviceInfo = serviceInfo;
this.listener = listener;
}
}
/**
* Resolve a serviceInfo or queue the request if there is a request currently in flight.
*
* @param nsdManager The nsd manager to use
* @param serviceInfo The service info to resolve
* @param listener The listener to call back once the info is resolved.
*/
public void resolve(@NonNull NsdManager nsdManager, @NonNull NsdServiceInfo serviceInfo,
@NonNull NsdManager.ResolveListener listener) {
synchronized (mLock) {
mResolveRequests.addLast(new NsdResolveRequest(nsdManager, serviceInfo,
new ListenerWrapper(listener)));
if (mResolveRequests.size() == 1) {
resolveNextRequest();
}
}
}
/**
* Wrapper for a {@link NsdManager.ResolveListener}. Calls the listener and then
* {@link #resolveNextRequest()}.
*/
private class ListenerWrapper implements NsdManager.ResolveListener {
private final @NonNull NsdManager.ResolveListener mListener;
private ListenerWrapper(@NonNull NsdManager.ResolveListener listener) {
mListener = listener;
}
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
mListener.onResolveFailed(serviceInfo, errorCode);
synchronized (mLock) {
mResolveRequests.pop();
resolveNextRequest();
}
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
mListener.onServiceResolved(serviceInfo);
synchronized (mLock) {
mResolveRequests.pop();
resolveNextRequest();
}
}
}
/**
* Resolve the next request if there is one.
*/
private void resolveNextRequest() {
if (!mResolveRequests.isEmpty()) {
NsdResolveRequest request = mResolveRequests.getFirst();
request.nsdManager.resolveService(request.serviceInfo, request.listener);
}
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="48.0"
android:viewportHeight="48.0">
<path
android:pathData="M40,12h-8L32,8l-4,-4h-8l-4,4v4L8,12c-2.21,0 -3.98,1.79 -3.98,4L4,38c0,2.21 1.79,4 4,4h32c2.21,0 4,-1.79 4,-4L44,16c0,-2.21 -1.79,-4 -4,-4zM20,8h8v4h-8L20,8zM24,38L14,28h6v-8h8v8h6L24,38z"
android:fillColor="?android:attr/colorAccent"/>
</vector>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:orientation="horizontal"
android:gravity="start|center_vertical">
<ImageView
android:layout_width="36dip"
android:layout_height="36dip"
android:src="@drawable/ic_download_from_market"
android:layout_marginRight="4dip"
android:layout_gravity="center_vertical"
android:contentDescription="@null" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dip">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItem"
android:singleLine="true"
android:ellipsize="end" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:text="@string/enable_print_service" />
</RelativeLayout>
</LinearLayout>

View File

@@ -185,6 +185,12 @@
<!-- Label for the list item that links to the list of all print services. [CHAR LIMIT=50] -->
<string name="all_services_title">All services</string>
<!-- Subtitle for a print service recommendation. [CHAR LIMIT=50] -->
<plurals name="print_services_recommendation_subtitle">
<item quantity="one">Install to discover <xliff:g id="count" example="1">%1$s</xliff:g> printer</item>
<item quantity="other">Install to discover <xliff:g id="count" example="2">%1$s</xliff:g> printers</item>
</plurals>
<!-- Notifications -->
<!-- Template for the notification label for a printing print job. [CHAR LIMIT=25] -->

View File

@@ -30,10 +30,13 @@ import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.print.PrintManager;
import android.printservice.recommendation.RecommendationInfo;
import android.print.PrintServiceRecommendationsLoader;
import android.print.PrintServicesLoader;
import android.printservice.PrintServiceInfo;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.view.View;
@@ -45,8 +48,10 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.android.printspooler.R;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
@@ -57,31 +62,38 @@ import java.util.List;
* when the item is clicked.</li>
* <li>{@link #mDisabledServicesAdapter} for all disabled services. Once clicked the settings page
* for this service is opened.</li>
* <li>{@link RecommendedServicesAdapter} for a link to all services. If this item is clicked
* <li>{@link #mRecommendedServicesAdapter} for a link to all services. If this item is clicked
* the market app is opened to show all print services.</li>
* </ul>
*/
public class AddPrinterActivity extends ListActivity implements
LoaderManager.LoaderCallbacks<List<PrintServiceInfo>>,
AdapterView.OnItemClickListener {
public class AddPrinterActivity extends ListActivity implements AdapterView.OnItemClickListener {
private static final String LOG_TAG = "AddPrinterActivity";
/** Ids for the loaders */
private static final int LOADER_ID_ENABLED_SERVICES = 1;
private static final int LOADER_ID_DISABLED_SERVICES = 2;
private static final int LOADER_ID_RECOMMENDED_SERVICES = 3;
private static final int LOADER_ID_ALL_SERVICES = 4;
/**
* The enabled services list. This is filled from the {@link #LOADER_ID_ENABLED_SERVICES}
* loader in {@link #onLoadFinished}.
* loader in {@link PrintServiceInfoLoaderCallbacks#onLoadFinished}.
*/
private EnabledServicesAdapter mEnabledServicesAdapter;
/**
* The disabled services list. This is filled from the {@link #LOADER_ID_DISABLED_SERVICES}
* loader in {@link #onLoadFinished}.
* loader in {@link PrintServiceInfoLoaderCallbacks#onLoadFinished}.
*/
private DisabledServicesAdapter mDisabledServicesAdapter;
/**
* The recommended services list. This is filled from the
* {@link #LOADER_ID_RECOMMENDED_SERVICES} loader in
* {@link PrintServicePrintServiceRecommendationLoaderCallbacks#onLoadFinished}.
*/
private RecommendedServicesAdapter mRecommendedServicesAdapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -90,36 +102,116 @@ public class AddPrinterActivity extends ListActivity implements
mEnabledServicesAdapter = new EnabledServicesAdapter();
mDisabledServicesAdapter = new DisabledServicesAdapter();
mRecommendedServicesAdapter = new RecommendedServicesAdapter();
ArrayList<ActionAdapter> adapterList = new ArrayList<>(3);
adapterList.add(mEnabledServicesAdapter);
adapterList.add(new RecommendedServicesAdapter());
adapterList.add(mRecommendedServicesAdapter);
adapterList.add(mDisabledServicesAdapter);
setListAdapter(new CombinedAdapter(adapterList));
getListView().setOnItemClickListener(this);
getLoaderManager().initLoader(LOADER_ID_ENABLED_SERVICES, null, this);
getLoaderManager().initLoader(LOADER_ID_DISABLED_SERVICES, null, this);
// TODO: Load recommended services
PrintServiceInfoLoaderCallbacks printServiceLoaderCallbacks =
new PrintServiceInfoLoaderCallbacks();
getLoaderManager().initLoader(LOADER_ID_ENABLED_SERVICES, null, printServiceLoaderCallbacks);
getLoaderManager().initLoader(LOADER_ID_DISABLED_SERVICES, null, printServiceLoaderCallbacks);
getLoaderManager().initLoader(LOADER_ID_RECOMMENDED_SERVICES, null,
new PrintServicePrintServiceRecommendationLoaderCallbacks());
getLoaderManager().initLoader(LOADER_ID_ALL_SERVICES, null, printServiceLoaderCallbacks);
}
@Override
public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_ID_ENABLED_SERVICES:
return new PrintServicesLoader(
(PrintManager) getSystemService(Context.PRINT_SERVICE), this,
PrintManager.ENABLED_SERVICES);
case LOADER_ID_DISABLED_SERVICES:
return new PrintServicesLoader(
(PrintManager) getSystemService(Context.PRINT_SERVICE), this,
PrintManager.DISABLED_SERVICES);
// TODO: Load recommended services
default:
// not reached
return null;
/**
* Callbacks for the loaders operating on list of {@link PrintServiceInfo print service infos}.
*/
private class PrintServiceInfoLoaderCallbacks implements
LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
@Override
public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_ID_ENABLED_SERVICES:
return new PrintServicesLoader(
(PrintManager) getSystemService(Context.PRINT_SERVICE),
AddPrinterActivity.this, PrintManager.ENABLED_SERVICES);
case LOADER_ID_DISABLED_SERVICES:
return new PrintServicesLoader(
(PrintManager) getSystemService(Context.PRINT_SERVICE),
AddPrinterActivity.this, PrintManager.DISABLED_SERVICES);
case LOADER_ID_ALL_SERVICES:
return new PrintServicesLoader(
(PrintManager) getSystemService(Context.PRINT_SERVICE),
AddPrinterActivity.this, PrintManager.ALL_SERVICES);
default:
// not reached
return null;
}
}
@Override
public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
List<PrintServiceInfo> data) {
switch (loader.getId()) {
case LOADER_ID_ENABLED_SERVICES:
mEnabledServicesAdapter.updateData(data);
break;
case LOADER_ID_DISABLED_SERVICES:
mDisabledServicesAdapter.updateData(data);
break;
case LOADER_ID_ALL_SERVICES:
mRecommendedServicesAdapter.updateInstalledServices(data);
default:
// not reached
}
}
@Override
public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
if (!isFinishing()) {
switch (loader.getId()) {
case LOADER_ID_ENABLED_SERVICES:
mEnabledServicesAdapter.updateData(null);
break;
case LOADER_ID_DISABLED_SERVICES:
mDisabledServicesAdapter.updateData(null);
break;
case LOADER_ID_ALL_SERVICES:
mRecommendedServicesAdapter.updateInstalledServices(null);
break;
default:
// not reached
}
}
}
}
/**
* Callbacks for the loaders operating on list of {@link RecommendationInfo print service
* recommendations}.
*/
private class PrintServicePrintServiceRecommendationLoaderCallbacks implements
LoaderManager.LoaderCallbacks<List<RecommendationInfo>> {
@Override
public Loader<List<RecommendationInfo>> onCreateLoader(int id, Bundle args) {
return new PrintServiceRecommendationsLoader(
(PrintManager) getSystemService(Context.PRINT_SERVICE),
AddPrinterActivity.this);
}
@Override
public void onLoadFinished(Loader<List<RecommendationInfo>> loader,
List<RecommendationInfo> data) {
mRecommendedServicesAdapter.updateRecommendations(data);
}
@Override
public void onLoaderReset(Loader<List<RecommendationInfo>> loader) {
if (!isFinishing()) {
mRecommendedServicesAdapter.updateRecommendations(null);
}
}
}
@@ -128,39 +220,6 @@ public class AddPrinterActivity extends ListActivity implements
((ActionAdapter) getListAdapter()).performAction(position);
}
@Override
public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
List<PrintServiceInfo> data) {
switch (loader.getId()) {
case LOADER_ID_ENABLED_SERVICES:
mEnabledServicesAdapter.updateData(data);
break;
case LOADER_ID_DISABLED_SERVICES:
mDisabledServicesAdapter.updateData(data);
break;
// TODO: Load recommended services
default:
// not reached
}
}
@Override
public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
if (!isFinishing()) {
switch (loader.getId()) {
case LOADER_ID_ENABLED_SERVICES:
mEnabledServicesAdapter.updateData(null);
break;
case LOADER_ID_DISABLED_SERVICES:
mDisabledServicesAdapter.updateData(null);
break;
// TODO: Reset recommended services
default:
// not reached
}
}
}
/**
* Marks an adapter that can can perform an action for a position in it's list.
*/
@@ -490,28 +549,65 @@ public class AddPrinterActivity extends ListActivity implements
* Adapter for the recommended services.
*/
private class RecommendedServicesAdapter extends ActionAdapter {
/** Package names of all installed print services */
private @NonNull final ArraySet<String> mInstalledServices;
/** All print service recommendations */
private @Nullable List<RecommendationInfo> mRecommendations;
/**
* Sorted print service recommendations for services that are not installed
*
* @see #filterRecommendations
*/
private @Nullable List<RecommendationInfo> mFilteredRecommendations;
/**
* Create a new adapter.
*/
private RecommendedServicesAdapter() {
mInstalledServices = new ArraySet<>();
}
@Override
public int getCount() {
return 2;
if (mFilteredRecommendations == null) {
return 2;
} else {
return mFilteredRecommendations.size() + 2;
}
}
@Override
public int getViewTypeCount() {
return 2;
return 3;
}
/**
* @return The position the all services link is at.
*/
private int getAllServicesPos() {
return getCount() - 1;
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return 0;
} else {
} else if (getAllServicesPos() == position) {
return 1;
} else {
return 2;
}
}
@Override
public Object getItem(int position) {
return null;
if (position == 0 || position == getAllServicesPos()) {
return null;
} else {
return mFilteredRecommendations.get(position - 1);
}
}
@Override
@@ -531,11 +627,27 @@ public class AddPrinterActivity extends ListActivity implements
.setText(R.string.recommended_services_title);
return convertView;
}
} else if (position == getAllServicesPos()) {
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.all_print_services_list_item,
parent, false);
}
} else {
RecommendationInfo recommendation = (RecommendationInfo) getItem(position);
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.all_print_services_list_item,
parent, false);
if (convertView == null) {
convertView = getLayoutInflater().inflate(
R.layout.print_service_recommendations_list_item, parent, false);
}
((TextView) convertView.findViewById(R.id.title)).setText(recommendation.getName());
((TextView) convertView.findViewById(R.id.subtitle)).setText(getResources()
.getQuantityString(R.plurals.print_services_recommendation_subtitle,
recommendation.getNumDiscoveredPrinters(),
recommendation.getNumDiscoveredPrinters()));
return convertView;
}
return convertView;
@@ -548,16 +660,107 @@ public class AddPrinterActivity extends ListActivity implements
@Override
public void performAction(@IntRange(from = 0) int position) {
String searchUri = Settings.Secure
.getString(getContentResolver(), Settings.Secure.PRINT_SERVICE_SEARCH_URI);
if (position == getAllServicesPos()) {
String searchUri = Settings.Secure
.getString(getContentResolver(), Settings.Secure.PRINT_SERVICE_SEARCH_URI);
if (searchUri != null) {
try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)));
} catch (ActivityNotFoundException e) {
Log.e(LOG_TAG, "Cannot start market", e);
}
}
} else {
RecommendationInfo recommendation = (RecommendationInfo) getItem(position);
if (searchUri != null) {
try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)));
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(
R.string.uri_package_details, recommendation.getPackageName()))));
} catch (ActivityNotFoundException e) {
Log.e(LOG_TAG, "Cannot start market", e);
}
}
}
/**
* Filter recommended services.
*/
private void filterRecommendations() {
if (mRecommendations == null) {
mFilteredRecommendations = null;
} else {
mFilteredRecommendations = new ArrayList<>();
// Filter out recommendations for already installed services
final int numRecommendations = mRecommendations.size();
for (int i = 0; i < numRecommendations; i++) {
RecommendationInfo recommendation = mRecommendations.get(i);
if (!mInstalledServices.contains(recommendation.getPackageName())) {
mFilteredRecommendations.add(recommendation);
}
}
}
notifyDataSetChanged();
}
/**
* Update the installed print services.
*
* @param services The new set of services
*/
public void updateInstalledServices(List<PrintServiceInfo> services) {
mInstalledServices.clear();
final int numServices = services.size();
for (int i = 0; i < numServices; i++) {
mInstalledServices.add(services.get(i).getComponentName().getPackageName());
}
filterRecommendations();
}
/**
* Update the recommended print services.
*
* @param recommendations The new set of recommendations
*/
public void updateRecommendations(List<RecommendationInfo> recommendations) {
if (recommendations != null) {
final Collator collator = Collator.getInstance();
// Sort recommendations (early conditions are more important)
// - higher number of discovered printers first
// - single vendor services first
// - alphabetically
Collections.sort(recommendations,
new Comparator<RecommendationInfo>() {
@Override public int compare(RecommendationInfo o1,
RecommendationInfo o2) {
if (o1.getNumDiscoveredPrinters() !=
o2.getNumDiscoveredPrinters()) {
return o2.getNumDiscoveredPrinters() -
o1.getNumDiscoveredPrinters();
} else if (o1.recommendsMultiVendorService()
!= o2.recommendsMultiVendorService()) {
if (o1.recommendsMultiVendorService()) {
return 1;
} else {
return -1;
}
} else {
return collator.compare(o1.getName().toString(),
o2.getName().toString());
}
}
});
}
mRecommendations = recommendations;
filterRecommendations();
}
}
}

View File

@@ -41,12 +41,14 @@ import android.os.UserManager;
import android.print.IPrintDocumentAdapter;
import android.print.IPrintJobStateChangeListener;
import android.print.IPrintManager;
import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.IPrintServicesChangeListener;
import android.print.IPrinterDiscoveryObserver;
import android.print.PrintAttributes;
import android.print.PrintJobId;
import android.print.PrintJobInfo;
import android.print.PrintManager;
import android.printservice.recommendation.RecommendationInfo;
import android.print.PrinterId;
import android.printservice.PrintServiceInfo;
import android.provider.Settings;
@@ -265,7 +267,7 @@ public final class PrintManagerService extends SystemService {
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
final UserState userState;
synchronized (mLock) {
// Only the current group members can get enabled services.
// Only the current group members can get print services.
if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
return null;
}
@@ -313,6 +315,25 @@ public final class PrintManagerService extends SystemService {
}
}
@Override
public List<RecommendationInfo> getPrintServiceRecommendations(int userId) {
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
final UserState userState;
synchronized (mLock) {
// Only the current group members can get print service recommendations.
if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
return null;
}
userState = getOrCreateUserStateLocked(resolvedUserId, false);
}
final long identity = Binder.clearCallingIdentity();
try {
return userState.getPrintServiceRecommendations();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void createPrinterDiscoverySession(IPrinterDiscoveryObserver observer,
int userId) {
@@ -543,7 +564,7 @@ public final class PrintManagerService extends SystemService {
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
final UserState userState;
synchronized (mLock) {
// Only the current group members can remove a print job listener.
// Only the current group members can remove a print services change listener.
if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
return;
}
@@ -557,6 +578,52 @@ public final class PrintManagerService extends SystemService {
}
}
@Override
public void addPrintServiceRecommendationsChangeListener(
IRecommendationsChangeListener listener, int userId)
throws RemoteException {
listener = Preconditions.checkNotNull(listener);
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
final UserState userState;
synchronized (mLock) {
// Only the current group members can add a print service recommendations listener.
if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
return;
}
userState = getOrCreateUserStateLocked(resolvedUserId, false);
}
final long identity = Binder.clearCallingIdentity();
try {
userState.addPrintServiceRecommendationsChangeListener(listener);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void removePrintServiceRecommendationsChangeListener(
IRecommendationsChangeListener listener, int userId) {
listener = Preconditions.checkNotNull(listener);
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
final UserState userState;
synchronized (mLock) {
// Only the current group members can remove a print service recommendations
// listener.
if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
return;
}
userState = getOrCreateUserStateLocked(resolvedUserId, false);
}
final long identity = Binder.clearCallingIdentity();
try {
userState.removePrintServiceRecommendationsChangeListener(listener);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
fd = Preconditions.checkNotNull(fd);

View File

@@ -0,0 +1,235 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.print;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.printservice.recommendation.IRecommendationService;
import android.printservice.recommendation.IRecommendationServiceCallbacks;
import android.printservice.recommendation.RecommendationInfo;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import java.util.List;
import static android.content.pm.PackageManager.GET_META_DATA;
import static android.content.pm.PackageManager.GET_SERVICES;
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
/**
* Connection to a remote print service recommendation service.
*/
class RemotePrintServiceRecommendationService {
private static final String LOG_TAG = "RemotePrintServiceRecS";
/** Lock for this object */
private final Object mLock = new Object();
/** Context used for the connection */
private @NonNull final Context mContext;
/** The connection to the service (if {@link #mIsBound bound}) */
@GuardedBy("mLock")
private @NonNull final Connection mConnection;
/** If the service is currently bound. */
@GuardedBy("mLock")
private boolean mIsBound;
/** The service once bound */
@GuardedBy("mLock")
private IRecommendationService mService;
/**
* Callbacks to be called when there are updates to the print service recommendations.
*/
public interface RemotePrintServiceRecommendationServiceCallbacks {
/**
* Called when there is an update list of print service recommendations.
*
* @param recommendations The new recommendations.
*/
void onPrintServiceRecommendationsUpdated(
@Nullable List<RecommendationInfo> recommendations);
}
/**
* @return The intent that is used to connect to the print service recommendation service.
*/
private Intent getServiceIntent(@NonNull UserHandle userHandle) throws Exception {
List<ResolveInfo> installedServices = mContext.getPackageManager()
.queryIntentServicesAsUser(new Intent(
android.printservice.recommendation.RecommendationService.SERVICE_INTERFACE),
GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
userHandle.getIdentifier());
if (installedServices.size() != 1) {
throw new Exception(installedServices.size() + " instead of exactly one service found");
}
ResolveInfo installedService = installedServices.get(0);
ComponentName serviceName = new ComponentName(
installedService.serviceInfo.packageName,
installedService.serviceInfo.name);
ApplicationInfo appInfo = mContext.getPackageManager()
.getApplicationInfo(installedService.serviceInfo.packageName, 0);
if (appInfo == null) {
throw new Exception("Cannot read appInfo for service");
}
if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
throw new Exception("Service is not part of the system");
}
if (!android.Manifest.permission.BIND_PRINT_RECOMMENDATION_SERVICE.equals(
installedService.serviceInfo.permission)) {
throw new Exception("Service " + serviceName.flattenToShortString()
+ " does not require permission "
+ android.Manifest.permission.BIND_PRINT_RECOMMENDATION_SERVICE);
}
Intent serviceIntent = new Intent();
serviceIntent.setComponent(serviceName);
return serviceIntent;
}
/**
* Open a new connection to a {@link IRecommendationService remote print service
* recommendation service}.
*
* @param context The context establishing the connection
* @param userHandle The user the connection is for
* @param callbacks The callbacks to call by the service
*/
RemotePrintServiceRecommendationService(@NonNull Context context,
@NonNull UserHandle userHandle,
@NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks) {
mContext = context;
mConnection = new Connection(callbacks);
try {
Intent serviceIntent = getServiceIntent(userHandle);
synchronized (mLock) {
mIsBound = mContext.bindServiceAsUser(serviceIntent, mConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, userHandle);
if (!mIsBound) {
throw new Exception("Failed to bind to service " + serviceIntent);
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "Could not connect to print service recommendation service", e);
}
}
/**
* Terminate the connection to the {@link IRecommendationService remote print
* service recommendation service}.
*/
void close() {
synchronized (mLock) {
if (mService != null) {
try {
mService.registerCallbacks(null);
} catch (RemoteException e) {
Log.e(LOG_TAG, "Could not unregister callbacks", e);
}
mService = null;
}
if (mIsBound) {
mContext.unbindService(mConnection);
mIsBound = false;
}
}
}
@Override
protected void finalize() throws Throwable {
if (mIsBound || mService != null) {
Log.w(LOG_TAG, "Service still connected on finalize()");
close();
}
super.finalize();
}
/**
* Connection to the service.
*/
private class Connection implements ServiceConnection {
private final RemotePrintServiceRecommendationServiceCallbacks mCallbacks;
public Connection(@NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks) {
mCallbacks = callbacks;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
mService = (IRecommendationService)IRecommendationService.Stub.asInterface(service);
try {
mService.registerCallbacks(new IRecommendationServiceCallbacks.Stub() {
@Override
public void onRecommendationsUpdated(
List<RecommendationInfo> recommendations) {
synchronized (mLock) {
if (mIsBound && mService != null) {
if (recommendations != null) {
Preconditions.checkCollectionElementsNotNull(
recommendations, "recommendation");
}
mCallbacks.onPrintServiceRecommendationsUpdated(
recommendations);
}
}
}
});
} catch (RemoteException e) {
Log.e(LOG_TAG, "Could not register callbacks", e);
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.w(LOG_TAG, "Unexpected termination of connection");
synchronized (mLock) {
mService = null;
}
}
}
}

View File

@@ -37,6 +37,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.IInterface;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteCallbackList;
@@ -44,12 +45,14 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.print.IPrintDocumentAdapter;
import android.print.IPrintJobStateChangeListener;
import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.IPrintServicesChangeListener;
import android.print.IPrinterDiscoveryObserver;
import android.print.PrintAttributes;
import android.print.PrintJobId;
import android.print.PrintJobInfo;
import android.print.PrintManager;
import android.printservice.recommendation.RecommendationInfo;
import android.print.PrinterId;
import android.print.PrinterInfo;
import android.printservice.PrintServiceInfo;
@@ -68,6 +71,7 @@ import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
import com.android.server.print.RemotePrintService.PrintServiceCallbacks;
import com.android.server.print.RemotePrintSpooler.PrintSpoolerCallbacks;
import com.android.server.print.RemotePrintServiceRecommendationService.RemotePrintServiceRecommendationServiceCallbacks;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -82,7 +86,8 @@ import java.util.Set;
/**
* Represents the print state for a user.
*/
final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
RemotePrintServiceRecommendationServiceCallbacks {
private static final String LOG_TAG = "UserState";
@@ -122,10 +127,22 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
private List<PrintJobStateChangeListenerRecord> mPrintJobStateChangeListenerRecords;
private List<PrintServicesChangeListenerRecord> mPrintServicesChangeListenerRecords;
private List<ListenerRecord<IPrintServicesChangeListener>> mPrintServicesChangeListenerRecords;
private List<ListenerRecord<IRecommendationsChangeListener>>
mPrintServiceRecommendationsChangeListenerRecords;
private boolean mDestroyed;
/** Currently known list of print service recommendations */
private List<RecommendationInfo> mPrintServiceRecommendations;
/**
* Connection to the service updating the {@link #mPrintServiceRecommendations print service
* recommendations}.
*/
private RemotePrintServiceRecommendationService mPrintServiceRecommendationsService;
public UserState(Context context, int userId, Object lock, boolean lowPriority) {
mContext = context;
mUserId = userId;
@@ -409,6 +426,13 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
}
}
/**
* @return The currently known print service recommendations
*/
public @Nullable List<RecommendationInfo> getPrintServiceRecommendations() {
return mPrintServiceRecommendations;
}
public void createPrinterDiscoverySession(@NonNull IPrinterDiscoveryObserver observer) {
synchronized (mLock) {
throwIfDestroyedLocked();
@@ -566,7 +590,7 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
mPrintServicesChangeListenerRecords = new ArrayList<>();
}
mPrintServicesChangeListenerRecords.add(
new PrintServicesChangeListenerRecord(listener) {
new ListenerRecord<IPrintServicesChangeListener>(listener) {
@Override
public void onBinderDied() {
mPrintServicesChangeListenerRecords.remove(this);
@@ -583,7 +607,7 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
}
final int recordCount = mPrintServicesChangeListenerRecords.size();
for (int i = 0; i < recordCount; i++) {
PrintServicesChangeListenerRecord record =
ListenerRecord<IPrintServicesChangeListener> record =
mPrintServicesChangeListenerRecords.get(i);
if (record.listener.asBinder().equals(listener.asBinder())) {
mPrintServicesChangeListenerRecords.remove(i);
@@ -596,6 +620,54 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
}
}
public void addPrintServiceRecommendationsChangeListener(
@NonNull IRecommendationsChangeListener listener) throws RemoteException {
synchronized (mLock) {
throwIfDestroyedLocked();
if (mPrintServiceRecommendationsChangeListenerRecords == null) {
mPrintServiceRecommendationsChangeListenerRecords = new ArrayList<>();
mPrintServiceRecommendationsService =
new RemotePrintServiceRecommendationService(mContext,
UserHandle.getUserHandleForUid(mUserId), this);
}
mPrintServiceRecommendationsChangeListenerRecords.add(
new ListenerRecord<IRecommendationsChangeListener>(listener) {
@Override
public void onBinderDied() {
mPrintServiceRecommendationsChangeListenerRecords.remove(this);
}
});
}
}
public void removePrintServiceRecommendationsChangeListener(
@NonNull IRecommendationsChangeListener listener) {
synchronized (mLock) {
throwIfDestroyedLocked();
if (mPrintServiceRecommendationsChangeListenerRecords == null) {
return;
}
final int recordCount = mPrintServiceRecommendationsChangeListenerRecords.size();
for (int i = 0; i < recordCount; i++) {
ListenerRecord<IRecommendationsChangeListener> record =
mPrintServiceRecommendationsChangeListenerRecords.get(i);
if (record.listener.asBinder().equals(listener.asBinder())) {
mPrintServiceRecommendationsChangeListenerRecords.remove(i);
break;
}
}
if (mPrintServiceRecommendationsChangeListenerRecords.isEmpty()) {
mPrintServiceRecommendationsChangeListenerRecords = null;
mPrintServiceRecommendations = null;
mPrintServiceRecommendationsService.close();
mPrintServiceRecommendationsService = null;
}
}
}
@Override
public void onPrintJobStateChanged(PrintJobInfo printJob) {
mPrintJobForAppCache.onPrintJobStateChanged(printJob);
@@ -607,6 +679,12 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
mHandler.obtainMessage(UserStateHandler.MSG_DISPATCH_PRINT_SERVICES_CHANGED).sendToTarget();
}
@Override
public void onPrintServiceRecommendationsUpdated(List<RecommendationInfo> recommendations) {
mHandler.obtainMessage(UserStateHandler.MSG_DISPATCH_PRINT_SERVICES_RECOMMENDATIONS_UPDATED,
0, 0, recommendations).sendToTarget();
}
@Override
public void onPrintersAdded(List<PrinterInfo> printers) {
synchronized (mLock) {
@@ -1058,7 +1136,7 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
}
private void handleDispatchPrintServicesChanged() {
final List<PrintServicesChangeListenerRecord> records;
final List<ListenerRecord<IPrintServicesChangeListener>> records;
synchronized (mLock) {
if (mPrintServicesChangeListenerRecords == null) {
return;
@@ -1067,7 +1145,7 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
}
final int recordCount = records.size();
for (int i = 0; i < recordCount; i++) {
PrintServicesChangeListenerRecord record = records.get(i);
ListenerRecord<IPrintServicesChangeListener> record = records.get(i);
try {
record.listener.onPrintServicesChanged();;
@@ -1077,9 +1155,33 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
}
}
private void handleDispatchPrintServiceRecommendationsUpdated(
@Nullable List<RecommendationInfo> recommendations) {
final List<ListenerRecord<IRecommendationsChangeListener>> records;
synchronized (mLock) {
if (mPrintServiceRecommendationsChangeListenerRecords == null) {
return;
}
records = new ArrayList<>(mPrintServiceRecommendationsChangeListenerRecords);
mPrintServiceRecommendations = recommendations;
}
final int recordCount = records.size();
for (int i = 0; i < recordCount; i++) {
ListenerRecord<IRecommendationsChangeListener> record = records.get(i);
try {
record.listener.onRecommendationsChanged();
} catch (RemoteException re) {
Log.e(LOG_TAG, "Error notifying for print service recommendations change", re);
}
}
}
private final class UserStateHandler extends Handler {
public static final int MSG_DISPATCH_PRINT_JOB_STATE_CHANGED = 1;
public static final int MSG_DISPATCH_PRINT_SERVICES_CHANGED = 2;
public static final int MSG_DISPATCH_PRINT_SERVICES_RECOMMENDATIONS_UPDATED = 3;
public UserStateHandler(Looper looper) {
super(looper, null, false);
@@ -1096,6 +1198,10 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
case MSG_DISPATCH_PRINT_SERVICES_CHANGED:
handleDispatchPrintServicesChanged();
break;
case MSG_DISPATCH_PRINT_SERVICES_RECOMMENDATIONS_UPDATED:
handleDispatchPrintServiceRecommendationsUpdated(
(List<RecommendationInfo>) message.obj);
break;
default:
// not reached
}
@@ -1122,10 +1228,10 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
public abstract void onBinderDied();
}
private abstract class PrintServicesChangeListenerRecord implements DeathRecipient {
@NonNull final IPrintServicesChangeListener listener;
private abstract class ListenerRecord<T extends IInterface> implements DeathRecipient {
@NonNull final T listener;
public PrintServicesChangeListenerRecord(@NonNull IPrintServicesChangeListener listener) throws RemoteException {
public ListenerRecord(@NonNull T listener) throws RemoteException {
this.listener = listener;
listener.asBinder().linkToDeath(this, 0);
}