Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b2565935 authored by Philip P. Moltmann's avatar Philip P. Moltmann
Browse files

Add RecommendationPlugin for Cloud Print

Reuses already existing infrastructure. The mMDNSFilteredDiscovery lambda
is called for every network service that supports #PRIVET_SERVICE. Then
the plugin checks the txt fields to make sure the printer is valid. The
check is not complete but good enough to make sure this is either a
cloud print capable printer or something that tries really hard.

Test: Connected to network with three printers, 2 GCP capable. Found the
      two printers
Fixes: 35766193
Change-Id: I7c9180c8c154fa092fec5b943a94bad77da74c86
parent 67ee79e8
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@
-->
-->


<resources>
<resources>
    <string name="plugin_vendor_google_cloud_print">Cloud Print</string>
    <string name="plugin_vendor_hp">HP</string>
    <string name="plugin_vendor_hp">HP</string>
    <string name="plugin_vendor_lexmark">Lexmark</string>
    <string name="plugin_vendor_lexmark">Lexmark</string>
    <string name="plugin_vendor_brother">Brother</string>
    <string name="plugin_vendor_brother">Brother</string>
+9 −0
Original line number Original line Diff line number Diff line
@@ -22,6 +22,7 @@ import android.printservice.recommendation.RecommendationInfo;
import android.printservice.recommendation.RecommendationService;
import android.printservice.recommendation.RecommendationService;
import android.util.Log;
import android.util.Log;


import com.android.printservice.recommendation.plugin.google.CloudPrintPlugin;
import com.android.printservice.recommendation.plugin.hp.HPRecommendationPlugin;
import com.android.printservice.recommendation.plugin.hp.HPRecommendationPlugin;
import com.android.printservice.recommendation.plugin.mdnsFilter.MDNSFilterPlugin;
import com.android.printservice.recommendation.plugin.mdnsFilter.MDNSFilterPlugin;
import com.android.printservice.recommendation.plugin.mdnsFilter.VendorConfig;
import com.android.printservice.recommendation.plugin.mdnsFilter.VendorConfig;
@@ -64,6 +65,14 @@ public class RecommendationServiceImpl extends RecommendationService
            throw new RuntimeException("Could not parse vendorconfig", e);
            throw new RuntimeException("Could not parse vendorconfig", e);
        }
        }


        try {
            mPlugins.add(new RemotePrintServicePlugin(new CloudPrintPlugin(this), this,
                    true));
        } catch (Exception e) {
            Log.e(LOG_TAG, "Could not initiate "
                            + getString(R.string.plugin_vendor_google_cloud_print) + " plugin", e);
        }

        try {
        try {
            mPlugins.add(new RemotePrintServicePlugin(new HPRecommendationPlugin(this), this,
            mPlugins.add(new RemotePrintServicePlugin(new HPRecommendationPlugin(this), this,
                    false));
                    false));
+168 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2017 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.google;

import static com.android.printservice.recommendation.util.MDNSUtils.ATTRIBUTE_TY;

import android.annotation.NonNull;
import android.annotation.StringRes;
import android.content.Context;
import android.util.ArrayMap;
import android.util.Log;

import com.android.printservice.recommendation.PrintServicePlugin;
import com.android.printservice.recommendation.R;
import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Plugin detecting <a href="https://developers.google.com/cloud-print/docs/privet">Google Cloud
 * Print</a> printers.
 */
public class CloudPrintPlugin implements PrintServicePlugin {
    private static final String LOG_TAG = CloudPrintPlugin.class.getSimpleName();
    private static final boolean DEBUG = false;

    private static final String ATTRIBUTE_TXTVERS = "txtvers";
    private static final String ATTRIBUTE_URL = "url";
    private static final String ATTRIBUTE_TYPE = "type";
    private static final String ATTRIBUTE_ID = "id";
    private static final String ATTRIBUTE_CS = "cs";

    private static final String TYPE = "printer";

    private static final String PRIVET_SERVICE = "_privet._tcp";

    /** The required mDNS service types */
    private static final Set<String> PRINTER_SERVICE_TYPE = new HashSet<String>() {{
        // Not checking _printer_._sub
        add(PRIVET_SERVICE);
    }};

    /** All possible connection states */
    private static final Set<String> POSSIBLE_CONNECTION_STATES = new HashSet<String>() {{
        add("online");
        add("offline");
        add("connecting");
        add("not-configured");
    }};

    private static final byte SUPPORTED_TXTVERS = '1';

    /** The mDNS filtered discovery */
    private final MDNSFilteredDiscovery mMDNSFilteredDiscovery;

    /**
     * Create a plugin detecting Google Cloud Print printers.
     *
     * @param context The context the plugin runs in
     */
    public CloudPrintPlugin(@NonNull Context context) {
        mMDNSFilteredDiscovery = new MDNSFilteredDiscovery(context, PRINTER_SERVICE_TYPE,
                nsdServiceInfo -> {
                    // The attributes are case insensitive. For faster searching create a clone of
                    // the map with the attribute-keys all in lower case.
                    ArrayMap<String, byte[]> caseInsensitiveAttributes =
                            new ArrayMap<>(nsdServiceInfo.getAttributes().size());
                    for (Map.Entry<String, byte[]> entry : nsdServiceInfo.getAttributes()
                            .entrySet()) {
                        caseInsensitiveAttributes.put(entry.getKey().toLowerCase(),
                                entry.getValue());
                    }

                    if (DEBUG) {
                        Log.i(LOG_TAG, nsdServiceInfo.getServiceName() + ":");
                        Log.i(LOG_TAG, "type:  " + nsdServiceInfo.getServiceType());
                        Log.i(LOG_TAG, "host:  " + nsdServiceInfo.getHost());
                        for (Map.Entry<String, byte[]> entry : caseInsensitiveAttributes.entrySet()) {
                            if (entry.getValue() == null) {
                                Log.i(LOG_TAG, entry.getKey() + "= null");
                            } else {
                                Log.i(LOG_TAG, entry.getKey() + "=" + new String(entry.getValue(),
                                        StandardCharsets.UTF_8));
                            }
                        }
                    }

                    byte[] txtvers = caseInsensitiveAttributes.get(ATTRIBUTE_TXTVERS);
                    if (txtvers == null || txtvers.length != 1 || txtvers[0] != SUPPORTED_TXTVERS) {
                        // The spec requires this to be the first attribute, but at this time we
                        // lost the order of the attributes
                        return false;
                    }

                    if (caseInsensitiveAttributes.get(ATTRIBUTE_TY) == null) {
                        return false;
                    }

                    byte[] url = caseInsensitiveAttributes.get(ATTRIBUTE_URL);
                    if (url == null || url.length == 0) {
                        return false;
                    }

                    byte[] type = caseInsensitiveAttributes.get(ATTRIBUTE_TYPE);
                    if (type == null || !TYPE.equals(
                            new String(type, StandardCharsets.UTF_8).toLowerCase())) {
                        return false;
                    }

                    if (caseInsensitiveAttributes.get(ATTRIBUTE_ID) == null) {
                        return false;
                    }

                    byte[] cs = caseInsensitiveAttributes.get(ATTRIBUTE_CS);
                    if (cs == null || !POSSIBLE_CONNECTION_STATES.contains(
                            new String(cs, StandardCharsets.UTF_8).toLowerCase())) {
                        return false;
                    }

                    InetAddress address = nsdServiceInfo.getHost();
                    if (!(address instanceof Inet4Address)) {
                        // Not checking for link local address
                        return false;
                    }

                    return true;
                });
    }

    @Override
    @NonNull public CharSequence getPackageName() {
        return "com.google.android.apps.cloudprint";
    }

    @Override
    public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
        mMDNSFilteredDiscovery.start(callback);
    }

    @Override
    @StringRes public int getName() {
        return R.string.plugin_vendor_google_cloud_print;
    }

    @Override
    public void stop() throws Exception {
        mMDNSFilteredDiscovery.stop();
    }
}