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 96 public int getMetricsCategory() { 97 return MetricsEvent.PRINT_SETTINGS; 98 } 99 100 @Override 101 protected int getHelpResource() { 102 return R.string.help_uri_printing; 103 } 104 105 @Override 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 127 public void onStart() { 128 super.onStart(); 129 setHasOptionsMenu(true); 130 startSubSettingsIfNeeded(); 131 } 132 133 @Override 134 public void onStop() { 135 super.onStop(); 136 } 137 138 @Override 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 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 LoaderCallbacks<List<PrintServiceInfo>> { 168 @Override 169 public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) { 170 PrintManager printManager = 171 (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE); 172 if (printManager != null) { 173 return new PrintServicesLoader(printManager, getContext(), 174 PrintManager.ALL_SERVICES); 175 } else { 176 return null; 177 } 178 } 179 180 @Override 181 public void onLoadFinished(Loader<List<PrintServiceInfo>> loader, 182 List<PrintServiceInfo> services) { 183 if (services.isEmpty()) { 184 getPreferenceScreen().removePreference(mPrintServicesCategory); 185 return; 186 } else if (getPreferenceScreen().findPreference(PRINT_SERVICES_CATEGORY) == null) { 187 getPreferenceScreen().addPreference(mPrintServicesCategory); 188 } 189 190 mPrintServicesCategory.removeAll(); 191 PackageManager pm = getActivity().getPackageManager(); 192 final Context context = getPrefContext(); 193 if (context == null) { 194 Log.w(TAG, "No preference context, skip adding print services"); 195 return; 196 } 197 198 for (PrintServiceInfo service : services) { 199 Preference preference = new Preference(context); 200 201 String title = service.getResolveInfo().loadLabel(pm).toString(); 202 preference.setTitle(title); 203 204 ComponentName componentName = service.getComponentName(); 205 preference.setKey(componentName.flattenToString()); 206 207 preference.setFragment(PrintServiceSettingsFragment.class.getName()); 208 preference.setPersistent(false); 209 210 if (service.isEnabled()) { 211 preference.setSummary(getString(R.string.print_feature_state_on)); 212 } else { 213 preference.setSummary(getString(R.string.print_feature_state_off)); 214 } 215 216 Drawable drawable = service.getResolveInfo().loadIcon(pm); 217 if (drawable != null) { 218 preference.setIcon(drawable); 219 } 220 221 Bundle extras = preference.getExtras(); 222 extras.putBoolean(EXTRA_CHECKED, service.isEnabled()); 223 extras.putString(EXTRA_TITLE, title); 224 extras.putString(EXTRA_SERVICE_COMPONENT_NAME, componentName.flattenToString()); 225 226 mPrintServicesCategory.addPreference(preference); 227 } 228 229 Preference addNewServicePreference = newAddServicePreferenceOrNull(); 230 if (addNewServicePreference != null) { 231 mPrintServicesCategory.addPreference(addNewServicePreference); 232 } 233 } 234 235 @Override 236 public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) { 237 getPreferenceScreen().removePreference(mPrintServicesCategory); 238 } 239 } 240 241 private Preference newAddServicePreferenceOrNull() { 242 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 243 if (addNewServiceIntent == null) { 244 return null; 245 } 246 Preference preference = new Preference(getPrefContext()); 247 preference.setTitle(R.string.print_menu_item_add_service); 248 preference.setIcon(R.drawable.ic_menu_add); 249 preference.setOrder(ORDER_LAST); 250 preference.setIntent(addNewServiceIntent); 251 preference.setPersistent(false); 252 return preference; 253 } 254 255 private Intent createAddNewServiceIntentOrNull() { 256 final String searchUri = Settings.Secure.getString(getContentResolver(), 257 Settings.Secure.PRINT_SERVICE_SEARCH_URI); 258 if (TextUtils.isEmpty(searchUri)) { 259 return null; 260 } 261 return new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); 262 } 263 264 private void startSubSettingsIfNeeded() { 265 if (getArguments() == null) { 266 return; 267 } 268 String componentName = getArguments().getString(EXTRA_PRINT_SERVICE_COMPONENT_NAME); 269 if (componentName != null) { 270 getArguments().remove(EXTRA_PRINT_SERVICE_COMPONENT_NAME); 271 Preference prereference = findPreference(componentName); 272 if (prereference != null) { 273 prereference.performClick(); 274 } 275 } 276 } 277 278 @Override 279 public void onClick(View v) { 280 if (mAddNewServiceButton == v) { 281 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 282 if (addNewServiceIntent != null) { // check again just in case. 283 try { 284 startActivity(addNewServiceIntent); 285 } catch (ActivityNotFoundException e) { 286 Log.w(TAG, "Unable to start activity", e); 287 } 288 } 289 } 290 } 291 292 private final class PrintJobsController implements LoaderCallbacks<List<PrintJobInfo>> { 293 294 @Override 295 public Loader<List<PrintJobInfo>> onCreateLoader(int id, Bundle args) { 296 if (id == LOADER_ID_PRINT_JOBS_LOADER) { 297 return new PrintJobsLoader(getContext()); 298 } 299 return null; 300 } 301 302 @Override 303 public void onLoadFinished(Loader<List<PrintJobInfo>> loader, 304 List<PrintJobInfo> printJobs) { 305 if (printJobs == null || printJobs.isEmpty()) { 306 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 307 } else { 308 if (getPreferenceScreen().findPreference(PRINT_JOBS_CATEGORY) == null) { 309 getPreferenceScreen().addPreference(mActivePrintJobsCategory); 310 } 311 312 mActivePrintJobsCategory.removeAll(); 313 final Context context = getPrefContext(); 314 if (context == null) { 315 Log.w(TAG, "No preference context, skip adding print jobs"); 316 return; 317 } 318 319 for (PrintJobInfo printJob : printJobs) { 320 Preference preference = new Preference(context); 321 322 preference.setPersistent(false); 323 preference.setFragment(PrintJobSettingsFragment.class.getName()); 324 preference.setKey(printJob.getId().flattenToString()); 325 326 switch (printJob.getState()) { 327 case PrintJobInfo.STATE_QUEUED: 328 case PrintJobInfo.STATE_STARTED: 329 if (!printJob.isCancelling()) { 330 preference.setTitle(getString( 331 R.string.print_printing_state_title_template, 332 printJob.getLabel())); 333 } else { 334 preference.setTitle(getString( 335 R.string.print_cancelling_state_title_template, 336 printJob.getLabel())); 337 } 338 break; 339 case PrintJobInfo.STATE_FAILED: 340 preference.setTitle(getString( 341 R.string.print_failed_state_title_template, 342 printJob.getLabel())); 343 break; 344 case PrintJobInfo.STATE_BLOCKED: 345 if (!printJob.isCancelling()) { 346 preference.setTitle(getString( 347 R.string.print_blocked_state_title_template, 348 printJob.getLabel())); 349 } else { 350 preference.setTitle(getString( 351 R.string.print_cancelling_state_title_template, 352 printJob.getLabel())); 353 } 354 break; 355 } 356 357 preference.setSummary(getString(R.string.print_job_summary, 358 printJob.getPrinterName(), DateUtils.formatSameDayTime( 359 printJob.getCreationTime(), printJob.getCreationTime(), 360 DateFormat.SHORT, DateFormat.SHORT))); 361 362 switch (printJob.getState()) { 363 case PrintJobInfo.STATE_QUEUED: 364 case PrintJobInfo.STATE_STARTED: 365 preference.setIcon(R.drawable.ic_print); 366 break; 367 case PrintJobInfo.STATE_FAILED: 368 case PrintJobInfo.STATE_BLOCKED: 369 preference.setIcon(R.drawable.ic_print_error); 370 break; 371 } 372 373 Bundle extras = preference.getExtras(); 374 extras.putString(EXTRA_PRINT_JOB_ID, printJob.getId().flattenToString()); 375 376 mActivePrintJobsCategory.addPreference(preference); 377 } 378 } 379 } 380 381 @Override 382 public void onLoaderReset(Loader<List<PrintJobInfo>> loader) { 383 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 384 } 385 } 386 387 private static final class PrintJobsLoader extends AsyncTaskLoader<List<PrintJobInfo>> { 388 389 private static final String LOG_TAG = "PrintJobsLoader"; 390 391 private static final boolean DEBUG = false; 392 393 private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); 394 395 private final PrintManager mPrintManager; 396 397 private PrintJobStateChangeListener mPrintJobStateChangeListener; 398 399 public PrintJobsLoader(Context context) { 400 super(context); 401 mPrintManager = ((PrintManager) context.getSystemService( 402 Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( 403 context.getUserId()); 404 } 405 406 @Override 407 public void deliverResult(List<PrintJobInfo> printJobs) { 408 if (isStarted()) { 409 super.deliverResult(printJobs); 410 } 411 } 412 413 @Override 414 protected void onStartLoading() { 415 if (DEBUG) { 416 Log.i(LOG_TAG, "onStartLoading()"); 417 } 418 // If we already have a result, deliver it immediately. 419 if (!mPrintJobs.isEmpty()) { 420 deliverResult(new ArrayList<PrintJobInfo>(mPrintJobs)); 421 } 422 // Start watching for changes. 423 if (mPrintJobStateChangeListener == null) { 424 mPrintJobStateChangeListener = new PrintJobStateChangeListener() { 425 @Override 426 public void onPrintJobStateChanged(PrintJobId printJobId) { 427 onForceLoad(); 428 } 429 }; 430 mPrintManager.addPrintJobStateChangeListener( 431 mPrintJobStateChangeListener); 432 } 433 // If the data changed or we have no data - load it now. 434 if (mPrintJobs.isEmpty()) { 435 onForceLoad(); 436 } 437 } 438 439 @Override 440 protected void onStopLoading() { 441 if (DEBUG) { 442 Log.i(LOG_TAG, "onStopLoading()"); 443 } 444 // Cancel the load in progress if possible. 445 onCancelLoad(); 446 } 447 448 @Override 449 protected void onReset() { 450 if (DEBUG) { 451 Log.i(LOG_TAG, "onReset()"); 452 } 453 // Stop loading. 454 onStopLoading(); 455 // Clear the cached result. 456 mPrintJobs.clear(); 457 // Stop watching for changes. 458 if (mPrintJobStateChangeListener != null) { 459 mPrintManager.removePrintJobStateChangeListener( 460 mPrintJobStateChangeListener); 461 mPrintJobStateChangeListener = null; 462 } 463 } 464 465 @Override 466 public List<PrintJobInfo> loadInBackground() { 467 List<PrintJobInfo> printJobInfos = null; 468 List<PrintJob> printJobs = mPrintManager.getPrintJobs(); 469 final int printJobCount = printJobs.size(); 470 for (int i = 0; i < printJobCount; i++) { 471 PrintJobInfo printJob = printJobs.get(i).getInfo(); 472 if (shouldShowToUser(printJob)) { 473 if (printJobInfos == null) { 474 printJobInfos = new ArrayList<>(); 475 } 476 printJobInfos.add(printJob); 477 } 478 } 479 return printJobInfos; 480 } 481 } 482 483 /** 484 * Should the print job the shown to the user in the settings app. 485 * 486 * @param printJob The print job in question. 487 * @return true iff the print job should be shown. 488 */ 489 private static boolean shouldShowToUser(PrintJobInfo printJob) { 490 switch (printJob.getState()) { 491 case PrintJobInfo.STATE_QUEUED: 492 case PrintJobInfo.STATE_STARTED: 493 case PrintJobInfo.STATE_BLOCKED: 494 case PrintJobInfo.STATE_FAILED: { 495 return true; 496 } 497 } 498 return false; 499 } 500 501 /** 502 * Provider for the print settings summary 503 */ 504 @VisibleForTesting 505 static class PrintSummaryProvider 506 implements SummaryLoader.SummaryProvider, PrintJobStateChangeListener { 507 private final Context mContext; 508 private final PrintManagerWrapper mPrintManager; 509 private final SummaryLoader mSummaryLoader; 510 511 /** 512 * Create a new {@link PrintSummaryProvider}. 513 * 514 * @param context The context this provider is for 515 * @param summaryLoader The summary load using this provider 516 */ 517 PrintSummaryProvider(Context context, SummaryLoader summaryLoader, 518 PrintManagerWrapper printManager) { 519 mContext = context; 520 mSummaryLoader = summaryLoader; 521 mPrintManager = printManager; 522 } 523 524 @Override 525 public void setListening(boolean isListening) { 526 if (mPrintManager != null) { 527 if (isListening) { 528 mPrintManager.addPrintJobStateChanegListner(this); 529 onPrintJobStateChanged(null); 530 } else { 531 mPrintManager.removePrintJobStateChangeListener(this); 532 } 533 } 534 } 535 536 @Override 537 public void onPrintJobStateChanged(PrintJobId printJobId) { 538 final List<PrintJob> printJobs = mPrintManager.getPrintJobs(); 539 540 int numActivePrintJobs = 0; 541 if (printJobs != null) { 542 for (PrintJob job : printJobs) { 543 if (shouldShowToUser(job.getInfo())) { 544 numActivePrintJobs++; 545 } 546 } 547 } 548 549 if (numActivePrintJobs > 0) { 550 mSummaryLoader.setSummary(this, mContext.getResources().getQuantityString( 551 R.plurals.print_jobs_summary, numActivePrintJobs, numActivePrintJobs)); 552 } else { 553 List<PrintServiceInfo> services = 554 mPrintManager.getPrintServices(PrintManager.ENABLED_SERVICES); 555 if (services == null || services.isEmpty()) { 556 mSummaryLoader.setSummary(this, 557 mContext.getString(R.string.print_settings_summary_no_service)); 558 } else { 559 final int count = services.size(); 560 mSummaryLoader.setSummary(this, 561 mContext.getResources().getQuantityString( 562 R.plurals.print_settings_summary, count, count)); 563 } 564 } 565 } 566 567 static class PrintManagerWrapper { 568 569 private final PrintManager mPrintManager; 570 571 PrintManagerWrapper(Context context) { 572 mPrintManager = ((PrintManager) context.getSystemService(Context.PRINT_SERVICE)) 573 .getGlobalPrintManagerForUser(context.getUserId()); 574 } 575 576 public List<PrintServiceInfo> getPrintServices(int selectionFlags) { 577 return mPrintManager.getPrintServices(selectionFlags); 578 } 579 580 public void addPrintJobStateChanegListner(PrintJobStateChangeListener listener) { 581 mPrintManager.addPrintJobStateChangeListener(listener); 582 } 583 584 public void removePrintJobStateChangeListener(PrintJobStateChangeListener listener) { 585 mPrintManager.removePrintJobStateChangeListener(listener); 586 } 587 588 public List<PrintJob> getPrintJobs() { 589 return mPrintManager.getPrintJobs(); 590 } 591 } 592 } 593 594 /** 595 * A factory for {@link PrintSummaryProvider providers} the settings app can use to read the 596 * print summary. 597 */ 598 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY = 599 (activity, summaryLoader) -> new PrintSummaryProvider(activity, summaryLoader, 600 new PrintSummaryProvider.PrintManagerWrapper(activity)); 601 602 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 603 new BaseSearchIndexProvider() { 604 @Override 605 public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { 606 List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>(); 607 608 PackageManager packageManager = context.getPackageManager(); 609 PrintManager printManager = (PrintManager) context.getSystemService( 610 Context.PRINT_SERVICE); 611 612 String screenTitle = context.getResources().getString(R.string.print_settings); 613 SearchIndexableRaw data = new SearchIndexableRaw(context); 614 data.title = screenTitle; 615 data.screenTitle = screenTitle; 616 indexables.add(data); 617 618 // Indexing all services, regardless if enabled. Please note that the index will not be 619 // updated until this function is called again 620 List<PrintServiceInfo> services = 621 printManager.getPrintServices(PrintManager.ALL_SERVICES); 622 623 if (services != null) { 624 final int serviceCount = services.size(); 625 for (int i = 0; i < serviceCount; i++) { 626 PrintServiceInfo service = services.get(i); 627 628 ComponentName componentName = new ComponentName( 629 service.getResolveInfo().serviceInfo.packageName, 630 service.getResolveInfo().serviceInfo.name); 631 632 data = new SearchIndexableRaw(context); 633 data.key = componentName.flattenToString(); 634 data.title = service.getResolveInfo().loadLabel(packageManager).toString(); 635 data.screenTitle = screenTitle; 636 indexables.add(data); 637 } 638 } 639 640 return indexables; 641 } 642 643 @Override 644 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, 645 boolean enabled) { 646 List<SearchIndexableResource> indexables = new ArrayList<SearchIndexableResource>(); 647 SearchIndexableResource indexable = new SearchIndexableResource(context); 648 indexable.xmlResId = R.xml.print_settings; 649 indexables.add(indexable); 650 return indexables; 651 } 652 }; 653 } 654