1 /* 2 * Copyright (C) 2019 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.car.developeroptions.print; 18 19 import static com.android.car.developeroptions.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.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.printservice.PrintServiceInfo; 37 import android.provider.SearchIndexableResource; 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.loader.app.LoaderManager.LoaderCallbacks; 50 import androidx.loader.content.AsyncTaskLoader; 51 import androidx.loader.content.Loader; 52 import androidx.preference.Preference; 53 import androidx.preference.PreferenceCategory; 54 55 import com.android.car.developeroptions.R; 56 import com.android.car.developeroptions.search.BaseSearchIndexProvider; 57 import com.android.car.developeroptions.search.Indexable; 58 import com.android.settingslib.search.SearchIndexable; 59 import com.android.settingslib.widget.apppreference.AppPreference; 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 @SearchIndexable 69 public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment 70 implements 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 public int getMetricsCategory() { 99 return SettingsEnums.PRINT_SETTINGS; 100 } 101 102 @Override getHelpResource()103 public 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 onViewCreated(View view, Bundle savedInstanceState)136 public void onViewCreated(View view, Bundle savedInstanceState) { 137 super.onViewCreated(view, savedInstanceState); 138 ViewGroup contentRoot = (ViewGroup) getListView().getParent(); 139 View emptyView = getActivity().getLayoutInflater().inflate( 140 R.layout.empty_print_state, contentRoot, false); 141 TextView textView = (TextView) emptyView.findViewById(R.id.message); 142 textView.setText(R.string.print_no_services_installed); 143 144 final Intent addNewServiceIntent = createAddNewServiceIntentOrNull(); 145 if (addNewServiceIntent != null) { 146 mAddNewServiceButton = (Button) emptyView.findViewById(R.id.add_new_service); 147 mAddNewServiceButton.setOnClickListener(this); 148 // The empty is used elsewhere too so it's hidden by default. 149 mAddNewServiceButton.setVisibility(View.VISIBLE); 150 } 151 152 contentRoot.addView(emptyView); 153 setEmptyView(emptyView); 154 } 155 156 @Override getIntentActionString()157 protected String getIntentActionString() { 158 return Settings.ACTION_PRINT_SETTINGS; 159 } 160 161 /** 162 * Adds preferences for all print services to the {@value PRINT_SERVICES_CATEGORY} cathegory. 163 */ 164 private final class PrintServicesController implements LoaderCallbacks<List<PrintServiceInfo>> { 165 @Override onCreateLoader(int id, Bundle args)166 public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) { 167 PrintManager printManager = 168 (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE); 169 if (printManager != null) { 170 return new SettingsPrintServicesLoader(printManager, getContext(), 171 PrintManager.ALL_SERVICES); 172 } else { 173 return null; 174 } 175 } 176 177 @Override onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)178 public void onLoadFinished(Loader<List<PrintServiceInfo>> loader, 179 List<PrintServiceInfo> services) { 180 if (services.isEmpty()) { 181 getPreferenceScreen().removePreference(mPrintServicesCategory); 182 return; 183 } else if (getPreferenceScreen().findPreference(PRINT_SERVICES_CATEGORY) == null) { 184 getPreferenceScreen().addPreference(mPrintServicesCategory); 185 } 186 187 mPrintServicesCategory.removeAll(); 188 PackageManager pm = getActivity().getPackageManager(); 189 final Context context = getPrefContext(); 190 if (context == null) { 191 Log.w(TAG, "No preference context, skip adding print services"); 192 return; 193 } 194 195 for (PrintServiceInfo service : services) { 196 AppPreference preference = new AppPreference(context); 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 onLoaderReset(Loader<List<PrintServiceInfo>> loader)233 public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) { 234 getPreferenceScreen().removePreference(mPrintServicesCategory); 235 } 236 } 237 newAddServicePreferenceOrNull()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 createAddNewServiceIntentOrNull()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 startSubSettingsIfNeeded()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 onClick(View v)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 onCreateLoader(int id, Bundle args)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 onLoadFinished(Loader<List<PrintJobInfo>> loader, List<PrintJobInfo> printJobs)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 final Context context = getPrefContext(); 311 if (context == null) { 312 Log.w(TAG, "No preference context, skip adding print jobs"); 313 return; 314 } 315 316 for (PrintJobInfo printJob : printJobs) { 317 Preference preference = new Preference(context); 318 319 preference.setPersistent(false); 320 preference.setFragment(PrintJobSettingsFragment.class.getName()); 321 preference.setKey(printJob.getId().flattenToString()); 322 323 switch (printJob.getState()) { 324 case PrintJobInfo.STATE_QUEUED: 325 case PrintJobInfo.STATE_STARTED: 326 if (!printJob.isCancelling()) { 327 preference.setTitle(getString( 328 R.string.print_printing_state_title_template, 329 printJob.getLabel())); 330 } else { 331 preference.setTitle(getString( 332 R.string.print_cancelling_state_title_template, 333 printJob.getLabel())); 334 } 335 break; 336 case PrintJobInfo.STATE_FAILED: 337 preference.setTitle(getString( 338 R.string.print_failed_state_title_template, 339 printJob.getLabel())); 340 break; 341 case PrintJobInfo.STATE_BLOCKED: 342 if (!printJob.isCancelling()) { 343 preference.setTitle(getString( 344 R.string.print_blocked_state_title_template, 345 printJob.getLabel())); 346 } else { 347 preference.setTitle(getString( 348 R.string.print_cancelling_state_title_template, 349 printJob.getLabel())); 350 } 351 break; 352 } 353 354 preference.setSummary(getString(R.string.print_job_summary, 355 printJob.getPrinterName(), DateUtils.formatSameDayTime( 356 printJob.getCreationTime(), printJob.getCreationTime(), 357 DateFormat.SHORT, DateFormat.SHORT))); 358 359 TypedArray a = getActivity().obtainStyledAttributes(new int[]{ 360 android.R.attr.colorControlNormal}); 361 int tintColor = a.getColor(0, 0); 362 a.recycle(); 363 364 switch (printJob.getState()) { 365 case PrintJobInfo.STATE_QUEUED: 366 case PrintJobInfo.STATE_STARTED: { 367 Drawable icon = getActivity().getDrawable( 368 com.android.internal.R.drawable.ic_print); 369 icon.setTint(tintColor); 370 preference.setIcon(icon); 371 break; 372 } 373 374 case PrintJobInfo.STATE_FAILED: 375 case PrintJobInfo.STATE_BLOCKED: { 376 Drawable icon = getActivity().getDrawable( 377 com.android.internal.R.drawable.ic_print_error); 378 icon.setTint(tintColor); 379 preference.setIcon(icon); 380 break; 381 } 382 } 383 384 Bundle extras = preference.getExtras(); 385 extras.putString(EXTRA_PRINT_JOB_ID, printJob.getId().flattenToString()); 386 387 mActivePrintJobsCategory.addPreference(preference); 388 } 389 } 390 } 391 392 @Override onLoaderReset(Loader<List<PrintJobInfo>> loader)393 public void onLoaderReset(Loader<List<PrintJobInfo>> loader) { 394 getPreferenceScreen().removePreference(mActivePrintJobsCategory); 395 } 396 } 397 398 private static final class PrintJobsLoader extends AsyncTaskLoader<List<PrintJobInfo>> { 399 400 private static final String LOG_TAG = "PrintJobsLoader"; 401 402 private static final boolean DEBUG = false; 403 404 private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); 405 406 private final PrintManager mPrintManager; 407 408 private PrintJobStateChangeListener mPrintJobStateChangeListener; 409 PrintJobsLoader(Context context)410 public PrintJobsLoader(Context context) { 411 super(context); 412 mPrintManager = ((PrintManager) context.getSystemService( 413 Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( 414 context.getUserId()); 415 } 416 417 @Override deliverResult(List<PrintJobInfo> printJobs)418 public void deliverResult(List<PrintJobInfo> printJobs) { 419 if (isStarted()) { 420 super.deliverResult(printJobs); 421 } 422 } 423 424 @Override onStartLoading()425 protected void onStartLoading() { 426 if (DEBUG) { 427 Log.i(LOG_TAG, "onStartLoading()"); 428 } 429 // If we already have a result, deliver it immediately. 430 if (!mPrintJobs.isEmpty()) { 431 deliverResult(new ArrayList<PrintJobInfo>(mPrintJobs)); 432 } 433 // Start watching for changes. 434 if (mPrintJobStateChangeListener == null) { 435 mPrintJobStateChangeListener = new PrintJobStateChangeListener() { 436 @Override 437 public void onPrintJobStateChanged(PrintJobId printJobId) { 438 onForceLoad(); 439 } 440 }; 441 mPrintManager.addPrintJobStateChangeListener( 442 mPrintJobStateChangeListener); 443 } 444 // If the data changed or we have no data - load it now. 445 if (mPrintJobs.isEmpty()) { 446 onForceLoad(); 447 } 448 } 449 450 @Override onStopLoading()451 protected void onStopLoading() { 452 if (DEBUG) { 453 Log.i(LOG_TAG, "onStopLoading()"); 454 } 455 // Cancel the load in progress if possible. 456 onCancelLoad(); 457 } 458 459 @Override onReset()460 protected void onReset() { 461 if (DEBUG) { 462 Log.i(LOG_TAG, "onReset()"); 463 } 464 // Stop loading. 465 onStopLoading(); 466 // Clear the cached result. 467 mPrintJobs.clear(); 468 // Stop watching for changes. 469 if (mPrintJobStateChangeListener != null) { 470 mPrintManager.removePrintJobStateChangeListener( 471 mPrintJobStateChangeListener); 472 mPrintJobStateChangeListener = null; 473 } 474 } 475 476 @Override loadInBackground()477 public List<PrintJobInfo> loadInBackground() { 478 List<PrintJobInfo> printJobInfos = null; 479 List<PrintJob> printJobs = mPrintManager.getPrintJobs(); 480 final int printJobCount = printJobs.size(); 481 for (int i = 0; i < printJobCount; i++) { 482 PrintJobInfo printJob = printJobs.get(i).getInfo(); 483 if (shouldShowToUser(printJob)) { 484 if (printJobInfos == null) { 485 printJobInfos = new ArrayList<>(); 486 } 487 printJobInfos.add(printJob); 488 } 489 } 490 return printJobInfos; 491 } 492 } 493 494 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 495 new BaseSearchIndexProvider() { 496 497 @Override 498 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, 499 boolean enabled) { 500 List<SearchIndexableResource> indexables = new ArrayList<>(); 501 SearchIndexableResource indexable = new SearchIndexableResource(context); 502 indexable.xmlResId = R.xml.print_settings; 503 indexables.add(indexable); 504 return indexables; 505 } 506 }; 507 } 508