/* * Copyright (C) 2013 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.settings.print; import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender.SendIntentException; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.print.PrintManager; import android.print.PrinterDiscoverySession; import android.print.PrinterDiscoverySession.OnPrintersChangeListener; import android.print.PrinterId; import android.print.PrinterInfo; import android.printservice.PrintServiceInfo; import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SearchView; import android.widget.Switch; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.widget.SettingsMainSwitchBar; import com.android.settingslib.widget.OnMainSwitchChangeListener; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Fragment with print service settings. */ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment implements OnMainSwitchChangeListener, LoaderManager.LoaderCallbacks> { private static final String LOG_TAG = "PrintServiceSettings"; private static final int LOADER_ID_PRINTERS_LOADER = 1; private static final int LOADER_ID_PRINT_SERVICE_LOADER = 2; private final AdapterDataObserver mDataObserver = new AdapterDataObserver() { @Override public void onChanged() { invalidateOptionsMenuIfNeeded(); updateEmptyView(); } private void invalidateOptionsMenuIfNeeded() { final int unfilteredItemCount = mPrintersAdapter.getUnfilteredCount(); if ((mLastUnfilteredItemCount <= 0 && unfilteredItemCount > 0) || mLastUnfilteredItemCount > 0 && unfilteredItemCount <= 0) { getActivity().invalidateOptionsMenu(); } mLastUnfilteredItemCount = unfilteredItemCount; } }; private SettingsMainSwitchBar mSwitchBar; private String mPreferenceKey; private Intent mSettingsIntent; private Intent mAddPrintersIntent; private ComponentName mComponentName; private PrintersAdapter mPrintersAdapter; private int mLastUnfilteredItemCount; private boolean mServiceEnabled; private SearchView mSearchView; @Override public int getMetricsCategory() { return SettingsEnums.PRINT_SERVICE_SETTINGS; } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); String title = getArguments().getString(PrintSettingsFragment.EXTRA_TITLE); if (!TextUtils.isEmpty(title)) { getActivity().setTitle(title); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = super.onCreateView(inflater, container, savedInstanceState); mServiceEnabled = getArguments().getBoolean(PrintSettingsFragment.EXTRA_CHECKED); return root; } @Override public void onStart() { super.onStart(); initComponents(); updateUiForArguments(); updateEmptyView(); updateUiForServiceState(); } @Override public void onPause() { if (mSearchView != null) { mSearchView.setOnQueryTextListener(null); } super.onPause(); } @Override public void onStop() { super.onStop(); mSwitchBar.removeOnSwitchChangeListener(this); mSwitchBar.hide(); mPrintersAdapter.unregisterAdapterDataObserver(mDataObserver); } private void onPreferenceToggled(String preferenceKey, boolean enabled) { ((PrintManager) getContext().getSystemService(Context.PRINT_SERVICE)) .setPrintServiceEnabled(mComponentName, enabled); } private void updateEmptyView() { ViewGroup contentRoot = (ViewGroup) getListView().getParent(); View emptyView = getEmptyView(); if (!mSwitchBar.isChecked()) { if (emptyView != null) { contentRoot.removeView(emptyView); emptyView = null; } if (emptyView == null) { emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_print_state, contentRoot, false); TextView textView = (TextView) emptyView.findViewById(R.id.message); textView.setText(R.string.print_service_disabled); contentRoot.addView(emptyView); setEmptyView(emptyView); } } else if (mPrintersAdapter.getUnfilteredCount() <= 0) { if (emptyView != null) { contentRoot.removeView(emptyView); emptyView = null; } if (emptyView == null) { emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_printers_list_service_enabled, contentRoot, false); contentRoot.addView(emptyView); setEmptyView(emptyView); } } else if (mPrintersAdapter.getItemCount() <= 0) { if (emptyView != null) { contentRoot.removeView(emptyView); emptyView = null; } if (emptyView == null) { emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_print_state, contentRoot, false); TextView textView = (TextView) emptyView.findViewById(R.id.message); textView.setText(R.string.print_no_printers_found); contentRoot.addView(emptyView); setEmptyView(emptyView); } } else if (mPrintersAdapter.getItemCount() > 0) { if (emptyView != null) { contentRoot.removeView(emptyView); } } } private void updateUiForServiceState() { if (mServiceEnabled) { mSwitchBar.setCheckedInternal(true); mPrintersAdapter.enable(); } else { mSwitchBar.setCheckedInternal(false); mPrintersAdapter.disable(); } getActivity().invalidateOptionsMenu(); } private void initComponents() { mPrintersAdapter = new PrintersAdapter(); mPrintersAdapter.registerAdapterDataObserver(mDataObserver); final SettingsActivity activity = (SettingsActivity) getActivity(); mSwitchBar = activity.getSwitchBar(); mSwitchBar.setTitle( getContext().getString(R.string.default_print_service_main_switch_title)); mSwitchBar.addOnSwitchChangeListener(this); mSwitchBar.show(); mSwitchBar.setOnBeforeCheckedChangeListener((toggleSwitch, checked) -> { onPreferenceToggled(mPreferenceKey, checked); return false; }); getListView().setAdapter(mPrintersAdapter); } @Override public void onSwitchChanged(Switch switchView, boolean isChecked) { updateEmptyView(); } private void updateUiForArguments() { Bundle arguments = getArguments(); // Component name. mComponentName = ComponentName.unflattenFromString(arguments .getString(PrintSettingsFragment.EXTRA_SERVICE_COMPONENT_NAME)); // Key. mPreferenceKey = mComponentName.flattenToString(); // Enabled. final boolean enabled = arguments.getBoolean(PrintSettingsFragment.EXTRA_CHECKED); mSwitchBar.setCheckedInternal(enabled); getLoaderManager().initLoader(LOADER_ID_PRINT_SERVICE_LOADER, null, this); setHasOptionsMenu(true); } @Override public Loader> onCreateLoader(int id, Bundle args) { return new SettingsPrintServicesLoader( (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE), getContext(), PrintManager.ALL_SERVICES); } @Override public void onLoadFinished(Loader> loader, List services) { PrintServiceInfo service = null; if (services != null) { final int numServices = services.size(); for (int i = 0; i < numServices; i++) { if (services.get(i).getComponentName().equals(mComponentName)) { service = services.get(i); break; } } } if (service == null) { // The print service was uninstalled finishFragment(); } mServiceEnabled = service.isEnabled(); if (service.getSettingsActivityName() != null) { Intent settingsIntent = new Intent(Intent.ACTION_MAIN); settingsIntent.setComponent( new ComponentName(service.getComponentName().getPackageName(), service.getSettingsActivityName())); List resolvedActivities = getPackageManager().queryIntentActivities( settingsIntent, 0); if (!resolvedActivities.isEmpty()) { // The activity is a component name, therefore it is one or none. if (resolvedActivities.get(0).activityInfo.exported) { mSettingsIntent = settingsIntent; } } } else { mSettingsIntent = null; } if (service.getAddPrintersActivityName() != null) { Intent addPrintersIntent = new Intent(Intent.ACTION_MAIN); addPrintersIntent.setComponent( new ComponentName(service.getComponentName().getPackageName(), service.getAddPrintersActivityName())); List resolvedActivities = getPackageManager().queryIntentActivities( addPrintersIntent, 0); if (!resolvedActivities.isEmpty()) { // The activity is a component name, therefore it is one or none. if (resolvedActivities.get(0).activityInfo.exported) { mAddPrintersIntent = addPrintersIntent; } } } else { mAddPrintersIntent = null; } updateUiForServiceState(); } @Override public void onLoaderReset(Loader> loader) { updateUiForServiceState(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.print_service_settings, menu); MenuItem addPrinters = menu.findItem(R.id.print_menu_item_add_printer); if (mServiceEnabled && mAddPrintersIntent != null) { addPrinters.setIntent(mAddPrintersIntent); } else { menu.removeItem(R.id.print_menu_item_add_printer); } MenuItem settings = menu.findItem(R.id.print_menu_item_settings); if (mServiceEnabled && mSettingsIntent != null) { settings.setIntent(mSettingsIntent); } else { menu.removeItem(R.id.print_menu_item_settings); } MenuItem searchItem = menu.findItem(R.id.print_menu_item_search); if (mServiceEnabled && mPrintersAdapter.getUnfilteredCount() > 0) { mSearchView = (SearchView) searchItem.getActionView(); mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return true; } @Override public boolean onQueryTextChange(String searchString) { mPrintersAdapter.getFilter().filter(searchString); return true; } }); mSearchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View view) { if (AccessibilityManager.getInstance(getActivity()).isEnabled()) { view.announceForAccessibility(getString( R.string.print_search_box_shown_utterance)); } } @Override public void onViewDetachedFromWindow(View view) { Activity activity = getActivity(); if (activity != null && !activity.isFinishing() && AccessibilityManager.getInstance(activity).isEnabled()) { view.announceForAccessibility(getString( R.string.print_search_box_hidden_utterance)); } } }); } else { menu.removeItem(R.id.print_menu_item_search); } } public static class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(@NonNull View itemView) { super(itemView); } } private final class PrintersAdapter extends RecyclerView.Adapter implements LoaderManager.LoaderCallbacks>, Filterable { private final Object mLock = new Object(); private final List mPrinters = new ArrayList(); private final List mFilteredPrinters = new ArrayList(); private CharSequence mLastSearchString; public void enable() { getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, null, this); } public void disable() { getLoaderManager().destroyLoader(LOADER_ID_PRINTERS_LOADER); mPrinters.clear(); } public int getUnfilteredCount() { return mPrinters.size(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { synchronized (mLock) { if (TextUtils.isEmpty(constraint)) { return null; } FilterResults results = new FilterResults(); List filteredPrinters = new ArrayList(); String constraintLowerCase = constraint.toString().toLowerCase(); final int printerCount = mPrinters.size(); for (int i = 0; i < printerCount; i++) { PrinterInfo printer = mPrinters.get(i); String name = printer.getName(); if (name != null && name.toLowerCase().contains(constraintLowerCase)) { filteredPrinters.add(printer); } } results.values = filteredPrinters; results.count = filteredPrinters.size(); return results; } } @Override @SuppressWarnings("unchecked") protected void publishResults(CharSequence constraint, FilterResults results) { synchronized (mLock) { mLastSearchString = constraint; mFilteredPrinters.clear(); if (results == null) { mFilteredPrinters.addAll(mPrinters); } else { List printers = (List) results.values; mFilteredPrinters.addAll(printers); } } notifyDataSetChanged(); } }; } @Override public int getItemCount() { synchronized (mLock) { return mFilteredPrinters.size(); } } private Object getItem(int position) { synchronized (mLock) { return mFilteredPrinters.get(position); } } @Override public long getItemId(int position) { return position; } /** * Checks if a printer can be used for printing * * @param position The position of the printer in the list * @return true iff the printer can be used for printing. */ public boolean isActionable(int position) { PrinterInfo printer = (PrinterInfo) getItem(position); return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.printer_dropdown_item, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.itemView.setEnabled(isActionable(position)); final PrinterInfo printer = (PrinterInfo) getItem(position); CharSequence title = printer.getName(); CharSequence subtitle = printer.getDescription(); Drawable icon = printer.loadIcon(getActivity()); TextView titleView = holder.itemView.findViewById(R.id.title); titleView.setText(title); TextView subtitleView = holder.itemView.findViewById(R.id.subtitle); if (!TextUtils.isEmpty(subtitle)) { subtitleView.setText(subtitle); subtitleView.setVisibility(View.VISIBLE); } else { subtitleView.setText(null); subtitleView.setVisibility(View.GONE); } LinearLayout moreInfoView = holder.itemView.findViewById(R.id.more_info); if (printer.getInfoIntent() != null) { moreInfoView.setVisibility(View.VISIBLE); moreInfoView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { try { getActivity().startIntentSender( printer.getInfoIntent().getIntentSender(), null, 0, 0, 0); } catch (SendIntentException e) { Log.e(LOG_TAG, "Could not execute pending info intent: %s", e); } } }); } else { moreInfoView.setVisibility(View.GONE); } ImageView iconView = holder.itemView.findViewById(R.id.icon); if (icon != null) { iconView.setVisibility(View.VISIBLE); if (!isActionable(position)) { icon.mutate(); TypedValue value = new TypedValue(); getActivity().getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true); icon.setAlpha((int) (value.getFloat() * 255)); } iconView.setImageDrawable(icon); } else { iconView.setVisibility(View.GONE); } holder.itemView.setOnClickListener(v -> { PrinterInfo pi = (PrinterInfo) getItem(position); if (pi.getInfoIntent() != null) { try { getActivity().startIntentSender(pi.getInfoIntent().getIntentSender(), null, 0, 0, 0); } catch (SendIntentException e) { Log.e(LOG_TAG, "Could not execute info intent: %s", e); } } }); } @Override public Loader> onCreateLoader(int id, Bundle args) { if (id == LOADER_ID_PRINTERS_LOADER) { return new PrintersLoader(getContext()); } return null; } @Override public void onLoadFinished(Loader> loader, List printers) { synchronized (mLock) { mPrinters.clear(); final int printerCount = printers.size(); for (int i = 0; i < printerCount; i++) { PrinterInfo printer = printers.get(i); if (printer.getId().getServiceName().equals(mComponentName)) { mPrinters.add(printer); } } mFilteredPrinters.clear(); mFilteredPrinters.addAll(mPrinters); if (!TextUtils.isEmpty(mLastSearchString)) { getFilter().filter(mLastSearchString); } } notifyDataSetChanged(); } @Override public void onLoaderReset(Loader> loader) { synchronized (mLock) { mPrinters.clear(); mFilteredPrinters.clear(); mLastSearchString = null; } notifyDataSetChanged(); } } private static class PrintersLoader extends Loader> { private static final String LOG_TAG = "PrintersLoader"; private static final boolean DEBUG = false; private final Map mPrinters = new LinkedHashMap(); private PrinterDiscoverySession mDiscoverySession; public PrintersLoader(Context context) { super(context); } @Override public void deliverResult(List printers) { if (isStarted()) { super.deliverResult(printers); } } @Override protected void onStartLoading() { if (DEBUG) { Log.i(LOG_TAG, "onStartLoading()"); } // The contract is that if we already have a valid, // result the we have to deliver it immediately. if (!mPrinters.isEmpty()) { deliverResult(new ArrayList(mPrinters.values())); } // We want to start discovery at this point. onForceLoad(); } @Override protected void onStopLoading() { if (DEBUG) { Log.i(LOG_TAG, "onStopLoading()"); } onCancelLoad(); } @Override protected void onForceLoad() { if (DEBUG) { Log.i(LOG_TAG, "onForceLoad()"); } loadInternal(); } @Override protected boolean onCancelLoad() { if (DEBUG) { Log.i(LOG_TAG, "onCancelLoad()"); } return cancelInternal(); } @Override protected void onReset() { if (DEBUG) { Log.i(LOG_TAG, "onReset()"); } onStopLoading(); mPrinters.clear(); if (mDiscoverySession != null) { mDiscoverySession.destroy(); mDiscoverySession = null; } } @Override protected void onAbandon() { if (DEBUG) { Log.i(LOG_TAG, "onAbandon()"); } onStopLoading(); } private boolean cancelInternal() { if (mDiscoverySession != null && mDiscoverySession.isPrinterDiscoveryStarted()) { mDiscoverySession.stopPrinterDiscovery(); return true; } return false; } private void loadInternal() { if (mDiscoverySession == null) { PrintManager printManager = (PrintManager) getContext() .getSystemService(Context.PRINT_SERVICE); mDiscoverySession = printManager.createPrinterDiscoverySession(); mDiscoverySession.setOnPrintersChangeListener(new OnPrintersChangeListener() { @Override public void onPrintersChanged() { deliverResult(new ArrayList( mDiscoverySession.getPrinters())); } }); } mDiscoverySession.startPrinterDiscovery(null); } } }