1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.print; 18 19 import android.app.LoaderManager.LoaderCallbacks; 20 import android.content.ActivityNotFoundException; 21 import android.content.AsyncTaskLoader; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.Loader; 26 import android.content.pm.PackageManager; 27 import android.graphics.drawable.Drawable; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.print.PrintJob; 31 import android.print.PrintJobId; 32 import android.print.PrintJobInfo; 33 import android.print.PrintManager; 34 import android.print.PrintManager.PrintJobStateChangeListener; 35 import android.print.PrintServicesLoader; 36 import android.printservice.PrintServiceInfo; 37 import android.provider.SearchIndexableResource; 38 import android.provider.Settings; 39 import android.support.annotation.VisibleForTesting; 40 import android.support.v7.preference.Preference; 41 import android.support.v7.preference.PreferenceCategory; 42 import android.text.TextUtils; 43 import android.text.format.DateUtils; 44 import android.util.Log; 45 import android.view.LayoutInflater; 46 import android.view.View; 47 import android.view.View.OnClickListener; 48 import android.view.ViewGroup; 49 import android.widget.Button; 50 import android.widget.TextView; 51 52 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 53 import com.android.settings.R; 54 import com.android.settings.dashboard.SummaryLoader; 55 import com.android.settings.search.BaseSearchIndexProvider; 56 import com.android.settings.search.Indexable; 57 import com.android.settings.search.SearchIndexableRaw; 58 import com.android.settings.utils.ProfileSettingsPreferenceFragment; 59 60 import java.text.DateFormat; 61 import java.util.ArrayList; 62 import java.util.List; 63 64 /** 65 * Fragment with the top level print settings. 66 */ 67 public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment 68 implements Indexable, OnClickListener { 69 public static final String TAG = "PrintSettingsFragment"; 70 private static final int LOADER_ID_PRINT_JOBS_LOADER = 1; 71 private static final int LOADER_ID_PRINT_SERVICES = 2; 72 73 private static final String PRINT_JOBS_CATEGORY = "print_jobs_category"; 74 private static final String PRINT_SERVICES_CATEGORY = "print_services_category"; 75 76 static final String EXTRA_CHECKED = "EXTRA_CHECKED"; 77 static final String EXTRA_TITLE = "EXTRA_TITLE"; 78 static final String EXTRA_SERVICE_COMPONENT_NAME = "EXTRA_SERVICE_COMPONENT_NAME"; 79 80 static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID"; 81 82 private static final String EXTRA_PRINT_SERVICE_COMPONENT_NAME = 83 "EXTRA_PRINT_SERVICE_COMPONENT_NAME"; 84 85 private static final int ORDER_LAST = Preference.DEFAULT_ORDER - 1; 86 87 private PreferenceCategory mActivePrintJobsCategory; 88 private PreferenceCategory mPrintServicesCategory; 89 90 private PrintJobsController mPrintJobsController; 91 private PrintServicesController mPrintServicesController; 92 93 private Button mAddNewServiceButton; 94 95 @Override getMetricsCategory()96 public int getMetricsCategory() { 97 return MetricsEvent.PRINT_SETTINGS; 98 } 99 100 @Override getHelpResource()101 protected int getHelpResource() { 102 return R.string.help_uri_printing; 103 } 104 105 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)106 public View onCreateView(LayoutInflater inflater, ViewGroup container, 107 Bundle savedInstanceState) { 108 View root = super.onCreateView(inflater, container, savedInstanceState); 109 addPreferencesFromResource(R.xml.print_settings); 110 111 mActivePrintJobsCategory = (PreferenceCategory) findPreference( 112 PRINT_JOBS_CATEGORY); 113 mPrintServicesCategory = (PreferenceCategory) findPreference( 114 PRINT_SERVICES_CATEGORY); 115 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 116 117 mPrintJobsController = new PrintJobsController(); 118 getLoaderManager().initLoader(LOADER_ID_PRINT_JOBS_LOADER, null, mPrintJobsController); 119 120 mPrintServicesController = new PrintServicesController(); 121 getLoaderManager().initLoader(LOADER_ID_PRINT_SERVICES, null, mPrintServicesController); 122 123 return root; 124 } 125 126 @Override onStart()127 public void onStart() { 128 super.onStart(); 129 setHasOptionsMenu(true); 130 startSubSettingsIfNeeded(); 131 } 132 133 @Override onStop()134 public void onStop() { 135 super.onStop(); 136 } 137 138 @Override onViewCreated(View view, Bundle savedInstanceState)139 public void onViewCreated(View view, Bundle savedInstanceState) { 140 super.onViewCreated(view, savedInstanceState); 141 ViewGroup contentRoot = (ViewGroup) getListView().getParent(); 142 View emptyView = getActivity().getLayoutInflater().inflate( 143 R.layout.empty_print_state, contentRoot, false); 144 TextView textView = (TextView) emptyView.findViewById(R.id.message); 145 textView.setText(R.string.print_no_services_installed); 146 147 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 148 if (addNewServiceIntent != null) { 149 mAddNewServiceButton = (Button) emptyView.findViewById(R.id.add_new_service); 150 mAddNewServiceButton.setOnClickListener(this); 151 // The empty is used elsewhere too so it's hidden by default. 152 mAddNewServiceButton.setVisibility(View.VISIBLE); 153 } 154 155 contentRoot.addView(emptyView); 156 setEmptyView(emptyView); 157 } 158 159 @Override getIntentActionString()160 protected String getIntentActionString() { 161 return Settings.ACTION_PRINT_SETTINGS; 162 } 163 164 /** 165 * Adds preferences for all print services to the {@value PRINT_SERVICES_CATEGORY} cathegory. 166 */ 167 private final class PrintServicesController implements 168 LoaderCallbacks<List<PrintServiceInfo>> { 169 @Override onCreateLoader(int id, Bundle args)170 public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) { 171 PrintManager printManager = 172 (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE); 173 if (printManager != null) { 174 return new PrintServicesLoader(printManager, getContext(), 175 PrintManager.ALL_SERVICES); 176 } else { 177 return null; 178 } 179 } 180 181 @Override onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)182 public void onLoadFinished(Loader<List<PrintServiceInfo>> loader, 183 List<PrintServiceInfo> services) { 184 if (services.isEmpty()) { 185 getPreferenceScreen().removePreference(mPrintServicesCategory); 186 return; 187 } else if (getPreferenceScreen().findPreference(PRINT_SERVICES_CATEGORY) == null) { 188 getPreferenceScreen().addPreference(mPrintServicesCategory); 189 } 190 191 mPrintServicesCategory.removeAll(); 192 PackageManager pm = getActivity().getPackageManager(); 193 final Context context = getPrefContext(); 194 if (context == null) { 195 Log.w(TAG, "No preference context, skip adding print services"); 196 return; 197 } 198 199 for (PrintServiceInfo service : services) { 200 Preference preference = new Preference(context); 201 202 String title = service.getResolveInfo().loadLabel(pm).toString(); 203 preference.setTitle(title); 204 205 ComponentName componentName = service.getComponentName(); 206 preference.setKey(componentName.flattenToString()); 207 208 preference.setFragment(PrintServiceSettingsFragment.class.getName()); 209 preference.setPersistent(false); 210 211 if (service.isEnabled()) { 212 preference.setSummary(getString(R.string.print_feature_state_on)); 213 } else { 214 preference.setSummary(getString(R.string.print_feature_state_off)); 215 } 216 217 Drawable drawable = service.getResolveInfo().loadIcon(pm); 218 if (drawable != null) { 219 preference.setIcon(drawable); 220 } 221 222 Bundle extras = preference.getExtras(); 223 extras.putBoolean(EXTRA_CHECKED, service.isEnabled()); 224 extras.putString(EXTRA_TITLE, title); 225 extras.putString(EXTRA_SERVICE_COMPONENT_NAME, componentName.flattenToString()); 226 227 mPrintServicesCategory.addPreference(preference); 228 } 229 230 Preference addNewServicePreference = newAddServicePreferenceOrNull(); 231 if (addNewServicePreference != null) { 232 mPrintServicesCategory.addPreference(addNewServicePreference); 233 } 234 } 235 236 @Override onLoaderReset(Loader<List<PrintServiceInfo>> loader)237 public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) { 238 getPreferenceScreen().removePreference(mPrintServicesCategory); 239 } 240 } 241 newAddServicePreferenceOrNull()242 private Preference newAddServicePreferenceOrNull() { 243 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 244 if (addNewServiceIntent == null) { 245 return null; 246 } 247 Preference preference = new Preference(getPrefContext()); 248 preference.setTitle(R.string.print_menu_item_add_service); 249 preference.setIcon(R.drawable.ic_menu_add); 250 preference.setOrder(ORDER_LAST); 251 preference.setIntent(addNewServiceIntent); 252 preference.setPersistent(false); 253 return preference; 254 } 255 createAddNewServiceIntentOrNull()256 private Intent createAddNewServiceIntentOrNull() { 257 final String searchUri = Settings.Secure.getString(getContentResolver(), 258 Settings.Secure.PRINT_SERVICE_SEARCH_URI); 259 if (TextUtils.isEmpty(searchUri)) { 260 return null; 261 } 262 return new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); 263 } 264 startSubSettingsIfNeeded()265 private void startSubSettingsIfNeeded() { 266 if (getArguments() == null) { 267 return; 268 } 269 String componentName = getArguments().getString(EXTRA_PRINT_SERVICE_COMPONENT_NAME); 270 if (componentName != null) { 271 getArguments().remove(EXTRA_PRINT_SERVICE_COMPONENT_NAME); 272 Preference prereference = findPreference(componentName); 273 if (prereference != null) { 274 prereference.performClick(); 275 } 276 } 277 } 278 279 @Override onClick(View v)280 public void onClick(View v) { 281 if (mAddNewServiceButton == v) { 282 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 283 if (addNewServiceIntent != null) { // check again just in case. 284 try { 285 startActivity(addNewServiceIntent); 286 } catch (ActivityNotFoundException e) { 287 Log.w(TAG, "Unable to start activity", e); 288 } 289 } 290 } 291 } 292 293 private final class PrintJobsController implements LoaderCallbacks<List<PrintJobInfo>> { 294 295 @Override onCreateLoader(int id, Bundle args)296 public Loader<List<PrintJobInfo>> onCreateLoader(int id, Bundle args) { 297 if (id == LOADER_ID_PRINT_JOBS_LOADER) { 298 return new PrintJobsLoader(getContext()); 299 } 300 return null; 301 } 302 303 @Override onLoadFinished(Loader<List<PrintJobInfo>> loader, List<PrintJobInfo> printJobs)304 public void onLoadFinished(Loader<List<PrintJobInfo>> loader, 305 List<PrintJobInfo> printJobs) { 306 if (printJobs == null || printJobs.isEmpty()) { 307 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 308 } else { 309 if (getPreferenceScreen().findPreference(PRINT_JOBS_CATEGORY) == null) { 310 getPreferenceScreen().addPreference(mActivePrintJobsCategory); 311 } 312 313 mActivePrintJobsCategory.removeAll(); 314 final Context context = getPrefContext(); 315 if (context == null) { 316 Log.w(TAG, "No preference context, skip adding print jobs"); 317 return; 318 } 319 320 for (PrintJobInfo printJob : printJobs) { 321 Preference preference = new Preference(context); 322 323 preference.setPersistent(false); 324 preference.setFragment(PrintJobSettingsFragment.class.getName()); 325 preference.setKey(printJob.getId().flattenToString()); 326 327 switch (printJob.getState()) { 328 case PrintJobInfo.STATE_QUEUED: 329 case PrintJobInfo.STATE_STARTED: { 330 if (!printJob.isCancelling()) { 331 preference.setTitle(getString( 332 R.string.print_printing_state_title_template, 333 printJob.getLabel())); 334 } else { 335 preference.setTitle(getString( 336 R.string.print_cancelling_state_title_template, 337 printJob.getLabel())); 338 } 339 } break; 340 341 case PrintJobInfo.STATE_FAILED: { 342 preference.setTitle(getString( 343 R.string.print_failed_state_title_template, 344 printJob.getLabel())); 345 } break; 346 347 case PrintJobInfo.STATE_BLOCKED: { 348 if (!printJob.isCancelling()) { 349 preference.setTitle(getString( 350 R.string.print_blocked_state_title_template, 351 printJob.getLabel())); 352 } else { 353 preference.setTitle(getString( 354 R.string.print_cancelling_state_title_template, 355 printJob.getLabel())); 356 } 357 } break; 358 } 359 360 preference.setSummary(getString(R.string.print_job_summary, 361 printJob.getPrinterName(), DateUtils.formatSameDayTime( 362 printJob.getCreationTime(), printJob.getCreationTime(), 363 DateFormat.SHORT, DateFormat.SHORT))); 364 365 switch (printJob.getState()) { 366 case PrintJobInfo.STATE_QUEUED: 367 case PrintJobInfo.STATE_STARTED: { 368 preference.setIcon(R.drawable.ic_print); 369 } break; 370 371 case PrintJobInfo.STATE_FAILED: 372 case PrintJobInfo.STATE_BLOCKED: { 373 preference.setIcon(R.drawable.ic_print_error); 374 } break; 375 } 376 377 Bundle extras = preference.getExtras(); 378 extras.putString(EXTRA_PRINT_JOB_ID, printJob.getId().flattenToString()); 379 380 mActivePrintJobsCategory.addPreference(preference); 381 } 382 } 383 } 384 385 @Override onLoaderReset(Loader<List<PrintJobInfo>> loader)386 public void onLoaderReset(Loader<List<PrintJobInfo>> loader) { 387 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 388 } 389 } 390 391 private static final class PrintJobsLoader extends AsyncTaskLoader<List<PrintJobInfo>> { 392 393 private static final String LOG_TAG = "PrintJobsLoader"; 394 395 private static final boolean DEBUG = false; 396 397 private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); 398 399 private final PrintManager mPrintManager; 400 401 private PrintJobStateChangeListener mPrintJobStateChangeListener; 402 PrintJobsLoader(Context context)403 public PrintJobsLoader(Context context) { 404 super(context); 405 mPrintManager = ((PrintManager) context.getSystemService( 406 Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( 407 context.getUserId()); 408 } 409 410 @Override deliverResult(List<PrintJobInfo> printJobs)411 public void deliverResult(List<PrintJobInfo> printJobs) { 412 if (isStarted()) { 413 super.deliverResult(printJobs); 414 } 415 } 416 417 @Override onStartLoading()418 protected void onStartLoading() { 419 if (DEBUG) { 420 Log.i(LOG_TAG, "onStartLoading()"); 421 } 422 // If we already have a result, deliver it immediately. 423 if (!mPrintJobs.isEmpty()) { 424 deliverResult(new ArrayList<PrintJobInfo>(mPrintJobs)); 425 } 426 // Start watching for changes. 427 if (mPrintJobStateChangeListener == null) { 428 mPrintJobStateChangeListener = new PrintJobStateChangeListener() { 429 @Override 430 public void onPrintJobStateChanged(PrintJobId printJobId) { 431 onForceLoad(); 432 } 433 }; 434 mPrintManager.addPrintJobStateChangeListener( 435 mPrintJobStateChangeListener); 436 } 437 // If the data changed or we have no data - load it now. 438 if (mPrintJobs.isEmpty()) { 439 onForceLoad(); 440 } 441 } 442 443 @Override onStopLoading()444 protected void onStopLoading() { 445 if (DEBUG) { 446 Log.i(LOG_TAG, "onStopLoading()"); 447 } 448 // Cancel the load in progress if possible. 449 onCancelLoad(); 450 } 451 452 @Override onReset()453 protected void onReset() { 454 if (DEBUG) { 455 Log.i(LOG_TAG, "onReset()"); 456 } 457 // Stop loading. 458 onStopLoading(); 459 // Clear the cached result. 460 mPrintJobs.clear(); 461 // Stop watching for changes. 462 if (mPrintJobStateChangeListener != null) { 463 mPrintManager.removePrintJobStateChangeListener( 464 mPrintJobStateChangeListener); 465 mPrintJobStateChangeListener = null; 466 } 467 } 468 469 @Override loadInBackground()470 public List<PrintJobInfo> loadInBackground() { 471 List<PrintJobInfo> printJobInfos = null; 472 List<PrintJob> printJobs = mPrintManager.getPrintJobs(); 473 final int printJobCount = printJobs.size(); 474 for (int i = 0; i < printJobCount; i++) { 475 PrintJobInfo printJob = printJobs.get(i).getInfo(); 476 if (shouldShowToUser(printJob)) { 477 if (printJobInfos == null) { 478 printJobInfos = new ArrayList<PrintJobInfo>(); 479 } 480 printJobInfos.add(printJob); 481 } 482 } 483 return printJobInfos; 484 } 485 } 486 487 /** 488 * Should the print job the shown to the user in the settings app. 489 * 490 * @param printJob The print job in question. 491 * @return true iff the print job should be shown. 492 */ shouldShowToUser(PrintJobInfo printJob)493 private static boolean shouldShowToUser(PrintJobInfo printJob) { 494 switch (printJob.getState()) { 495 case PrintJobInfo.STATE_QUEUED: 496 case PrintJobInfo.STATE_STARTED: 497 case PrintJobInfo.STATE_BLOCKED: 498 case PrintJobInfo.STATE_FAILED: { 499 return true; 500 } 501 } 502 return false; 503 } 504 505 /** 506 * Provider for the print settings summary 507 */ 508 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 509 static class PrintSummaryProvider implements SummaryLoader.SummaryProvider { 510 private final Context mContext; 511 private final PrintManagerWrapper mPrintManager; 512 private final SummaryLoader mSummaryLoader; 513 514 /** 515 * Create a new {@link PrintSummaryProvider}. 516 * 517 * @param context The context this provider is for 518 * @param summaryLoader The summary load using this provider 519 */ PrintSummaryProvider(Context context, SummaryLoader summaryLoader, PrintManagerWrapper printManager)520 PrintSummaryProvider(Context context, SummaryLoader summaryLoader, 521 PrintManagerWrapper printManager) { 522 mContext = context; 523 mSummaryLoader = summaryLoader; 524 mPrintManager = printManager; 525 } 526 527 @Override setListening(boolean isListening)528 public void setListening(boolean isListening) { 529 if (mPrintManager != null) { 530 if (isListening) { 531 List<PrintServiceInfo> services = 532 mPrintManager.getPrintServices(PrintManager.ENABLED_SERVICES); 533 if (services == null || services.isEmpty()) { 534 mSummaryLoader.setSummary(this, 535 mContext.getString(R.string.print_settings_summary_no_service)); 536 } else { 537 final int count = services.size(); 538 mSummaryLoader.setSummary(this, 539 mContext.getResources().getQuantityString( 540 R.plurals.print_settings_summary, count, count)); 541 } 542 } 543 } 544 } 545 546 static class PrintManagerWrapper { 547 548 private final PrintManager mPrintManager; 549 PrintManagerWrapper(Context context)550 PrintManagerWrapper(Context context) { 551 mPrintManager = ((PrintManager) context.getSystemService(Context.PRINT_SERVICE)) 552 .getGlobalPrintManagerForUser(context.getUserId()); 553 } 554 getPrintServices(int selectionFlags)555 public List<PrintServiceInfo> getPrintServices(int selectionFlags) { 556 return mPrintManager.getPrintServices(selectionFlags); 557 } 558 } 559 } 560 561 /** 562 * A factory for {@link PrintSummaryProvider providers} the settings app can use to read the 563 * print summary. 564 */ 565 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY = 566 (activity, summaryLoader) -> new PrintSummaryProvider(activity, summaryLoader, 567 new PrintSummaryProvider.PrintManagerWrapper(activity)); 568 569 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 570 new BaseSearchIndexProvider() { 571 @Override 572 public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { 573 List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>(); 574 575 PackageManager packageManager = context.getPackageManager(); 576 PrintManager printManager = (PrintManager) context.getSystemService( 577 Context.PRINT_SERVICE); 578 579 String screenTitle = context.getResources().getString(R.string.print_settings); 580 SearchIndexableRaw data = new SearchIndexableRaw(context); 581 data.title = screenTitle; 582 data.screenTitle = screenTitle; 583 indexables.add(data); 584 585 // Indexing all services, regardless if enabled. Please note that the index will not be 586 // updated until this function is called again 587 List<PrintServiceInfo> services = 588 printManager.getPrintServices(PrintManager.ALL_SERVICES); 589 590 if (services != null) { 591 final int serviceCount = services.size(); 592 for (int i = 0; i < serviceCount; i++) { 593 PrintServiceInfo service = services.get(i); 594 595 ComponentName componentName = new ComponentName( 596 service.getResolveInfo().serviceInfo.packageName, 597 service.getResolveInfo().serviceInfo.name); 598 599 data = new SearchIndexableRaw(context); 600 data.key = componentName.flattenToString(); 601 data.title = service.getResolveInfo().loadLabel(packageManager).toString(); 602 data.screenTitle = screenTitle; 603 indexables.add(data); 604 } 605 } 606 607 return indexables; 608 } 609 610 @Override 611 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, 612 boolean enabled) { 613 List<SearchIndexableResource> indexables = new ArrayList<SearchIndexableResource>(); 614 SearchIndexableResource indexable = new SearchIndexableResource(context); 615 indexable.xmlResId = R.xml.print_settings; 616 indexables.add(indexable); 617 return indexables; 618 } 619 }; 620 } 621