PrintSettingsFragment.java revision fe47a8dc89fe0393dae16b1c2152c901feab4dcb
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
17package com.android.settings.print;
18
19import android.app.Activity;
20import android.app.ActivityManager;
21import android.app.LoaderManager.LoaderCallbacks;
22import android.content.AsyncTaskLoader;
23import android.content.ComponentName;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.Loader;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.database.ContentObserver;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Message;
35import android.os.UserHandle;
36import android.os.UserManager;
37import android.os.Process;
38import android.preference.Preference;
39import android.preference.PreferenceCategory;
40import android.preference.PreferenceScreen;
41import android.print.PrintJob;
42import android.print.PrintJobId;
43import android.print.PrintJobInfo;
44import android.print.PrintManager;
45import android.print.PrintManager.PrintJobStateChangeListener;
46import android.printservice.PrintServiceInfo;
47import android.provider.SearchIndexableResource;
48import android.provider.Settings;
49import android.text.TextUtils;
50import android.text.format.DateUtils;
51import android.util.Log;
52import android.view.Menu;
53import android.view.MenuInflater;
54import android.view.MenuItem;
55import android.view.View;
56import android.view.ViewGroup;
57import android.widget.AdapterView;
58import android.widget.TextView;
59
60import com.android.internal.content.PackageMonitor;
61import com.android.settings.UserSpinnerAdapter;
62import com.android.settings.UserSpinnerAdapter.UserDetails;
63import com.android.settings.DialogCreatable;
64import com.android.settings.R;
65import com.android.settings.SettingsPreferenceFragment;
66import com.android.settings.search.BaseSearchIndexProvider;
67import com.android.settings.search.Indexable;
68import com.android.settings.search.SearchIndexableRaw;
69
70import java.text.DateFormat;
71import java.util.ArrayList;
72import java.util.List;
73
74import android.widget.AdapterView.OnItemSelectedListener;
75import android.widget.Spinner;
76
77/**
78 * Fragment with the top level print settings.
79 */
80public class PrintSettingsFragment extends SettingsPreferenceFragment
81        implements DialogCreatable, Indexable, OnItemSelectedListener {
82
83    private static final int LOADER_ID_PRINT_JOBS_LOADER = 1;
84
85    private static final String PRINT_JOBS_CATEGORY = "print_jobs_category";
86    private static final String PRINT_SERVICES_CATEGORY = "print_services_category";
87
88    // Extras passed to sub-fragments.
89    static final String EXTRA_PREFERENCE_KEY = "EXTRA_PREFERENCE_KEY";
90    static final String EXTRA_CHECKED = "EXTRA_CHECKED";
91    static final String EXTRA_TITLE = "EXTRA_TITLE";
92    static final String EXTRA_ENABLE_WARNING_TITLE = "EXTRA_ENABLE_WARNING_TITLE";
93    static final String EXTRA_ENABLE_WARNING_MESSAGE = "EXTRA_ENABLE_WARNING_MESSAGE";
94    static final String EXTRA_SETTINGS_TITLE = "EXTRA_SETTINGS_TITLE";
95    static final String EXTRA_SETTINGS_COMPONENT_NAME = "EXTRA_SETTINGS_COMPONENT_NAME";
96    static final String EXTRA_ADD_PRINTERS_TITLE = "EXTRA_ADD_PRINTERS_TITLE";
97    static final String EXTRA_ADD_PRINTERS_COMPONENT_NAME = "EXTRA_ADD_PRINTERS_COMPONENT_NAME";
98    static final String EXTRA_SERVICE_COMPONENT_NAME = "EXTRA_SERVICE_COMPONENT_NAME";
99
100    static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID";
101
102    private static final String EXTRA_PRINT_SERVICE_COMPONENT_NAME =
103            "EXTRA_PRINT_SERVICE_COMPONENT_NAME";
104
105    private final PackageMonitor mSettingsPackageMonitor = new SettingsPackageMonitor();
106
107    private final Handler mHandler = new Handler() {
108        @Override
109        public void dispatchMessage(Message msg) {
110            updateServicesPreferences();
111        }
112    };
113
114    private final SettingsContentObserver mSettingsContentObserver =
115            new SettingsContentObserver(mHandler) {
116        @Override
117        public void onChange(boolean selfChange, Uri uri) {
118            updateServicesPreferences();
119        }
120    };
121
122    private PreferenceCategory mActivePrintJobsCategory;
123    private PreferenceCategory mPrintServicesCategory;
124
125    private PrintJobsController mPrintJobsController;
126    private Context mContext;
127    private UserSpinnerAdapter mProfileSpinnerAdapter;
128
129    @Override
130    public void onAttach(Activity activity) {
131        super.onAttach(activity);
132        mContext = activity;
133    }
134
135    @Override
136    public void onCreate(Bundle icicle) {
137        super.onCreate(icicle);
138        addPreferencesFromResource(R.xml.print_settings);
139
140        mActivePrintJobsCategory = (PreferenceCategory) findPreference(
141                PRINT_JOBS_CATEGORY);
142        mPrintServicesCategory = (PreferenceCategory) findPreference(
143                PRINT_SERVICES_CATEGORY);
144        getPreferenceScreen().removePreference(mActivePrintJobsCategory);
145
146        mPrintJobsController = new PrintJobsController();
147        getActivity().getLoaderManager().initLoader(LOADER_ID_PRINT_JOBS_LOADER,
148                null, mPrintJobsController);
149    }
150
151    @Override
152    public void onResume() {
153        super.onResume();
154        mSettingsPackageMonitor.register(getActivity(), getActivity().getMainLooper(), false);
155        mSettingsContentObserver.register(getContentResolver());
156        updateServicesPreferences();
157        setHasOptionsMenu(true);
158        startSubSettingsIfNeeded();
159    }
160
161    @Override
162    public void onPause() {
163        mSettingsPackageMonitor.unregister();
164        mSettingsContentObserver.unregister(getContentResolver());
165        super.onPause();
166    }
167
168    @Override
169    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
170        super.onCreateOptionsMenu(menu, inflater);
171        String searchUri = Settings.Secure.getString(getContentResolver(),
172                Settings.Secure.PRINT_SERVICE_SEARCH_URI);
173        if (!TextUtils.isEmpty(searchUri)) {
174            MenuItem menuItem = menu.add(R.string.print_menu_item_add_service);
175            menuItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER);
176            menuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)));
177        }
178    }
179
180    @Override
181    public void onViewCreated(View view, Bundle savedInstanceState) {
182        super.onViewCreated(view, savedInstanceState);
183        ViewGroup contentRoot = (ViewGroup) getListView().getParent();
184        View emptyView = getActivity().getLayoutInflater().inflate(
185                R.layout.empty_print_state, contentRoot, false);
186        TextView textView = (TextView) emptyView.findViewById(R.id.message);
187        textView.setText(R.string.print_no_services_installed);
188        contentRoot.addView(emptyView);
189        getListView().setEmptyView(emptyView);
190
191        final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
192        List<UserHandle> userProfiles = um.getUserProfiles();
193        if (userProfiles.size() >= 2) {
194            Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate(
195                    R.layout.spinner_view, null);
196
197            UserHandle myUserHandle = Process.myUserHandle();
198            userProfiles.remove(myUserHandle);
199            userProfiles.add(0, myUserHandle);
200            ArrayList<UserDetails> userDetails = new ArrayList<UserDetails>(userProfiles.size());
201            final int count = userProfiles.size();
202            for (int i = 0; i < count; i++) {
203                userDetails.add(new UserDetails(userProfiles.get(i), um, mContext));
204            }
205
206            mProfileSpinnerAdapter = new UserSpinnerAdapter(mContext, userDetails);
207            spinner.setAdapter(mProfileSpinnerAdapter);
208            spinner.setOnItemSelectedListener(this);
209            setPinnedHeaderView(spinner);
210        }
211    }
212
213    private void updateServicesPreferences() {
214        if (getPreferenceScreen().findPreference(PRINT_SERVICES_CATEGORY) == null) {
215            getPreferenceScreen().addPreference(mPrintServicesCategory);
216        } else {
217            // Since services category is auto generated we have to do a pass
218            // to generate it since services can come and go.
219            mPrintServicesCategory.removeAll();
220        }
221
222        List<ComponentName> enabledServices = PrintSettingsUtils
223                .readEnabledPrintServices(getActivity());
224
225        List<ResolveInfo> installedServices = getActivity().getPackageManager()
226                .queryIntentServices(
227                        new Intent(android.printservice.PrintService.SERVICE_INTERFACE),
228                        PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
229
230        final int installedServiceCount = installedServices.size();
231        for (int i = 0; i < installedServiceCount; i++) {
232            ResolveInfo installedService = installedServices.get(i);
233
234            PreferenceScreen preference = getPreferenceManager().createPreferenceScreen(
235                    getActivity());
236
237            String title = installedService.loadLabel(getPackageManager()).toString();
238            preference.setTitle(title);
239
240            ComponentName componentName = new ComponentName(
241                    installedService.serviceInfo.packageName,
242                    installedService.serviceInfo.name);
243            preference.setKey(componentName.flattenToString());
244
245            preference.setOrder(i);
246            preference.setFragment(PrintServiceSettingsFragment.class.getName());
247            preference.setPersistent(false);
248
249            final boolean serviceEnabled = enabledServices.contains(componentName);
250            if (serviceEnabled) {
251                preference.setSummary(getString(R.string.print_feature_state_on));
252            } else {
253                preference.setSummary(getString(R.string.print_feature_state_off));
254            }
255
256            Bundle extras = preference.getExtras();
257            extras.putString(EXTRA_PREFERENCE_KEY, preference.getKey());
258            extras.putBoolean(EXTRA_CHECKED, serviceEnabled);
259            extras.putString(EXTRA_TITLE, title);
260
261            PrintServiceInfo printServiceInfo = PrintServiceInfo.create(
262                    installedService, getActivity());
263
264            CharSequence applicationLabel = installedService.loadLabel(getPackageManager());
265
266            extras.putString(EXTRA_ENABLE_WARNING_TITLE, getString(
267                    R.string.print_service_security_warning_title, applicationLabel));
268            extras.putString(EXTRA_ENABLE_WARNING_MESSAGE, getString(
269                    R.string.print_service_security_warning_summary, applicationLabel));
270
271            String settingsClassName = printServiceInfo.getSettingsActivityName();
272            if (!TextUtils.isEmpty(settingsClassName)) {
273                extras.putString(EXTRA_SETTINGS_TITLE,
274                        getString(R.string.print_menu_item_settings));
275                extras.putString(EXTRA_SETTINGS_COMPONENT_NAME,
276                        new ComponentName(installedService.serviceInfo.packageName,
277                                settingsClassName).flattenToString());
278            }
279
280            String addPrinterClassName = printServiceInfo.getAddPrintersActivityName();
281            if (!TextUtils.isEmpty(addPrinterClassName)) {
282                extras.putString(EXTRA_ADD_PRINTERS_TITLE,
283                        getString(R.string.print_menu_item_add_printers));
284                extras.putString(EXTRA_ADD_PRINTERS_COMPONENT_NAME,
285                        new ComponentName(installedService.serviceInfo.packageName,
286                                addPrinterClassName).flattenToString());
287            }
288
289            extras.putString(EXTRA_SERVICE_COMPONENT_NAME, componentName.flattenToString());
290
291            mPrintServicesCategory.addPreference(preference);
292        }
293
294        if (mPrintServicesCategory.getPreferenceCount() == 0) {
295            getPreferenceScreen().removePreference(mPrintServicesCategory);
296        }
297    }
298
299    private void startSubSettingsIfNeeded() {
300        if (getArguments() == null) {
301            return;
302        }
303        String componentName = getArguments().getString(EXTRA_PRINT_SERVICE_COMPONENT_NAME);
304        if (componentName != null) {
305            getArguments().remove(EXTRA_PRINT_SERVICE_COMPONENT_NAME);
306            Preference prereference = findPreference(componentName);
307            if (prereference != null) {
308                prereference.performClick(getPreferenceScreen());
309            }
310        }
311    }
312
313    @Override
314    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
315        UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position);
316        if (selectedUser.getIdentifier() != UserHandle.myUserId()) {
317            Intent intent = new Intent(Settings.ACTION_PRINT_SETTINGS);
318            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
319            mContext.startActivityAsUser(intent, selectedUser);
320            getActivity().finish();
321        }
322    }
323
324    @Override
325    public void onNothingSelected(AdapterView<?> parent) {
326        // Nothing to do
327    }
328
329    private class SettingsPackageMonitor extends PackageMonitor {
330        @Override
331        public void onPackageAdded(String packageName, int uid) {
332            mHandler.obtainMessage().sendToTarget();
333        }
334
335        @Override
336        public void onPackageAppeared(String packageName, int reason) {
337            mHandler.obtainMessage().sendToTarget();
338        }
339
340        @Override
341        public void onPackageDisappeared(String packageName, int reason) {
342            mHandler.obtainMessage().sendToTarget();
343        }
344
345        @Override
346        public void onPackageRemoved(String packageName, int uid) {
347            mHandler.obtainMessage().sendToTarget();
348        }
349    }
350
351    private static abstract class SettingsContentObserver extends ContentObserver {
352
353        public SettingsContentObserver(Handler handler) {
354            super(handler);
355        }
356
357        public void register(ContentResolver contentResolver) {
358            contentResolver.registerContentObserver(Settings.Secure.getUriFor(
359                    Settings.Secure.ENABLED_PRINT_SERVICES), false, this);
360        }
361
362        public void unregister(ContentResolver contentResolver) {
363            contentResolver.unregisterContentObserver(this);
364        }
365
366        @Override
367        public abstract void onChange(boolean selfChange, Uri uri);
368    }
369
370    private final class PrintJobsController implements LoaderCallbacks<List<PrintJobInfo>> {
371
372        @Override
373        public Loader<List<PrintJobInfo>> onCreateLoader(int id, Bundle args) {
374            if (id == LOADER_ID_PRINT_JOBS_LOADER) {
375                return new PrintJobsLoader(getActivity());
376            }
377            return null;
378        }
379
380        @Override
381        public void onLoadFinished(Loader<List<PrintJobInfo>> loader,
382                List<PrintJobInfo> printJobs) {
383            if (printJobs == null || printJobs.isEmpty()) {
384                getPreferenceScreen().removePreference(mActivePrintJobsCategory);
385            } else {
386                if (getPreferenceScreen().findPreference(PRINT_JOBS_CATEGORY) == null) {
387                    getPreferenceScreen().addPreference(mActivePrintJobsCategory);
388                }
389
390                mActivePrintJobsCategory.removeAll();
391
392                final int printJobCount = printJobs.size();
393                for (int i = 0; i < printJobCount; i++) {
394                    PrintJobInfo printJob = printJobs.get(i);
395
396                    PreferenceScreen preference = getPreferenceManager()
397                            .createPreferenceScreen(getActivity());
398
399                    preference.setPersistent(false);
400                    preference.setFragment(PrintJobSettingsFragment.class.getName());
401                    preference.setKey(printJob.getId().flattenToString());
402
403                    switch (printJob.getState()) {
404                        case PrintJobInfo.STATE_QUEUED:
405                        case PrintJobInfo.STATE_STARTED: {
406                            if (!printJob.isCancelling()) {
407                                preference.setTitle(getString(
408                                        R.string.print_printing_state_title_template,
409                                        printJob.getLabel()));
410                            } else {
411                                preference.setTitle(getString(
412                                        R.string.print_cancelling_state_title_template,
413                                        printJob.getLabel()));
414                            }
415                        } break;
416
417                        case PrintJobInfo.STATE_FAILED: {
418                            preference.setTitle(getString(
419                                    R.string.print_failed_state_title_template,
420                                    printJob.getLabel()));
421                        } break;
422
423                        case PrintJobInfo.STATE_BLOCKED: {
424                            if (!printJob.isCancelling()) {
425                                preference.setTitle(getString(
426                                        R.string.print_blocked_state_title_template,
427                                        printJob.getLabel()));
428                            } else {
429                                preference.setTitle(getString(
430                                        R.string.print_cancelling_state_title_template,
431                                        printJob.getLabel()));
432                            }
433                        } break;
434                    }
435
436                    preference.setSummary(getString(R.string.print_job_summary,
437                            printJob.getPrinterName(), DateUtils.formatSameDayTime(
438                                    printJob.getCreationTime(), printJob.getCreationTime(),
439                                    DateFormat.SHORT, DateFormat.SHORT)));
440
441                    switch (printJob.getState()) {
442                        case PrintJobInfo.STATE_QUEUED:
443                        case PrintJobInfo.STATE_STARTED: {
444                            preference.setIcon(com.android.internal.R.drawable.ic_print);
445                        } break;
446
447                        case PrintJobInfo.STATE_FAILED:
448                        case PrintJobInfo.STATE_BLOCKED: {
449                            preference.setIcon(com.android.internal.R.drawable.ic_print_error);
450                        } break;
451                    }
452
453                    Bundle extras = preference.getExtras();
454                    extras.putString(EXTRA_PRINT_JOB_ID, printJob.getId().flattenToString());
455
456                    mActivePrintJobsCategory.addPreference(preference);
457                }
458            }
459        }
460
461        @Override
462        public void onLoaderReset(Loader<List<PrintJobInfo>> loader) {
463            getPreferenceScreen().removePreference(mActivePrintJobsCategory);
464        }
465    }
466
467    private static final class PrintJobsLoader extends AsyncTaskLoader<List<PrintJobInfo>> {
468
469        private static final String LOG_TAG = "PrintJobsLoader";
470
471        private static final boolean DEBUG = false;
472
473        private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>();
474
475        private final PrintManager mPrintManager;
476
477        private PrintJobStateChangeListener mPrintJobStateChangeListener;
478
479        public PrintJobsLoader(Context context) {
480            super(context);
481            mPrintManager = ((PrintManager) context.getSystemService(
482                    Context.PRINT_SERVICE)).getGlobalPrintManagerForUser(
483                        ActivityManager.getCurrentUser());
484        }
485
486        @Override
487        public void deliverResult(List<PrintJobInfo> printJobs) {
488            if (isStarted()) {
489                super.deliverResult(printJobs);
490            }
491        }
492
493        @Override
494        protected void onStartLoading() {
495            if (DEBUG) {
496                Log.i(LOG_TAG, "onStartLoading()");
497            }
498            // If we already have a result, deliver it immediately.
499            if (!mPrintJobs.isEmpty()) {
500                deliverResult(new ArrayList<PrintJobInfo>(mPrintJobs));
501            }
502            // Start watching for changes.
503            if (mPrintJobStateChangeListener == null) {
504                mPrintJobStateChangeListener = new PrintJobStateChangeListener() {
505                    @Override
506                    public void onPrintJobStateChanged(PrintJobId printJobId) {
507                        onForceLoad();
508                    }
509                };
510                mPrintManager.addPrintJobStateChangeListener(
511                        mPrintJobStateChangeListener);
512            }
513            // If the data changed or we have no data - load it now.
514            if (mPrintJobs.isEmpty()) {
515                onForceLoad();
516            }
517        }
518
519        @Override
520        protected void onStopLoading() {
521            if (DEBUG) {
522                Log.i(LOG_TAG, "onStopLoading()");
523            }
524            // Cancel the load in progress if possible.
525            onCancelLoad();
526        }
527
528        @Override
529        protected void onReset() {
530            if (DEBUG) {
531                Log.i(LOG_TAG, "onReset()");
532            }
533            // Stop loading.
534            onStopLoading();
535            // Clear the cached result.
536            mPrintJobs.clear();
537            // Stop watching for changes.
538            if (mPrintJobStateChangeListener != null) {
539                mPrintManager.removePrintJobStateChangeListener(
540                        mPrintJobStateChangeListener);
541                mPrintJobStateChangeListener = null;
542            }
543        }
544
545        @Override
546        public List<PrintJobInfo> loadInBackground() {
547            List<PrintJobInfo> printJobInfos = null;
548            List<PrintJob> printJobs = mPrintManager.getPrintJobs();
549            final int printJobCount = printJobs.size();
550            for (int i = 0; i < printJobCount; i++) {
551                PrintJobInfo printJob = printJobs.get(i).getInfo();
552                if (shouldShowToUser(printJob)) {
553                    if (printJobInfos == null) {
554                        printJobInfos = new ArrayList<PrintJobInfo>();
555                    }
556                    printJobInfos.add(printJob);
557                }
558            }
559            return printJobInfos;
560        }
561
562        private static boolean shouldShowToUser(PrintJobInfo printJob) {
563            switch (printJob.getState()) {
564                case PrintJobInfo.STATE_QUEUED:
565                case PrintJobInfo.STATE_STARTED:
566                case PrintJobInfo.STATE_BLOCKED:
567                case PrintJobInfo.STATE_FAILED: {
568                    return true;
569                }
570            }
571            return false;
572        }
573    }
574
575    public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
576            new BaseSearchIndexProvider() {
577        @Override
578        public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) {
579            List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>();
580
581            PackageManager packageManager = context.getPackageManager();
582            PrintManager printManager = (PrintManager) context.getSystemService(
583                    Context.PRINT_SERVICE);
584
585            String screenTitle = context.getResources().getString(R.string.print_settings);
586            SearchIndexableRaw data = new SearchIndexableRaw(context);
587            data.title = screenTitle;
588            data.screenTitle = screenTitle;
589            indexables.add(data);
590
591            // Indexing all services, regardless if enabled.
592            List<PrintServiceInfo> services = printManager.getInstalledPrintServices();
593            final int serviceCount = services.size();
594            for (int i = 0; i < serviceCount; i++) {
595                PrintServiceInfo service = services.get(i);
596
597                ComponentName componentName = new ComponentName(
598                        service.getResolveInfo().serviceInfo.packageName,
599                        service.getResolveInfo().serviceInfo.name);
600
601                data = new SearchIndexableRaw(context);
602                data.key = componentName.flattenToString();
603                data.title = service.getResolveInfo().loadLabel(packageManager).toString();
604                data.summaryOn = context.getString(R.string.print_feature_state_on);
605                data.summaryOff = context.getString(R.string.print_feature_state_off);
606                data.screenTitle = screenTitle;
607                indexables.add(data);
608            }
609
610            return indexables;
611        }
612
613        @Override
614        public List<SearchIndexableResource> getXmlResourcesToIndex(Context context,
615                boolean enabled) {
616            List<SearchIndexableResource> indexables = new ArrayList<SearchIndexableResource>();
617            SearchIndexableResource indexable = new SearchIndexableResource(context);
618            indexable.xmlResId = R.xml.print_settings;
619            indexables.add(indexable);
620            return indexables;
621        }
622    };
623}
624