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