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
getMetricsCategory()97     protected int getMetricsCategory() {
98         return MetricsEvent.PRINT_SETTINGS;
99     }
100 
101     @Override
getHelpResource()102     protected int getHelpResource() {
103         return R.string.help_uri_printing;
104     }
105 
106     @Override
onCreate(Bundle icicle)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
onStart()125     public void onStart() {
126         super.onStart();
127         setHasOptionsMenu(true);
128         startSubSettingsIfNeeded();
129     }
130 
131     @Override
onStop()132     public void onStop() {
133         super.onStop();
134     }
135 
136     @Override
onViewCreated(View view, Bundle savedInstanceState)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
getIntentActionString()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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)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
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 
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
onLoaderReset(Loader<List<PrintJobInfo>> loader)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 
PrintJobsLoader(Context context)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
deliverResult(List<PrintJobInfo> printJobs)406         public void deliverResult(List<PrintJobInfo> printJobs) {
407             if (isStarted()) {
408                 super.deliverResult(printJobs);
409             }
410         }
411 
412         @Override
onStartLoading()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
onStopLoading()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
onReset()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
loadInBackground()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      */
shouldShowToUser(PrintJobInfo printJob)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          */
PrintSummaryProvider(Context context, SummaryLoader summaryLoader)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
setListening(boolean isListening)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
onPrintJobStateChanged(PrintJobId printJobId)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