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 static com.android.settings.print.PrintSettingPreferenceController.shouldShowToUser; 20 21 import android.app.settings.SettingsEnums; 22 import android.content.ActivityNotFoundException; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.content.res.TypedArray; 28 import android.graphics.drawable.Drawable; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.UserManager; 32 import android.print.PrintJob; 33 import android.print.PrintJobId; 34 import android.print.PrintJobInfo; 35 import android.print.PrintManager; 36 import android.print.PrintManager.PrintJobStateChangeListener; 37 import android.printservice.PrintServiceInfo; 38 import android.provider.Settings; 39 import android.text.TextUtils; 40 import android.text.format.DateUtils; 41 import android.util.Log; 42 import android.view.LayoutInflater; 43 import android.view.View; 44 import android.view.View.OnClickListener; 45 import android.view.ViewGroup; 46 import android.widget.Button; 47 import android.widget.TextView; 48 49 import androidx.annotation.NonNull; 50 import androidx.annotation.VisibleForTesting; 51 import androidx.loader.app.LoaderManager.LoaderCallbacks; 52 import androidx.loader.content.AsyncTaskLoader; 53 import androidx.loader.content.Loader; 54 import androidx.preference.Preference; 55 import androidx.preference.PreferenceCategory; 56 57 import com.android.settings.R; 58 import com.android.settings.flags.Flags; 59 import com.android.settings.search.BaseSearchIndexProvider; 60 import com.android.settings.spa.SpaActivity; 61 import com.android.settingslib.search.Indexable; 62 import com.android.settingslib.search.SearchIndexable; 63 import com.android.settingslib.widget.AppPreference; 64 65 import java.text.DateFormat; 66 import java.util.ArrayList; 67 import java.util.List; 68 69 /** 70 * Fragment with the top level print settings. 71 */ 72 @SearchIndexable 73 public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment 74 implements Indexable, OnClickListener { 75 public static final String TAG = "PrintSettingsFragment"; 76 private static final int LOADER_ID_PRINT_JOBS_LOADER = 1; 77 private static final int LOADER_ID_PRINT_SERVICES = 2; 78 79 private static final String PRINT_JOBS_CATEGORY = "print_jobs_category"; 80 private static final String PRINT_SERVICES_CATEGORY = "print_services_category"; 81 82 static final String EXTRA_CHECKED = "EXTRA_CHECKED"; 83 static final String EXTRA_TITLE = "EXTRA_TITLE"; 84 static final String EXTRA_SERVICE_COMPONENT_NAME = "EXTRA_SERVICE_COMPONENT_NAME"; 85 86 static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID"; 87 88 private static final String EXTRA_PRINT_SERVICE_COMPONENT_NAME = 89 "EXTRA_PRINT_SERVICE_COMPONENT_NAME"; 90 91 private static final int ORDER_LAST = Preference.DEFAULT_ORDER - 1; 92 93 private PreferenceCategory mActivePrintJobsCategory; 94 private PreferenceCategory mPrintServicesCategory; 95 96 private PrintJobsController mPrintJobsController; 97 private PrintServicesController mPrintServicesController; 98 99 private Button mAddNewServiceButton; 100 @VisibleForTesting 101 boolean mIsUiRestricted; 102 PrintSettingsFragment()103 public PrintSettingsFragment() { 104 super(UserManager.DISALLOW_PRINTING); 105 } 106 107 @Override onAttach(@onNull Context context)108 public void onAttach(@NonNull Context context) { 109 super.onAttach(context); 110 if (Flags.refactorPrintSettings()) { 111 SpaActivity.startSpaActivity(context, PrintSettingsPageProvider.INSTANCE.getName()); 112 finish(); 113 } 114 } 115 116 @Override getLogTag()117 protected String getLogTag() { 118 return TAG; 119 } 120 121 @Override getPreferenceScreenResId()122 protected int getPreferenceScreenResId() { 123 return R.xml.print_settings; 124 } 125 126 @Override getMetricsCategory()127 public int getMetricsCategory() { 128 return SettingsEnums.PRINT_SETTINGS; 129 } 130 131 @Override getHelpResource()132 public int getHelpResource() { 133 return R.string.help_uri_printing; 134 } 135 136 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)137 public View onCreateView(LayoutInflater inflater, ViewGroup container, 138 Bundle savedInstanceState) { 139 View root = super.onCreateView(inflater, container, savedInstanceState); 140 mIsUiRestricted = isUiRestricted(); 141 setupPreferences(); 142 return root; 143 } 144 145 @VisibleForTesting setupPreferences()146 void setupPreferences() { 147 if (mIsUiRestricted) { 148 return; 149 } 150 151 mActivePrintJobsCategory = (PreferenceCategory) findPreference(PRINT_JOBS_CATEGORY); 152 mPrintServicesCategory = (PreferenceCategory) findPreference(PRINT_SERVICES_CATEGORY); 153 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 154 155 mPrintJobsController = new PrintJobsController(); 156 getLoaderManager().initLoader(LOADER_ID_PRINT_JOBS_LOADER, null, mPrintJobsController); 157 158 mPrintServicesController = new PrintServicesController(); 159 getLoaderManager().initLoader(LOADER_ID_PRINT_SERVICES, null, mPrintServicesController); 160 } 161 162 @Override onViewCreated(View view, Bundle savedInstanceState)163 public void onViewCreated(View view, Bundle savedInstanceState) { 164 super.onViewCreated(view, savedInstanceState); 165 setupEmptyViews(); 166 } 167 168 @VisibleForTesting setupEmptyViews()169 void setupEmptyViews() { 170 if (mIsUiRestricted) { 171 return; 172 } 173 174 ViewGroup contentRoot = (ViewGroup) getListView().getParent(); 175 View emptyView = getActivity().getLayoutInflater().inflate( 176 R.layout.empty_print_state, contentRoot, false); 177 TextView textView = (TextView) emptyView.findViewById(R.id.message); 178 textView.setText(R.string.print_no_services_installed); 179 180 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 181 if (addNewServiceIntent != null) { 182 mAddNewServiceButton = (Button) emptyView.findViewById(R.id.add_new_service); 183 mAddNewServiceButton.setOnClickListener(this); 184 // The empty is used elsewhere too so it's hidden by default. 185 mAddNewServiceButton.setVisibility(View.VISIBLE); 186 } 187 188 contentRoot.addView(emptyView); 189 setEmptyView(emptyView); 190 } 191 192 @Override onStart()193 public void onStart() { 194 super.onStart(); 195 startSettings(); 196 } 197 198 @VisibleForTesting startSettings()199 void startSettings() { 200 if (mIsUiRestricted) { 201 getPreferenceScreen().removeAll(); 202 return; 203 } 204 205 setHasOptionsMenu(true); 206 startSubSettingsIfNeeded(); 207 } 208 209 @Override getIntentActionString()210 protected String getIntentActionString() { 211 return Settings.ACTION_PRINT_SETTINGS; 212 } 213 214 /** 215 * Adds preferences for all print services to the {@value PRINT_SERVICES_CATEGORY} cathegory. 216 */ 217 private final class PrintServicesController implements LoaderCallbacks<List<PrintServiceInfo>> { 218 @Override onCreateLoader(int id, Bundle args)219 public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) { 220 PrintManager printManager = 221 (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE); 222 if (printManager != null) { 223 return new SettingsPrintServicesLoader(printManager, getContext(), 224 PrintManager.ALL_SERVICES); 225 } else { 226 return null; 227 } 228 } 229 230 @Override onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)231 public void onLoadFinished(Loader<List<PrintServiceInfo>> loader, 232 List<PrintServiceInfo> services) { 233 if (services.isEmpty()) { 234 getPreferenceScreen().removePreference(mPrintServicesCategory); 235 return; 236 } else if (getPreferenceScreen().findPreference(PRINT_SERVICES_CATEGORY) == null) { 237 getPreferenceScreen().addPreference(mPrintServicesCategory); 238 } 239 240 mPrintServicesCategory.removeAll(); 241 PackageManager pm = getActivity().getPackageManager(); 242 final Context context = getPrefContext(); 243 if (context == null) { 244 Log.w(TAG, "No preference context, skip adding print services"); 245 return; 246 } 247 248 for (PrintServiceInfo service : services) { 249 AppPreference preference = new AppPreference(context); 250 251 String title = service.getResolveInfo().loadLabel(pm).toString(); 252 preference.setTitle(title); 253 254 ComponentName componentName = service.getComponentName(); 255 preference.setKey(componentName.flattenToString()); 256 257 preference.setFragment(PrintServiceSettingsFragment.class.getName()); 258 preference.setPersistent(false); 259 260 if (service.isEnabled()) { 261 preference.setSummary(getString(R.string.print_feature_state_on)); 262 } else { 263 preference.setSummary(getString(R.string.print_feature_state_off)); 264 } 265 266 Drawable drawable = service.getResolveInfo().loadIcon(pm); 267 if (drawable != null) { 268 preference.setIcon(drawable); 269 } 270 271 Bundle extras = preference.getExtras(); 272 extras.putBoolean(EXTRA_CHECKED, service.isEnabled()); 273 extras.putString(EXTRA_TITLE, title); 274 extras.putString(EXTRA_SERVICE_COMPONENT_NAME, componentName.flattenToString()); 275 276 mPrintServicesCategory.addPreference(preference); 277 } 278 279 Preference addNewServicePreference = newAddServicePreferenceOrNull(); 280 if (addNewServicePreference != null) { 281 mPrintServicesCategory.addPreference(addNewServicePreference); 282 } 283 } 284 285 @Override onLoaderReset(Loader<List<PrintServiceInfo>> loader)286 public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) { 287 getPreferenceScreen().removePreference(mPrintServicesCategory); 288 } 289 } 290 newAddServicePreferenceOrNull()291 private Preference newAddServicePreferenceOrNull() { 292 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 293 if (addNewServiceIntent == null) { 294 return null; 295 } 296 Preference preference = new Preference(getPrefContext()); 297 preference.setTitle(R.string.print_menu_item_add_service); 298 preference.setIcon(R.drawable.ic_add_24dp); 299 preference.setOrder(ORDER_LAST); 300 preference.setIntent(addNewServiceIntent); 301 preference.setPersistent(false); 302 return preference; 303 } 304 createAddNewServiceIntentOrNull()305 private Intent createAddNewServiceIntentOrNull() { 306 final String searchUri = Settings.Secure.getString(getContentResolver(), 307 Settings.Secure.PRINT_SERVICE_SEARCH_URI); 308 if (TextUtils.isEmpty(searchUri)) { 309 return null; 310 } 311 return new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); 312 } 313 startSubSettingsIfNeeded()314 private void startSubSettingsIfNeeded() { 315 if (getArguments() == null) { 316 return; 317 } 318 String componentName = getArguments().getString(EXTRA_PRINT_SERVICE_COMPONENT_NAME); 319 if (componentName != null) { 320 getArguments().remove(EXTRA_PRINT_SERVICE_COMPONENT_NAME); 321 Preference prereference = findPreference(componentName); 322 if (prereference != null) { 323 prereference.performClick(); 324 } 325 } 326 } 327 328 @Override onClick(View v)329 public void onClick(View v) { 330 if (mAddNewServiceButton == v) { 331 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 332 if (addNewServiceIntent != null) { // check again just in case. 333 try { 334 startActivity(addNewServiceIntent); 335 } catch (ActivityNotFoundException e) { 336 Log.w(TAG, "Unable to start activity", e); 337 } 338 } 339 } 340 } 341 342 private final class PrintJobsController implements LoaderCallbacks<List<PrintJobInfo>> { 343 344 @Override onCreateLoader(int id, Bundle args)345 public Loader<List<PrintJobInfo>> onCreateLoader(int id, Bundle args) { 346 if (id == LOADER_ID_PRINT_JOBS_LOADER) { 347 return new PrintJobsLoader(getContext()); 348 } 349 return null; 350 } 351 352 @Override onLoadFinished(Loader<List<PrintJobInfo>> loader, List<PrintJobInfo> printJobs)353 public void onLoadFinished(Loader<List<PrintJobInfo>> loader, 354 List<PrintJobInfo> printJobs) { 355 if (printJobs == null || printJobs.isEmpty()) { 356 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 357 } else { 358 if (getPreferenceScreen().findPreference(PRINT_JOBS_CATEGORY) == null) { 359 getPreferenceScreen().addPreference(mActivePrintJobsCategory); 360 } 361 362 mActivePrintJobsCategory.removeAll(); 363 final Context context = getPrefContext(); 364 if (context == null) { 365 Log.w(TAG, "No preference context, skip adding print jobs"); 366 return; 367 } 368 369 for (PrintJobInfo printJob : printJobs) { 370 Preference preference = new Preference(context); 371 372 preference.setPersistent(false); 373 preference.setFragment(PrintJobSettingsFragment.class.getName()); 374 preference.setKey(printJob.getId().flattenToString()); 375 376 switch (printJob.getState()) { 377 case PrintJobInfo.STATE_QUEUED: 378 case PrintJobInfo.STATE_STARTED: 379 if (!printJob.isCancelling()) { 380 preference.setTitle(getString( 381 R.string.print_printing_state_title_template, 382 printJob.getLabel())); 383 } else { 384 preference.setTitle(getString( 385 R.string.print_cancelling_state_title_template, 386 printJob.getLabel())); 387 } 388 break; 389 case PrintJobInfo.STATE_FAILED: 390 preference.setTitle(getString( 391 R.string.print_failed_state_title_template, 392 printJob.getLabel())); 393 break; 394 case PrintJobInfo.STATE_BLOCKED: 395 if (!printJob.isCancelling()) { 396 preference.setTitle(getString( 397 R.string.print_blocked_state_title_template, 398 printJob.getLabel())); 399 } else { 400 preference.setTitle(getString( 401 R.string.print_cancelling_state_title_template, 402 printJob.getLabel())); 403 } 404 break; 405 } 406 407 preference.setSummary(getString(R.string.print_job_summary, 408 printJob.getPrinterName(), DateUtils.formatSameDayTime( 409 printJob.getCreationTime(), printJob.getCreationTime(), 410 DateFormat.SHORT, DateFormat.SHORT))); 411 412 TypedArray a = getActivity().obtainStyledAttributes(new int[]{ 413 android.R.attr.colorControlNormal}); 414 int tintColor = a.getColor(0, 0); 415 a.recycle(); 416 417 switch (printJob.getState()) { 418 case PrintJobInfo.STATE_QUEUED: 419 case PrintJobInfo.STATE_STARTED: { 420 Drawable icon = getActivity().getDrawable( 421 com.android.internal.R.drawable.ic_print); 422 icon.setTint(tintColor); 423 preference.setIcon(icon); 424 break; 425 } 426 427 case PrintJobInfo.STATE_FAILED: 428 case PrintJobInfo.STATE_BLOCKED: { 429 Drawable icon = getActivity().getDrawable( 430 com.android.internal.R.drawable.ic_print_error); 431 icon.setTint(tintColor); 432 preference.setIcon(icon); 433 break; 434 } 435 } 436 437 Bundle extras = preference.getExtras(); 438 extras.putString(EXTRA_PRINT_JOB_ID, printJob.getId().flattenToString()); 439 440 mActivePrintJobsCategory.addPreference(preference); 441 } 442 } 443 } 444 445 @Override onLoaderReset(Loader<List<PrintJobInfo>> loader)446 public void onLoaderReset(Loader<List<PrintJobInfo>> loader) { 447 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 448 } 449 } 450 451 private static final class PrintJobsLoader extends AsyncTaskLoader<List<PrintJobInfo>> { 452 453 private static final String LOG_TAG = "PrintJobsLoader"; 454 455 private static final boolean DEBUG = false; 456 457 private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); 458 459 private final PrintManager mPrintManager; 460 461 private PrintJobStateChangeListener mPrintJobStateChangeListener; 462 PrintJobsLoader(Context context)463 public PrintJobsLoader(Context context) { 464 super(context); 465 mPrintManager = ((PrintManager) context.getSystemService( 466 Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( 467 context.getUserId()); 468 } 469 470 @Override deliverResult(List<PrintJobInfo> printJobs)471 public void deliverResult(List<PrintJobInfo> printJobs) { 472 if (isStarted()) { 473 super.deliverResult(printJobs); 474 } 475 } 476 477 @Override onStartLoading()478 protected void onStartLoading() { 479 if (DEBUG) { 480 Log.i(LOG_TAG, "onStartLoading()"); 481 } 482 // If we already have a result, deliver it immediately. 483 if (!mPrintJobs.isEmpty()) { 484 deliverResult(new ArrayList<PrintJobInfo>(mPrintJobs)); 485 } 486 // Start watching for changes. 487 if (mPrintJobStateChangeListener == null) { 488 mPrintJobStateChangeListener = new PrintJobStateChangeListener() { 489 @Override 490 public void onPrintJobStateChanged(PrintJobId printJobId) { 491 onForceLoad(); 492 } 493 }; 494 mPrintManager.addPrintJobStateChangeListener( 495 mPrintJobStateChangeListener); 496 } 497 // If the data changed or we have no data - load it now. 498 if (mPrintJobs.isEmpty()) { 499 onForceLoad(); 500 } 501 } 502 503 @Override onStopLoading()504 protected void onStopLoading() { 505 if (DEBUG) { 506 Log.i(LOG_TAG, "onStopLoading()"); 507 } 508 // Cancel the load in progress if possible. 509 onCancelLoad(); 510 } 511 512 @Override onReset()513 protected void onReset() { 514 if (DEBUG) { 515 Log.i(LOG_TAG, "onReset()"); 516 } 517 // Stop loading. 518 onStopLoading(); 519 // Clear the cached result. 520 mPrintJobs.clear(); 521 // Stop watching for changes. 522 if (mPrintJobStateChangeListener != null) { 523 mPrintManager.removePrintJobStateChangeListener( 524 mPrintJobStateChangeListener); 525 mPrintJobStateChangeListener = null; 526 } 527 } 528 529 @Override loadInBackground()530 public List<PrintJobInfo> loadInBackground() { 531 List<PrintJobInfo> printJobInfos = null; 532 List<PrintJob> printJobs = mPrintManager.getPrintJobs(); 533 final int printJobCount = printJobs.size(); 534 for (int i = 0; i < printJobCount; i++) { 535 PrintJobInfo printJob = printJobs.get(i).getInfo(); 536 if (shouldShowToUser(printJob)) { 537 if (printJobInfos == null) { 538 printJobInfos = new ArrayList<>(); 539 } 540 printJobInfos.add(printJob); 541 } 542 } 543 return printJobInfos; 544 } 545 } 546 547 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 548 new BaseSearchIndexProvider(R.xml.print_settings); 549 } 550