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