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