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