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.Activity; 20 import android.app.LoaderManager.LoaderCallbacks; 21 import android.content.ActivityNotFoundException; 22 import android.content.AsyncTaskLoader; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.content.pm.PackageManager; 28 import android.graphics.drawable.Drawable; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.print.PrintJob; 32 import android.print.PrintJobId; 33 import android.print.PrintJobInfo; 34 import android.print.PrintManager; 35 import android.print.PrintManager.PrintJobStateChangeListener; 36 import android.print.PrintServicesLoader; 37 import android.printservice.PrintServiceInfo; 38 import android.provider.SearchIndexableResource; 39 import android.provider.Settings; 40 import android.support.v7.preference.Preference; 41 import android.support.v7.preference.PreferenceCategory; 42 import android.support.v7.preference.PreferenceScreen; 43 import android.text.TextUtils; 44 import android.text.format.DateUtils; 45 import android.util.Log; 46 import android.view.LayoutInflater; 47 import android.view.View; 48 import android.view.View.OnClickListener; 49 import android.view.ViewGroup; 50 import android.widget.Button; 51 import android.widget.TextView; 52 53 import com.android.internal.logging.MetricsProto.MetricsEvent; 54 import com.android.settings.DialogCreatable; 55 import com.android.settings.R; 56 import com.android.settings.utils.ProfileSettingsPreferenceFragment; 57 import com.android.settings.dashboard.SummaryLoader; 58 import com.android.settings.search.BaseSearchIndexProvider; 59 import com.android.settings.search.Indexable; 60 import com.android.settings.search.SearchIndexableRaw; 61 62 import java.text.DateFormat; 63 import java.util.ArrayList; 64 import java.util.List; 65 66 /** 67 * Fragment with the top level print settings. 68 */ 69 public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment 70 implements DialogCreatable, Indexable, OnClickListener { 71 public static final String TAG = "PrintSettingsFragment"; 72 private static final int LOADER_ID_PRINT_JOBS_LOADER = 1; 73 private static final int LOADER_ID_PRINT_SERVICES = 2; 74 75 private static final String PRINT_JOBS_CATEGORY = "print_jobs_category"; 76 private static final String PRINT_SERVICES_CATEGORY = "print_services_category"; 77 78 static final String EXTRA_CHECKED = "EXTRA_CHECKED"; 79 static final String EXTRA_TITLE = "EXTRA_TITLE"; 80 static final String EXTRA_SERVICE_COMPONENT_NAME = "EXTRA_SERVICE_COMPONENT_NAME"; 81 82 static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID"; 83 84 private static final String EXTRA_PRINT_SERVICE_COMPONENT_NAME = 85 "EXTRA_PRINT_SERVICE_COMPONENT_NAME"; 86 87 private static final int ORDER_LAST = Preference.DEFAULT_ORDER - 1; 88 89 private PreferenceCategory mActivePrintJobsCategory; 90 private PreferenceCategory mPrintServicesCategory; 91 92 private PrintJobsController mPrintJobsController; 93 private PrintServicesController mPrintServicesController; 94 95 private Button mAddNewServiceButton; 96 97 @Override getMetricsCategory()98 protected int getMetricsCategory() { 99 return MetricsEvent.PRINT_SETTINGS; 100 } 101 102 @Override getHelpResource()103 protected int getHelpResource() { 104 return R.string.help_uri_printing; 105 } 106 107 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)108 public View onCreateView(LayoutInflater inflater, ViewGroup container, 109 Bundle savedInstanceState) { 110 View root = super.onCreateView(inflater, container, savedInstanceState); 111 addPreferencesFromResource(R.xml.print_settings); 112 113 mActivePrintJobsCategory = (PreferenceCategory) findPreference( 114 PRINT_JOBS_CATEGORY); 115 mPrintServicesCategory = (PreferenceCategory) findPreference( 116 PRINT_SERVICES_CATEGORY); 117 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 118 119 mPrintJobsController = new PrintJobsController(); 120 getLoaderManager().initLoader(LOADER_ID_PRINT_JOBS_LOADER, null, mPrintJobsController); 121 122 mPrintServicesController = new PrintServicesController(); 123 getLoaderManager().initLoader(LOADER_ID_PRINT_SERVICES, null, mPrintServicesController); 124 125 return root; 126 } 127 128 @Override onStart()129 public void onStart() { 130 super.onStart(); 131 setHasOptionsMenu(true); 132 startSubSettingsIfNeeded(); 133 } 134 135 @Override onStop()136 public void onStop() { 137 super.onStop(); 138 } 139 140 @Override onViewCreated(View view, Bundle savedInstanceState)141 public void onViewCreated(View view, Bundle savedInstanceState) { 142 super.onViewCreated(view, savedInstanceState); 143 ViewGroup contentRoot = (ViewGroup) getListView().getParent(); 144 View emptyView = getActivity().getLayoutInflater().inflate( 145 R.layout.empty_print_state, contentRoot, false); 146 TextView textView = (TextView) emptyView.findViewById(R.id.message); 147 textView.setText(R.string.print_no_services_installed); 148 149 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 150 if (addNewServiceIntent != null) { 151 mAddNewServiceButton = (Button) emptyView.findViewById(R.id.add_new_service); 152 mAddNewServiceButton.setOnClickListener(this); 153 // The empty is used elsewhere too so it's hidden by default. 154 mAddNewServiceButton.setVisibility(View.VISIBLE); 155 } 156 157 contentRoot.addView(emptyView); 158 setEmptyView(emptyView); 159 } 160 161 @Override getIntentActionString()162 protected String getIntentActionString() { 163 return Settings.ACTION_PRINT_SETTINGS; 164 } 165 166 /** 167 * Adds preferences for all print services to the {@value PRINT_SERVICES_CATEGORY} cathegory. 168 */ 169 private final class PrintServicesController implements 170 LoaderCallbacks<List<PrintServiceInfo>> { 171 @Override onCreateLoader(int id, Bundle args)172 public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) { 173 PrintManager printManager = 174 (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE); 175 if (printManager != null) { 176 return new PrintServicesLoader(printManager, getContext(), 177 PrintManager.ALL_SERVICES); 178 } else { 179 return null; 180 } 181 } 182 183 @Override onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)184 public void onLoadFinished(Loader<List<PrintServiceInfo>> loader, 185 List<PrintServiceInfo> services) { 186 if (services.isEmpty()) { 187 getPreferenceScreen().removePreference(mPrintServicesCategory); 188 return; 189 } else if (getPreferenceScreen().findPreference(PRINT_SERVICES_CATEGORY) == null) { 190 getPreferenceScreen().addPreference(mPrintServicesCategory); 191 } 192 193 mPrintServicesCategory.removeAll(); 194 PackageManager pm = getActivity().getPackageManager(); 195 196 final int numServices = services.size(); 197 for (int i = 0; i < numServices; i++) { 198 PrintServiceInfo service = services.get(i); 199 PreferenceScreen preference = getPreferenceManager().createPreferenceScreen( 200 getActivity()); 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 315 final int printJobCount = printJobs.size(); 316 for (int i = 0; i < printJobCount; i++) { 317 PrintJobInfo printJob = printJobs.get(i); 318 319 PreferenceScreen preference = getPreferenceManager() 320 .createPreferenceScreen(getActivity()); 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 340 case PrintJobInfo.STATE_FAILED: { 341 preference.setTitle(getString( 342 R.string.print_failed_state_title_template, 343 printJob.getLabel())); 344 } break; 345 346 case PrintJobInfo.STATE_BLOCKED: { 347 if (!printJob.isCancelling()) { 348 preference.setTitle(getString( 349 R.string.print_blocked_state_title_template, 350 printJob.getLabel())); 351 } else { 352 preference.setTitle(getString( 353 R.string.print_cancelling_state_title_template, 354 printJob.getLabel())); 355 } 356 } break; 357 } 358 359 preference.setSummary(getString(R.string.print_job_summary, 360 printJob.getPrinterName(), DateUtils.formatSameDayTime( 361 printJob.getCreationTime(), printJob.getCreationTime(), 362 DateFormat.SHORT, DateFormat.SHORT))); 363 364 switch (printJob.getState()) { 365 case PrintJobInfo.STATE_QUEUED: 366 case PrintJobInfo.STATE_STARTED: { 367 preference.setIcon(R.drawable.ic_print); 368 } break; 369 370 case PrintJobInfo.STATE_FAILED: 371 case PrintJobInfo.STATE_BLOCKED: { 372 preference.setIcon(R.drawable.ic_print_error); 373 } break; 374 } 375 376 Bundle extras = preference.getExtras(); 377 extras.putString(EXTRA_PRINT_JOB_ID, printJob.getId().flattenToString()); 378 379 mActivePrintJobsCategory.addPreference(preference); 380 } 381 } 382 } 383 384 @Override onLoaderReset(Loader<List<PrintJobInfo>> loader)385 public void onLoaderReset(Loader<List<PrintJobInfo>> loader) { 386 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 387 } 388 } 389 390 private static final class PrintJobsLoader extends AsyncTaskLoader<List<PrintJobInfo>> { 391 392 private static final String LOG_TAG = "PrintJobsLoader"; 393 394 private static final boolean DEBUG = false; 395 396 private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); 397 398 private final PrintManager mPrintManager; 399 400 private PrintJobStateChangeListener mPrintJobStateChangeListener; 401 PrintJobsLoader(Context context)402 public PrintJobsLoader(Context context) { 403 super(context); 404 mPrintManager = ((PrintManager) context.getSystemService( 405 Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( 406 context.getUserId()); 407 } 408 409 @Override deliverResult(List<PrintJobInfo> printJobs)410 public void deliverResult(List<PrintJobInfo> printJobs) { 411 if (isStarted()) { 412 super.deliverResult(printJobs); 413 } 414 } 415 416 @Override onStartLoading()417 protected void onStartLoading() { 418 if (DEBUG) { 419 Log.i(LOG_TAG, "onStartLoading()"); 420 } 421 // If we already have a result, deliver it immediately. 422 if (!mPrintJobs.isEmpty()) { 423 deliverResult(new ArrayList<PrintJobInfo>(mPrintJobs)); 424 } 425 // Start watching for changes. 426 if (mPrintJobStateChangeListener == null) { 427 mPrintJobStateChangeListener = new PrintJobStateChangeListener() { 428 @Override 429 public void onPrintJobStateChanged(PrintJobId printJobId) { 430 onForceLoad(); 431 } 432 }; 433 mPrintManager.addPrintJobStateChangeListener( 434 mPrintJobStateChangeListener); 435 } 436 // If the data changed or we have no data - load it now. 437 if (mPrintJobs.isEmpty()) { 438 onForceLoad(); 439 } 440 } 441 442 @Override onStopLoading()443 protected void onStopLoading() { 444 if (DEBUG) { 445 Log.i(LOG_TAG, "onStopLoading()"); 446 } 447 // Cancel the load in progress if possible. 448 onCancelLoad(); 449 } 450 451 @Override onReset()452 protected void onReset() { 453 if (DEBUG) { 454 Log.i(LOG_TAG, "onReset()"); 455 } 456 // Stop loading. 457 onStopLoading(); 458 // Clear the cached result. 459 mPrintJobs.clear(); 460 // Stop watching for changes. 461 if (mPrintJobStateChangeListener != null) { 462 mPrintManager.removePrintJobStateChangeListener( 463 mPrintJobStateChangeListener); 464 mPrintJobStateChangeListener = null; 465 } 466 } 467 468 @Override loadInBackground()469 public List<PrintJobInfo> loadInBackground() { 470 List<PrintJobInfo> printJobInfos = null; 471 List<PrintJob> printJobs = mPrintManager.getPrintJobs(); 472 final int printJobCount = printJobs.size(); 473 for (int i = 0; i < printJobCount; i++) { 474 PrintJobInfo printJob = printJobs.get(i).getInfo(); 475 if (shouldShowToUser(printJob)) { 476 if (printJobInfos == null) { 477 printJobInfos = new ArrayList<PrintJobInfo>(); 478 } 479 printJobInfos.add(printJob); 480 } 481 } 482 return printJobInfos; 483 } 484 } 485 486 /** 487 * Should the print job the shown to the user in the settings app. 488 * 489 * @param printJob The print job in question. 490 * @return true iff the print job should be shown. 491 */ shouldShowToUser(PrintJobInfo printJob)492 private static boolean shouldShowToUser(PrintJobInfo printJob) { 493 switch (printJob.getState()) { 494 case PrintJobInfo.STATE_QUEUED: 495 case PrintJobInfo.STATE_STARTED: 496 case PrintJobInfo.STATE_BLOCKED: 497 case PrintJobInfo.STATE_FAILED: { 498 return true; 499 } 500 } 501 return false; 502 } 503 504 /** 505 * Provider for the print settings summary 506 */ 507 private static class PrintSummaryProvider 508 implements SummaryLoader.SummaryProvider, PrintJobStateChangeListener { 509 private final Context mContext; 510 private final PrintManager mPrintManager; 511 private final SummaryLoader mSummaryLoader; 512 513 /** 514 * Create a new {@link PrintSummaryProvider}. 515 * 516 * @param context The context this provider is for 517 * @param summaryLoader The summary load using this provider 518 */ PrintSummaryProvider(Context context, SummaryLoader summaryLoader)519 public PrintSummaryProvider(Context context, SummaryLoader summaryLoader) { 520 mContext = context; 521 mSummaryLoader = summaryLoader; 522 mPrintManager = ((PrintManager) context.getSystemService(Context.PRINT_SERVICE)) 523 .getGlobalPrintManagerForUser(context.getUserId()); 524 } 525 526 @Override setListening(boolean isListening)527 public void setListening(boolean isListening) { 528 if (mPrintManager != null) { 529 if (isListening) { 530 mPrintManager.addPrintJobStateChangeListener(this); 531 onPrintJobStateChanged(null); 532 } else { 533 mPrintManager.removePrintJobStateChangeListener(this); 534 } 535 } 536 } 537 538 @Override onPrintJobStateChanged(PrintJobId printJobId)539 public void onPrintJobStateChanged(PrintJobId printJobId) { 540 List<PrintJob> printJobs = mPrintManager.getPrintJobs(); 541 542 int numActivePrintJobs = 0; 543 final int numPrintJobs = printJobs.size(); 544 for (int i = 0; i < numPrintJobs; i++) { 545 if (shouldShowToUser(printJobs.get(i).getInfo())) { 546 numActivePrintJobs++; 547 } 548 } 549 550 mSummaryLoader.setSummary(this, mContext.getResources().getQuantityString( 551 R.plurals.print_settings_title, numActivePrintJobs, numActivePrintJobs)); 552 } 553 } 554 555 /** 556 * A factory for {@link PrintSummaryProvider providers} the settings app can use to read the 557 * print summary. 558 */ 559 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 560 = new SummaryLoader.SummaryProviderFactory() { 561 @Override 562 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 563 SummaryLoader summaryLoader) { 564 return new PrintSummaryProvider(activity, summaryLoader); 565 } 566 }; 567 568 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 569 new BaseSearchIndexProvider() { 570 @Override 571 public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { 572 List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>(); 573 574 PackageManager packageManager = context.getPackageManager(); 575 PrintManager printManager = (PrintManager) context.getSystemService( 576 Context.PRINT_SERVICE); 577 578 String screenTitle = context.getResources().getString(R.string.print_settings); 579 SearchIndexableRaw data = new SearchIndexableRaw(context); 580 data.title = screenTitle; 581 data.screenTitle = screenTitle; 582 indexables.add(data); 583 584 // Indexing all services, regardless if enabled. Please note that the index will not be 585 // updated until this function is called again 586 List<PrintServiceInfo> services = 587 printManager.getPrintServices(PrintManager.ALL_SERVICES); 588 589 if (services != null) { 590 final int serviceCount = services.size(); 591 for (int i = 0; i < serviceCount; i++) { 592 PrintServiceInfo service = services.get(i); 593 594 ComponentName componentName = new ComponentName( 595 service.getResolveInfo().serviceInfo.packageName, 596 service.getResolveInfo().serviceInfo.name); 597 598 data = new SearchIndexableRaw(context); 599 data.key = componentName.flattenToString(); 600 data.title = service.getResolveInfo().loadLabel(packageManager).toString(); 601 data.summaryOn = context.getString(R.string.print_feature_state_on); 602 data.summaryOff = context.getString(R.string.print_feature_state_off); 603 data.screenTitle = screenTitle; 604 indexables.add(data); 605 } 606 } 607 608 return indexables; 609 } 610 611 @Override 612 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, 613 boolean enabled) { 614 List<SearchIndexableResource> indexables = new ArrayList<SearchIndexableResource>(); 615 SearchIndexableResource indexable = new SearchIndexableResource(context); 616 indexable.xmlResId = R.xml.print_settings; 617 indexables.add(indexable); 618 return indexables; 619 } 620 }; 621 } 622