1 /*
2  * Copyright (C) 2019 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.car.developeroptions.print;
18 
19 import static com.android.car.developeroptions.print.PrintSettingPreferenceController.shouldShowToUser;
20 
21 import android.app.settings.SettingsEnums;
22 import android.content.ActivityNotFoundException;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.content.res.TypedArray;
28 import android.graphics.drawable.Drawable;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.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.printservice.PrintServiceInfo;
37 import android.provider.SearchIndexableResource;
38 import android.provider.Settings;
39 import android.text.TextUtils;
40 import android.text.format.DateUtils;
41 import android.util.Log;
42 import android.view.LayoutInflater;
43 import android.view.View;
44 import android.view.View.OnClickListener;
45 import android.view.ViewGroup;
46 import android.widget.Button;
47 import android.widget.TextView;
48 
49 import androidx.loader.app.LoaderManager.LoaderCallbacks;
50 import androidx.loader.content.AsyncTaskLoader;
51 import androidx.loader.content.Loader;
52 import androidx.preference.Preference;
53 import androidx.preference.PreferenceCategory;
54 
55 import com.android.car.developeroptions.R;
56 import com.android.car.developeroptions.search.BaseSearchIndexProvider;
57 import com.android.car.developeroptions.search.Indexable;
58 import com.android.settingslib.search.SearchIndexable;
59 import com.android.settingslib.widget.apppreference.AppPreference;
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 @SearchIndexable
69 public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment
70         implements Indexable, OnClickListener {
71     public static final String TAG = "PrintSettingsFragment";
72     private static final int LOADER_ID_PRINT_JOBS_LOADER = 1;
73     private static final int LOADER_ID_PRINT_SERVICES = 2;
74 
75     private static final String PRINT_JOBS_CATEGORY = "print_jobs_category";
76     private static final String PRINT_SERVICES_CATEGORY = "print_services_category";
77 
78     static final String EXTRA_CHECKED = "EXTRA_CHECKED";
79     static final String EXTRA_TITLE = "EXTRA_TITLE";
80     static final String EXTRA_SERVICE_COMPONENT_NAME = "EXTRA_SERVICE_COMPONENT_NAME";
81 
82     static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID";
83 
84     private static final String EXTRA_PRINT_SERVICE_COMPONENT_NAME =
85             "EXTRA_PRINT_SERVICE_COMPONENT_NAME";
86 
87     private static final int ORDER_LAST = Preference.DEFAULT_ORDER - 1;
88 
89     private PreferenceCategory mActivePrintJobsCategory;
90     private PreferenceCategory mPrintServicesCategory;
91 
92     private PrintJobsController mPrintJobsController;
93     private PrintServicesController mPrintServicesController;
94 
95     private Button mAddNewServiceButton;
96 
97     @Override
getMetricsCategory()98     public int getMetricsCategory() {
99         return SettingsEnums.PRINT_SETTINGS;
100     }
101 
102     @Override
getHelpResource()103     public int getHelpResource() {
104         return R.string.help_uri_printing;
105     }
106 
107     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)108     public View onCreateView(LayoutInflater inflater, ViewGroup container,
109             Bundle savedInstanceState) {
110         View root = super.onCreateView(inflater, container, savedInstanceState);
111         addPreferencesFromResource(R.xml.print_settings);
112 
113         mActivePrintJobsCategory = (PreferenceCategory) findPreference(
114                 PRINT_JOBS_CATEGORY);
115         mPrintServicesCategory = (PreferenceCategory) findPreference(
116                 PRINT_SERVICES_CATEGORY);
117         getPreferenceScreen().removePreference(mActivePrintJobsCategory);
118 
119         mPrintJobsController = new PrintJobsController();
120         getLoaderManager().initLoader(LOADER_ID_PRINT_JOBS_LOADER, null, mPrintJobsController);
121 
122         mPrintServicesController = new PrintServicesController();
123         getLoaderManager().initLoader(LOADER_ID_PRINT_SERVICES, null, mPrintServicesController);
124 
125         return root;
126     }
127 
128     @Override
onStart()129     public void onStart() {
130         super.onStart();
131         setHasOptionsMenu(true);
132         startSubSettingsIfNeeded();
133     }
134 
135     @Override
onViewCreated(View view, Bundle savedInstanceState)136     public void onViewCreated(View view, Bundle savedInstanceState) {
137         super.onViewCreated(view, savedInstanceState);
138         ViewGroup contentRoot = (ViewGroup) getListView().getParent();
139         View emptyView = getActivity().getLayoutInflater().inflate(
140                 R.layout.empty_print_state, contentRoot, false);
141         TextView textView = (TextView) emptyView.findViewById(R.id.message);
142         textView.setText(R.string.print_no_services_installed);
143 
144         final Intent addNewServiceIntent = createAddNewServiceIntentOrNull();
145         if (addNewServiceIntent != null) {
146             mAddNewServiceButton = (Button) emptyView.findViewById(R.id.add_new_service);
147             mAddNewServiceButton.setOnClickListener(this);
148             // The empty is used elsewhere too so it's hidden by default.
149             mAddNewServiceButton.setVisibility(View.VISIBLE);
150         }
151 
152         contentRoot.addView(emptyView);
153         setEmptyView(emptyView);
154     }
155 
156     @Override
getIntentActionString()157     protected String getIntentActionString() {
158         return Settings.ACTION_PRINT_SETTINGS;
159     }
160 
161     /**
162      * Adds preferences for all print services to the {@value PRINT_SERVICES_CATEGORY} cathegory.
163      */
164     private final class PrintServicesController implements LoaderCallbacks<List<PrintServiceInfo>> {
165         @Override
onCreateLoader(int id, Bundle args)166         public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
167             PrintManager printManager =
168                     (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE);
169             if (printManager != null) {
170                 return new SettingsPrintServicesLoader(printManager, getContext(),
171                         PrintManager.ALL_SERVICES);
172             } else {
173                 return null;
174             }
175         }
176 
177         @Override
onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)178         public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
179                 List<PrintServiceInfo> services) {
180             if (services.isEmpty()) {
181                 getPreferenceScreen().removePreference(mPrintServicesCategory);
182                 return;
183             } else if (getPreferenceScreen().findPreference(PRINT_SERVICES_CATEGORY) == null) {
184                 getPreferenceScreen().addPreference(mPrintServicesCategory);
185             }
186 
187             mPrintServicesCategory.removeAll();
188             PackageManager pm = getActivity().getPackageManager();
189             final Context context = getPrefContext();
190             if (context == null) {
191                 Log.w(TAG, "No preference context, skip adding print services");
192                 return;
193             }
194 
195             for (PrintServiceInfo service : services) {
196                 AppPreference preference = new AppPreference(context);
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                 final Context context = getPrefContext();
311                 if (context == null) {
312                     Log.w(TAG, "No preference context, skip adding print jobs");
313                     return;
314                 }
315 
316                 for (PrintJobInfo printJob : printJobs) {
317                     Preference preference = new Preference(context);
318 
319                     preference.setPersistent(false);
320                     preference.setFragment(PrintJobSettingsFragment.class.getName());
321                     preference.setKey(printJob.getId().flattenToString());
322 
323                     switch (printJob.getState()) {
324                         case PrintJobInfo.STATE_QUEUED:
325                         case PrintJobInfo.STATE_STARTED:
326                             if (!printJob.isCancelling()) {
327                                 preference.setTitle(getString(
328                                         R.string.print_printing_state_title_template,
329                                         printJob.getLabel()));
330                             } else {
331                                 preference.setTitle(getString(
332                                         R.string.print_cancelling_state_title_template,
333                                         printJob.getLabel()));
334                             }
335                             break;
336                         case PrintJobInfo.STATE_FAILED:
337                             preference.setTitle(getString(
338                                     R.string.print_failed_state_title_template,
339                                     printJob.getLabel()));
340                             break;
341                         case PrintJobInfo.STATE_BLOCKED:
342                             if (!printJob.isCancelling()) {
343                                 preference.setTitle(getString(
344                                         R.string.print_blocked_state_title_template,
345                                         printJob.getLabel()));
346                             } else {
347                                 preference.setTitle(getString(
348                                         R.string.print_cancelling_state_title_template,
349                                         printJob.getLabel()));
350                             }
351                             break;
352                     }
353 
354                     preference.setSummary(getString(R.string.print_job_summary,
355                             printJob.getPrinterName(), DateUtils.formatSameDayTime(
356                                     printJob.getCreationTime(), printJob.getCreationTime(),
357                                     DateFormat.SHORT, DateFormat.SHORT)));
358 
359                     TypedArray a = getActivity().obtainStyledAttributes(new int[]{
360                             android.R.attr.colorControlNormal});
361                     int tintColor = a.getColor(0, 0);
362                     a.recycle();
363 
364                     switch (printJob.getState()) {
365                         case PrintJobInfo.STATE_QUEUED:
366                         case PrintJobInfo.STATE_STARTED: {
367                             Drawable icon = getActivity().getDrawable(
368                                     com.android.internal.R.drawable.ic_print);
369                             icon.setTint(tintColor);
370                             preference.setIcon(icon);
371                             break;
372                         }
373 
374                         case PrintJobInfo.STATE_FAILED:
375                         case PrintJobInfo.STATE_BLOCKED: {
376                             Drawable icon = getActivity().getDrawable(
377                                     com.android.internal.R.drawable.ic_print_error);
378                             icon.setTint(tintColor);
379                             preference.setIcon(icon);
380                             break;
381                         }
382                     }
383 
384                     Bundle extras = preference.getExtras();
385                     extras.putString(EXTRA_PRINT_JOB_ID, printJob.getId().flattenToString());
386 
387                     mActivePrintJobsCategory.addPreference(preference);
388                 }
389             }
390         }
391 
392         @Override
onLoaderReset(Loader<List<PrintJobInfo>> loader)393         public void onLoaderReset(Loader<List<PrintJobInfo>> loader) {
394             getPreferenceScreen().removePreference(mActivePrintJobsCategory);
395         }
396     }
397 
398     private static final class PrintJobsLoader extends AsyncTaskLoader<List<PrintJobInfo>> {
399 
400         private static final String LOG_TAG = "PrintJobsLoader";
401 
402         private static final boolean DEBUG = false;
403 
404         private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>();
405 
406         private final PrintManager mPrintManager;
407 
408         private PrintJobStateChangeListener mPrintJobStateChangeListener;
409 
PrintJobsLoader(Context context)410         public PrintJobsLoader(Context context) {
411             super(context);
412             mPrintManager = ((PrintManager) context.getSystemService(
413                     Context.PRINT_SERVICE)).getGlobalPrintManagerForUser(
414                     context.getUserId());
415         }
416 
417         @Override
deliverResult(List<PrintJobInfo> printJobs)418         public void deliverResult(List<PrintJobInfo> printJobs) {
419             if (isStarted()) {
420                 super.deliverResult(printJobs);
421             }
422         }
423 
424         @Override
onStartLoading()425         protected void onStartLoading() {
426             if (DEBUG) {
427                 Log.i(LOG_TAG, "onStartLoading()");
428             }
429             // If we already have a result, deliver it immediately.
430             if (!mPrintJobs.isEmpty()) {
431                 deliverResult(new ArrayList<PrintJobInfo>(mPrintJobs));
432             }
433             // Start watching for changes.
434             if (mPrintJobStateChangeListener == null) {
435                 mPrintJobStateChangeListener = new PrintJobStateChangeListener() {
436                     @Override
437                     public void onPrintJobStateChanged(PrintJobId printJobId) {
438                         onForceLoad();
439                     }
440                 };
441                 mPrintManager.addPrintJobStateChangeListener(
442                         mPrintJobStateChangeListener);
443             }
444             // If the data changed or we have no data - load it now.
445             if (mPrintJobs.isEmpty()) {
446                 onForceLoad();
447             }
448         }
449 
450         @Override
onStopLoading()451         protected void onStopLoading() {
452             if (DEBUG) {
453                 Log.i(LOG_TAG, "onStopLoading()");
454             }
455             // Cancel the load in progress if possible.
456             onCancelLoad();
457         }
458 
459         @Override
onReset()460         protected void onReset() {
461             if (DEBUG) {
462                 Log.i(LOG_TAG, "onReset()");
463             }
464             // Stop loading.
465             onStopLoading();
466             // Clear the cached result.
467             mPrintJobs.clear();
468             // Stop watching for changes.
469             if (mPrintJobStateChangeListener != null) {
470                 mPrintManager.removePrintJobStateChangeListener(
471                         mPrintJobStateChangeListener);
472                 mPrintJobStateChangeListener = null;
473             }
474         }
475 
476         @Override
loadInBackground()477         public List<PrintJobInfo> loadInBackground() {
478             List<PrintJobInfo> printJobInfos = null;
479             List<PrintJob> printJobs = mPrintManager.getPrintJobs();
480             final int printJobCount = printJobs.size();
481             for (int i = 0; i < printJobCount; i++) {
482                 PrintJobInfo printJob = printJobs.get(i).getInfo();
483                 if (shouldShowToUser(printJob)) {
484                     if (printJobInfos == null) {
485                         printJobInfos = new ArrayList<>();
486                     }
487                     printJobInfos.add(printJob);
488                 }
489             }
490             return printJobInfos;
491         }
492     }
493 
494     public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
495             new BaseSearchIndexProvider() {
496 
497                 @Override
498                 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context,
499                         boolean enabled) {
500                     List<SearchIndexableResource> indexables = new ArrayList<>();
501                     SearchIndexableResource indexable = new SearchIndexableResource(context);
502                     indexable.xmlResId = R.xml.print_settings;
503                     indexables.add(indexable);
504                     return indexables;
505                 }
506             };
507 }
508