/*
* 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.printspooler.ui;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ListActivity;
import android.app.LoaderManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
import android.content.pm.ResolveInfo;
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;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
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;
/**
* This is an activity for adding a printer or. It consists of a list fed from three adapters:
*
* - {@link #mEnabledServicesAdapter} for all enabled services. If a service has an {@link
* PrintServiceInfo#getAddPrintersActivityName() add printer activity} this is started
* when the item is clicked.
* - {@link #mDisabledServicesAdapter} for all disabled services. Once clicked the settings page
* for this service is opened.
* - {@link #mRecommendedServicesAdapter} for a link to all services. If this item is clicked
* the market app is opened to show all print services.
*
*/
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 PrintServiceInfoLoaderCallbacks#onLoadFinished}.
*/
private EnabledServicesAdapter mEnabledServicesAdapter;
/**
* The disabled services list. This is filled from the {@link #LOADER_ID_DISABLED_SERVICES}
* 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);
setContentView(R.layout.add_printer_activity);
mEnabledServicesAdapter = new EnabledServicesAdapter();
mDisabledServicesAdapter = new DisabledServicesAdapter();
mRecommendedServicesAdapter = new RecommendedServicesAdapter();
ArrayList adapterList = new ArrayList<>(3);
adapterList.add(mEnabledServicesAdapter);
adapterList.add(mRecommendedServicesAdapter);
adapterList.add(mDisabledServicesAdapter);
setListAdapter(new CombinedAdapter(adapterList));
getListView().setOnItemClickListener(this);
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);
}
/**
* Callbacks for the loaders operating on list of {@link PrintServiceInfo print service infos}.
*/
private class PrintServiceInfoLoaderCallbacks implements
LoaderManager.LoaderCallbacks> {
@Override
public Loader> 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> loader,
List 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> 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> {
@Override
public Loader> onCreateLoader(int id, Bundle args) {
return new PrintServiceRecommendationsLoader(
(PrintManager) getSystemService(Context.PRINT_SERVICE),
AddPrinterActivity.this);
}
@Override
public void onLoadFinished(Loader> loader,
List data) {
mRecommendedServicesAdapter.updateRecommendations(data);
}
@Override
public void onLoaderReset(Loader> loader) {
if (!isFinishing()) {
mRecommendedServicesAdapter.updateRecommendations(null);
}
}
}
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
((ActionAdapter) getListAdapter()).performAction(position);
}
/**
* Marks an adapter that can can perform an action for a position in it's list.
*/
private abstract class ActionAdapter extends BaseAdapter {
/**
* Perform the action for a position in the list.
*
* @param position The position of the item
*/
abstract void performAction(@IntRange(from = 0) int position);
@Override
public boolean areAllItemsEnabled() {
return false;
}
}
/**
* An adapter presenting multiple sub adapters as a single combined adapter.
*/
private class CombinedAdapter extends ActionAdapter {
/** The adapters to combine */
private final @NonNull ArrayList mAdapters;
/**
* Create a combined adapter.
*
* @param adapters the list of adapters to combine
*/
CombinedAdapter(@NonNull ArrayList adapters) {
mAdapters = adapters;
final int numAdapters = mAdapters.size();
for (int i = 0; i < numAdapters; i++) {
mAdapters.get(i).registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
notifyDataSetChanged();
}
});
}
}
@Override
public int getCount() {
int totalCount = 0;
final int numAdapters = mAdapters.size();
for (int i = 0; i < numAdapters; i++) {
totalCount += mAdapters.get(i).getCount();
}
return totalCount;
}
/**
* Find the sub adapter and the position in the sub-adapter the position in the combined
* adapter refers to.
*
* @param position The position in the combined adapter
*
* @return The pair of adapter and position in sub adapter
*/
private @NonNull Pair getSubAdapter(int position) {
final int numAdapters = mAdapters.size();
for (int i = 0; i < numAdapters; i++) {
ActionAdapter adapter = mAdapters.get(i);
if (position < adapter.getCount()) {
return new Pair<>(adapter, position);
} else {
position -= adapter.getCount();
}
}
throw new IllegalArgumentException("Invalid position");
}
@Override
public int getItemViewType(int position) {
int numLowerViewTypes = 0;
final int numAdapters = mAdapters.size();
for (int i = 0; i < numAdapters; i++) {
Adapter adapter = mAdapters.get(i);
if (position < adapter.getCount()) {
return numLowerViewTypes + adapter.getItemViewType(position);
} else {
numLowerViewTypes += adapter.getViewTypeCount();
position -= adapter.getCount();
}
}
throw new IllegalArgumentException("Invalid position");
}
@Override
public int getViewTypeCount() {
int totalViewCount = 0;
final int numAdapters = mAdapters.size();
for (int i = 0; i < numAdapters; i++) {
totalViewCount += mAdapters.get(i).getViewTypeCount();
}
return totalViewCount;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Pair realPosition = getSubAdapter(position);
return realPosition.first.getView(realPosition.second, convertView, parent);
}
@Override
public Object getItem(int position) {
Pair realPosition = getSubAdapter(position);
return realPosition.first.getItem(realPosition.second);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean isEnabled(int position) {
Pair realPosition = getSubAdapter(position);
return realPosition.first.isEnabled(realPosition.second);
}
@Override
public void performAction(@IntRange(from = 0) int position) {
Pair realPosition = getSubAdapter(position);
realPosition.first.performAction(realPosition.second);
}
}
/**
* Superclass for all adapters that just display a list of {@link PrintServiceInfo}.
*/
private abstract class PrintServiceInfoAdapter extends ActionAdapter {
/**
* Raw data of the list.
*
* @see #updateData(List)
*/
private @NonNull List mServices;
/**
* Create a new adapter.
*/
PrintServiceInfoAdapter() {
mServices = Collections.emptyList();
}
/**
* Update the data.
*
* @param services The new raw data.
*/
void updateData(@Nullable List services) {
if (services == null || services.isEmpty()) {
mServices = Collections.emptyList();
} else {
mServices = services;
}
notifyDataSetChanged();
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return 0;
} else {
return 1;
}
}
@Override
public int getCount() {
if (mServices.isEmpty()) {
return 0;
} else {
return mServices.size() + 1;
}
}
@Override
public Object getItem(int position) {
if (position == 0) {
return null;
} else {
return mServices.get(position - 1);
}
}
@Override
public boolean isEnabled(int position) {
return position != 0;
}
@Override
public long getItemId(int position) {
return position;
}
}
/**
* Adapter for the enabled services.
*/
private class EnabledServicesAdapter extends PrintServiceInfoAdapter {
@Override
public void performAction(@IntRange(from = 0) int position) {
Intent intent = getAddPrinterIntent((PrintServiceInfo) getItem(position));
if (intent != null) {
try {
startActivity(intent);
} catch (ActivityNotFoundException|SecurityException e) {
Log.e(LOG_TAG, "Cannot start add printers activity", e);
}
}
}
/**
* Get the intent used to launch the add printers activity.
*
* @param service The service the printer should be added for
*
* @return The intent to launch the activity or null if the activity could not be launched.
*/
private Intent getAddPrinterIntent(@NonNull PrintServiceInfo service) {
String addPrinterActivityName = service.getAddPrintersActivityName();
if (!TextUtils.isEmpty(addPrinterActivityName)) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(new ComponentName(service.getComponentName().getPackageName(),
addPrinterActivityName));
List resolvedActivities = getPackageManager().queryIntentActivities(
intent, 0);
if (!resolvedActivities.isEmpty()) {
// The activity is a component name, therefore it is one or none.
if (resolvedActivities.get(0).activityInfo.exported) {
return intent;
}
}
}
return null;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (position == 0) {
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.add_printer_list_header,
parent, false);
}
((TextView) convertView.findViewById(R.id.text))
.setText(R.string.enabled_services_title);
return convertView;
}
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.enabled_print_services_list_item,
parent, false);
}
PrintServiceInfo service = (PrintServiceInfo) getItem(position);
TextView title = (TextView) convertView.findViewById(R.id.title);
ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
TextView subtitle = (TextView) convertView.findViewById(R.id.subtitle);
title.setText(service.getResolveInfo().loadLabel(getPackageManager()));
icon.setImageDrawable(service.getResolveInfo().loadIcon(getPackageManager()));
if (getAddPrinterIntent(service) == null) {
subtitle.setText(getString(R.string.cannot_add_printer));
} else {
subtitle.setText(getString(R.string.select_to_add_printers));
}
return convertView;
}
}
/**
* Adapter for the disabled services.
*/
private class DisabledServicesAdapter extends PrintServiceInfoAdapter {
@Override
public void performAction(@IntRange(from = 0) int position) {
((PrintManager) getSystemService(Context.PRINT_SERVICE)).setPrintServiceEnabled(
((PrintServiceInfo) getItem(position)).getComponentName(), true);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (position == 0) {
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.add_printer_list_header,
parent, false);
}
((TextView) convertView.findViewById(R.id.text))
.setText(R.string.disabled_services_title);
return convertView;
}
if (convertView == null) {
convertView = getLayoutInflater().inflate(
R.layout.disabled_print_services_list_item, parent, false);
}
PrintServiceInfo service = (PrintServiceInfo) getItem(position);
TextView title = (TextView) convertView.findViewById(R.id.title);
ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
title.setText(service.getResolveInfo().loadLabel(getPackageManager()));
icon.setImageDrawable(service.getResolveInfo().loadIcon(getPackageManager()));
return convertView;
}
}
/**
* Adapter for the recommended services.
*/
private class RecommendedServicesAdapter extends ActionAdapter {
/** Package names of all installed print services */
private @NonNull final ArraySet mInstalledServices;
/** All print service recommendations */
private @Nullable List mRecommendations;
/**
* Sorted print service recommendations for services that are not installed
*
* @see #filterRecommendations
*/
private @Nullable List mFilteredRecommendations;
/**
* Create a new adapter.
*/
private RecommendedServicesAdapter() {
mInstalledServices = new ArraySet<>();
}
@Override
public int getCount() {
if (mFilteredRecommendations == null) {
return 2;
} else {
return mFilteredRecommendations.size() + 2;
}
}
@Override
public int getViewTypeCount() {
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 if (getAllServicesPos() == position) {
return 1;
} else {
return 2;
}
}
@Override
public Object getItem(int position) {
if (position == 0 || position == getAllServicesPos()) {
return null;
} else {
return mFilteredRecommendations.get(position - 1);
}
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (position == 0) {
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.add_printer_list_header,
parent, false);
}
((TextView) convertView.findViewById(R.id.text))
.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.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;
}
@Override
public boolean isEnabled(int position) {
return position != 0;
}
@Override
public void performAction(@IntRange(from = 0) int position) {
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);
try {
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 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 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() {
@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();
}
}
}