1/*
2 * Copyright (C) 2017 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.tv.data;
18
19import android.annotation.TargetApi;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.Context;
23import android.database.Cursor;
24import android.database.SQLException;
25import android.graphics.Bitmap;
26import android.graphics.drawable.BitmapDrawable;
27import android.graphics.drawable.Drawable;
28import android.media.tv.TvContract;
29import android.net.Uri;
30import android.os.AsyncTask;
31import android.os.Build;
32import android.support.annotation.IntDef;
33import android.support.annotation.MainThread;
34import android.support.media.tv.ChannelLogoUtils;
35import android.support.media.tv.PreviewProgram;
36import android.util.Log;
37import android.util.Pair;
38import com.android.tv.R;
39import com.android.tv.common.util.PermissionUtils;
40import java.lang.annotation.Retention;
41import java.lang.annotation.RetentionPolicy;
42import java.util.HashMap;
43import java.util.Map;
44import java.util.Set;
45import java.util.concurrent.CopyOnWriteArraySet;
46
47/** Class to manage the preview data. */
48@TargetApi(Build.VERSION_CODES.O)
49@MainThread
50public class PreviewDataManager {
51    private static final String TAG = "PreviewDataManager";
52    private static final boolean DEBUG = false;
53
54    /** Invalid preview channel ID. */
55    public static final long INVALID_PREVIEW_CHANNEL_ID = -1;
56
57    @IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL})
58    @Retention(RetentionPolicy.SOURCE)
59    public @interface PreviewChannelType {}
60
61    /** Type of default preview channel */
62    public static final int TYPE_DEFAULT_PREVIEW_CHANNEL = 1;
63    /** Type of recorded program channel */
64    public static final int TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2;
65
66    private final Context mContext;
67    private final ContentResolver mContentResolver;
68    private boolean mLoadFinished;
69    private PreviewData mPreviewData = new PreviewData();
70    private final Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
71
72    private QueryPreviewDataTask mQueryPreviewTask;
73    private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks = new HashMap<>();
74    private final Map<Long, UpdatePreviewProgramTask> mUpdatePreviewProgramTasks = new HashMap<>();
75
76    private final int mPreviewChannelLogoWidth;
77    private final int mPreviewChannelLogoHeight;
78
79    public PreviewDataManager(Context context) {
80        mContext = context.getApplicationContext();
81        mContentResolver = context.getContentResolver();
82        mPreviewChannelLogoWidth =
83                mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_width);
84        mPreviewChannelLogoHeight =
85                mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_height);
86    }
87
88    /** Starts the preview data manager. */
89    public void start() {
90        if (mQueryPreviewTask == null) {
91            mQueryPreviewTask = new QueryPreviewDataTask();
92            mQueryPreviewTask.execute();
93        }
94    }
95
96    /** Stops the preview data manager. */
97    public void stop() {
98        if (mQueryPreviewTask != null) {
99            mQueryPreviewTask.cancel(true);
100        }
101        for (CreatePreviewChannelTask createPreviewChannelTask :
102                mCreatePreviewChannelTasks.values()) {
103            createPreviewChannelTask.cancel(true);
104        }
105        for (UpdatePreviewProgramTask updatePreviewProgramTask :
106                mUpdatePreviewProgramTasks.values()) {
107            updatePreviewProgramTask.cancel(true);
108        }
109
110        mQueryPreviewTask = null;
111        mCreatePreviewChannelTasks.clear();
112        mUpdatePreviewProgramTasks.clear();
113    }
114
115    /** Gets preview channel ID from the preview channel type. */
116    public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
117        return mPreviewData.getPreviewChannelId(previewChannelType);
118    }
119
120    /** Creates default preview channel. */
121    public void createDefaultPreviewChannel(
122            OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
123        createPreviewChannel(TYPE_DEFAULT_PREVIEW_CHANNEL, onPreviewChannelCreationResultListener);
124    }
125
126    /** Creates a preview channel for specific channel type. */
127    public void createPreviewChannel(
128            @PreviewChannelType long previewChannelType,
129            OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
130        CreatePreviewChannelTask currentRunningCreateTask =
131                mCreatePreviewChannelTasks.get(previewChannelType);
132        if (currentRunningCreateTask == null) {
133            CreatePreviewChannelTask createPreviewChannelTask =
134                    new CreatePreviewChannelTask(previewChannelType);
135            createPreviewChannelTask.addOnPreviewChannelCreationResultListener(
136                    onPreviewChannelCreationResultListener);
137            createPreviewChannelTask.execute();
138            mCreatePreviewChannelTasks.put(previewChannelType, createPreviewChannelTask);
139        } else {
140            currentRunningCreateTask.addOnPreviewChannelCreationResultListener(
141                    onPreviewChannelCreationResultListener);
142        }
143    }
144
145    /** Returns {@code true} if the preview data is loaded. */
146    public boolean isLoadFinished() {
147        return mLoadFinished;
148    }
149
150    /** Adds listener. */
151    public void addListener(PreviewDataListener previewDataListener) {
152        mPreviewDataListeners.add(previewDataListener);
153    }
154
155    /** Removes listener. */
156    public void removeListener(PreviewDataListener previewDataListener) {
157        mPreviewDataListeners.remove(previewDataListener);
158    }
159
160    /** Updates the preview programs table for a specific preview channel. */
161    public void updatePreviewProgramsForChannel(
162            long previewChannelId,
163            Set<PreviewProgramContent> programs,
164            PreviewDataListener previewDataListener) {
165        UpdatePreviewProgramTask currentRunningUpdateTask =
166                mUpdatePreviewProgramTasks.get(previewChannelId);
167        if (currentRunningUpdateTask != null
168                && currentRunningUpdateTask.getPrograms().equals(programs)) {
169            currentRunningUpdateTask.addPreviewDataListener(previewDataListener);
170            return;
171        }
172        UpdatePreviewProgramTask updatePreviewProgramTask =
173                new UpdatePreviewProgramTask(previewChannelId, programs);
174        updatePreviewProgramTask.addPreviewDataListener(previewDataListener);
175        if (currentRunningUpdateTask != null) {
176            currentRunningUpdateTask.cancel(true);
177            currentRunningUpdateTask.saveStatus();
178            updatePreviewProgramTask.addPreviewDataListeners(
179                    currentRunningUpdateTask.getPreviewDataListeners());
180        }
181        updatePreviewProgramTask.execute();
182        mUpdatePreviewProgramTasks.put(previewChannelId, updatePreviewProgramTask);
183    }
184
185    private void notifyPreviewDataLoadFinished() {
186        for (PreviewDataListener l : mPreviewDataListeners) {
187            l.onPreviewDataLoadFinished();
188        }
189    }
190
191    public interface PreviewDataListener {
192        /** Called when the preview data is loaded. */
193        void onPreviewDataLoadFinished();
194
195        /** Called when the preview data is updated. */
196        void onPreviewDataUpdateFinished();
197    }
198
199    public interface OnPreviewChannelCreationResultListener {
200        /**
201         * Called when the creation of preview channel is finished.
202         *
203         * @param createdPreviewChannelId The preview channel ID if created successfully, otherwise
204         *     it's {@value #INVALID_PREVIEW_CHANNEL_ID}.
205         */
206        void onPreviewChannelCreationResult(long createdPreviewChannelId);
207    }
208
209    private final class QueryPreviewDataTask extends AsyncTask<Void, Void, PreviewData> {
210        private final String PARAM_PREVIEW = "preview";
211        private final String mChannelSelection = TvContract.Channels.COLUMN_PACKAGE_NAME + "=?";
212
213        @Override
214        protected PreviewData doInBackground(Void... voids) {
215            // Query preview channels and programs.
216            if (DEBUG) Log.d(TAG, "QueryPreviewDataTask.doInBackground");
217            PreviewData previewData = new PreviewData();
218            try {
219                Uri previewChannelsUri =
220                        PreviewDataUtils.addQueryParamToUri(
221                                TvContract.Channels.CONTENT_URI,
222                                new Pair<>(PARAM_PREVIEW, String.valueOf(true)));
223                String packageName = mContext.getPackageName();
224                if (PermissionUtils.hasAccessAllEpg(mContext)) {
225                    try (Cursor cursor =
226                            mContentResolver.query(
227                                    previewChannelsUri,
228                                    android.support.media.tv.Channel.PROJECTION,
229                                    mChannelSelection,
230                                    new String[] {packageName},
231                                    null)) {
232                        if (cursor != null) {
233                            while (cursor.moveToNext()) {
234                                android.support.media.tv.Channel previewChannel =
235                                        android.support.media.tv.Channel.fromCursor(cursor);
236                                Long previewChannelType = previewChannel.getInternalProviderFlag1();
237                                if (previewChannelType != null) {
238                                    previewData.addPreviewChannelId(
239                                            previewChannelType, previewChannel.getId());
240                                }
241                            }
242                        }
243                    }
244                } else {
245                    try (Cursor cursor =
246                            mContentResolver.query(
247                                    previewChannelsUri,
248                                    android.support.media.tv.Channel.PROJECTION,
249                                    null,
250                                    null,
251                                    null)) {
252                        if (cursor != null) {
253                            while (cursor.moveToNext()) {
254                                android.support.media.tv.Channel previewChannel =
255                                        android.support.media.tv.Channel.fromCursor(cursor);
256                                Long previewChannelType = previewChannel.getInternalProviderFlag1();
257                                if (packageName.equals(previewChannel.getPackageName())
258                                        && previewChannelType != null) {
259                                    previewData.addPreviewChannelId(
260                                            previewChannelType, previewChannel.getId());
261                                }
262                            }
263                        }
264                    }
265                }
266
267                for (long previewChannelId : previewData.getAllPreviewChannelIds().values()) {
268                    Uri previewProgramsUriForPreviewChannel =
269                            TvContract.buildPreviewProgramsUriForChannel(previewChannelId);
270                    try (Cursor previewProgramCursor =
271                            mContentResolver.query(
272                                    previewProgramsUriForPreviewChannel,
273                                    PreviewProgram.PROJECTION,
274                                    null,
275                                    null,
276                                    null)) {
277                        if (previewProgramCursor != null) {
278                            while (previewProgramCursor.moveToNext()) {
279                                PreviewProgram previewProgram =
280                                        PreviewProgram.fromCursor(previewProgramCursor);
281                                previewData.addPreviewProgram(previewProgram);
282                            }
283                        }
284                    }
285                }
286            } catch (SQLException e) {
287                Log.w(TAG, "Unable to get preview data", e);
288            }
289            return previewData;
290        }
291
292        @Override
293        protected void onPostExecute(PreviewData result) {
294            super.onPostExecute(result);
295            if (mQueryPreviewTask == this) {
296                mQueryPreviewTask = null;
297                mPreviewData = new PreviewData(result);
298                mLoadFinished = true;
299                notifyPreviewDataLoadFinished();
300            }
301        }
302    }
303
304    private final class CreatePreviewChannelTask extends AsyncTask<Void, Void, Long> {
305        private final long mPreviewChannelType;
306        private Set<OnPreviewChannelCreationResultListener>
307                mOnPreviewChannelCreationResultListeners = new CopyOnWriteArraySet<>();
308
309        public CreatePreviewChannelTask(long previewChannelType) {
310            mPreviewChannelType = previewChannelType;
311        }
312
313        public void addOnPreviewChannelCreationResultListener(
314                OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
315            if (onPreviewChannelCreationResultListener != null) {
316                mOnPreviewChannelCreationResultListeners.add(
317                        onPreviewChannelCreationResultListener);
318            }
319        }
320
321        @Override
322        protected Long doInBackground(Void... params) {
323            if (DEBUG) Log.d(TAG, "CreatePreviewChannelTask.doInBackground");
324            long previewChannelId;
325            try {
326                Uri channelUri =
327                        mContentResolver.insert(
328                                TvContract.Channels.CONTENT_URI,
329                                PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType)
330                                        .toContentValues());
331                if (channelUri != null) {
332                    previewChannelId = ContentUris.parseId(channelUri);
333                } else {
334                    Log.e(TAG, "Fail to insert preview channel");
335                    return INVALID_PREVIEW_CHANNEL_ID;
336                }
337            } catch (UnsupportedOperationException | NumberFormatException e) {
338                Log.e(TAG, "Fail to get channel ID");
339                return INVALID_PREVIEW_CHANNEL_ID;
340            }
341            Drawable appIcon = mContext.getApplicationInfo().loadIcon(mContext.getPackageManager());
342            if (appIcon != null && appIcon instanceof BitmapDrawable) {
343                ChannelLogoUtils.storeChannelLogo(
344                        mContext,
345                        previewChannelId,
346                        Bitmap.createScaledBitmap(
347                                ((BitmapDrawable) appIcon).getBitmap(),
348                                mPreviewChannelLogoWidth,
349                                mPreviewChannelLogoHeight,
350                                false));
351            }
352            return previewChannelId;
353        }
354
355        @Override
356        protected void onPostExecute(Long result) {
357            super.onPostExecute(result);
358            if (result != INVALID_PREVIEW_CHANNEL_ID) {
359                mPreviewData.addPreviewChannelId(mPreviewChannelType, result);
360            }
361            for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener :
362                    mOnPreviewChannelCreationResultListeners) {
363                onPreviewChannelCreationResultListener.onPreviewChannelCreationResult(result);
364            }
365            mCreatePreviewChannelTasks.remove(mPreviewChannelType);
366        }
367    }
368
369    /**
370     * Updates the whole data which belongs to the package in preview programs table for a specific
371     * preview channel with a set of {@link PreviewProgramContent}.
372     */
373    private final class UpdatePreviewProgramTask extends AsyncTask<Void, Void, Void> {
374        private long mPreviewChannelId;
375        private Set<PreviewProgramContent> mPrograms;
376        private Map<Long, Long> mCurrentProgramId2PreviewProgramId;
377        private Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
378
379        public UpdatePreviewProgramTask(
380                long previewChannelId, Set<PreviewProgramContent> programs) {
381            mPreviewChannelId = previewChannelId;
382            mPrograms = programs;
383            if (mPreviewData.getPreviewProgramIds(previewChannelId) == null) {
384                mCurrentProgramId2PreviewProgramId = new HashMap<>();
385            } else {
386                mCurrentProgramId2PreviewProgramId =
387                        new HashMap<>(mPreviewData.getPreviewProgramIds(previewChannelId));
388            }
389        }
390
391        public void addPreviewDataListener(PreviewDataListener previewDataListener) {
392            if (previewDataListener != null) {
393                mPreviewDataListeners.add(previewDataListener);
394            }
395        }
396
397        public void addPreviewDataListeners(Set<PreviewDataListener> previewDataListeners) {
398            if (previewDataListeners != null) {
399                mPreviewDataListeners.addAll(previewDataListeners);
400            }
401        }
402
403        public Set<PreviewProgramContent> getPrograms() {
404            return mPrograms;
405        }
406
407        public Set<PreviewDataListener> getPreviewDataListeners() {
408            return mPreviewDataListeners;
409        }
410
411        @Override
412        protected Void doInBackground(Void... params) {
413            if (DEBUG) Log.d(TAG, "UpdatePreviewProgamTask.doInBackground");
414            Map<Long, Long> uncheckedPrograms = new HashMap<>(mCurrentProgramId2PreviewProgramId);
415            for (PreviewProgramContent program : mPrograms) {
416                if (isCancelled()) {
417                    return null;
418                }
419                Long existingPreviewProgramId = uncheckedPrograms.remove(program.getId());
420                if (existingPreviewProgramId != null) {
421                    if (DEBUG)
422                        Log.d(
423                                TAG,
424                                "Preview program "
425                                        + existingPreviewProgramId
426                                        + " "
427                                        + "already exists for program "
428                                        + program.getId());
429                    continue;
430                }
431                try {
432                    Uri programUri =
433                            mContentResolver.insert(
434                                    TvContract.PreviewPrograms.CONTENT_URI,
435                                    PreviewDataUtils.createPreviewProgramFromContent(program)
436                                            .toContentValues());
437                    if (programUri != null) {
438                        long previewProgramId = ContentUris.parseId(programUri);
439                        mCurrentProgramId2PreviewProgramId.put(program.getId(), previewProgramId);
440                        if (DEBUG) Log.d(TAG, "Add new preview program " + previewProgramId);
441                    } else {
442                        Log.e(TAG, "Fail to insert preview program");
443                    }
444                } catch (Exception e) {
445                    Log.e(TAG, "Fail to get preview program ID");
446                }
447            }
448
449            for (Long key : uncheckedPrograms.keySet()) {
450                if (isCancelled()) {
451                    return null;
452                }
453                try {
454                    if (DEBUG) Log.d(TAG, "Remove preview program " + uncheckedPrograms.get(key));
455                    mContentResolver.delete(
456                            TvContract.buildPreviewProgramUri(uncheckedPrograms.get(key)),
457                            null,
458                            null);
459                    mCurrentProgramId2PreviewProgramId.remove(key);
460                } catch (Exception e) {
461                    Log.e(TAG, "Fail to remove preview program " + uncheckedPrograms.get(key));
462                }
463            }
464            return null;
465        }
466
467        @Override
468        protected void onPostExecute(Void result) {
469            super.onPostExecute(result);
470            mPreviewData.setPreviewProgramIds(
471                    mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
472            mUpdatePreviewProgramTasks.remove(mPreviewChannelId);
473            for (PreviewDataListener previewDataListener : mPreviewDataListeners) {
474                previewDataListener.onPreviewDataUpdateFinished();
475            }
476        }
477
478        public void saveStatus() {
479            mPreviewData.setPreviewProgramIds(
480                    mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
481        }
482    }
483
484    /** Class to store the query result of preview data. */
485    private static final class PreviewData {
486        private Map<Long, Long> mPreviewChannelType2Id = new HashMap<>();
487        private Map<Long, Map<Long, Long>> mProgramId2PreviewProgramId = new HashMap<>();
488
489        PreviewData() {
490            mPreviewChannelType2Id = new HashMap<>();
491            mProgramId2PreviewProgramId = new HashMap<>();
492        }
493
494        PreviewData(PreviewData previewData) {
495            mPreviewChannelType2Id = new HashMap<>(previewData.mPreviewChannelType2Id);
496            mProgramId2PreviewProgramId = new HashMap<>(previewData.mProgramId2PreviewProgramId);
497        }
498
499        public void addPreviewProgram(PreviewProgram previewProgram) {
500            long previewChannelId = previewProgram.getChannelId();
501            Map<Long, Long> programId2PreviewProgram =
502                    mProgramId2PreviewProgramId.get(previewChannelId);
503            if (programId2PreviewProgram == null) {
504                programId2PreviewProgram = new HashMap<>();
505            }
506            mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgram);
507            if (previewProgram.getInternalProviderId() != null) {
508                programId2PreviewProgram.put(
509                        Long.parseLong(previewProgram.getInternalProviderId()),
510                        previewProgram.getId());
511            }
512        }
513
514        public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
515            Long result = mPreviewChannelType2Id.get(previewChannelType);
516            return result == null ? INVALID_PREVIEW_CHANNEL_ID : result;
517        }
518
519        public Map<Long, Long> getAllPreviewChannelIds() {
520            return mPreviewChannelType2Id;
521        }
522
523        public void addPreviewChannelId(long previewChannelType, long previewChannelId) {
524            mPreviewChannelType2Id.put(previewChannelType, previewChannelId);
525        }
526
527        public void removePreviewChannelId(long previewChannelType) {
528            mPreviewChannelType2Id.remove(previewChannelType);
529        }
530
531        public void removePreviewChannel(long previewChannelId) {
532            removePreviewChannelId(previewChannelId);
533            removePreviewProgramIds(previewChannelId);
534        }
535
536        public Map<Long, Long> getPreviewProgramIds(long previewChannelId) {
537            return mProgramId2PreviewProgramId.get(previewChannelId);
538        }
539
540        public Map<Long, Map<Long, Long>> getAllPreviewProgramIds() {
541            return mProgramId2PreviewProgramId;
542        }
543
544        public void setPreviewProgramIds(
545                long previewChannelId, Map<Long, Long> programId2PreviewProgramId) {
546            mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgramId);
547        }
548
549        public void removePreviewProgramIds(long previewChannelId) {
550            mProgramId2PreviewProgramId.remove(previewChannelId);
551        }
552    }
553
554    /** A utils class for preview data. */
555    public static final class PreviewDataUtils {
556        /** Creates a preview channel. */
557        public static android.support.media.tv.Channel createPreviewChannel(
558                Context context, @PreviewChannelType long previewChannelType) {
559            if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
560                return createRecordedProgramPreviewChannel(context, previewChannelType);
561            }
562            return createDefaultPreviewChannel(context, previewChannelType);
563        }
564
565        private static android.support.media.tv.Channel createDefaultPreviewChannel(
566                Context context, @PreviewChannelType long previewChannelType) {
567            android.support.media.tv.Channel.Builder builder =
568                    new android.support.media.tv.Channel.Builder();
569            CharSequence appLabel =
570                    context.getApplicationInfo().loadLabel(context.getPackageManager());
571            CharSequence appDescription =
572                    context.getApplicationInfo().loadDescription(context.getPackageManager());
573            builder.setType(TvContract.Channels.TYPE_PREVIEW)
574                    .setDisplayName(appLabel == null ? null : appLabel.toString())
575                    .setDescription(appDescription == null ? null : appDescription.toString())
576                    .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
577                    .setInternalProviderFlag1(previewChannelType);
578            return builder.build();
579        }
580
581        private static android.support.media.tv.Channel createRecordedProgramPreviewChannel(
582                Context context, @PreviewChannelType long previewChannelType) {
583            android.support.media.tv.Channel.Builder builder =
584                    new android.support.media.tv.Channel.Builder();
585            builder.setType(TvContract.Channels.TYPE_PREVIEW)
586                    .setDisplayName(
587                            context.getResources()
588                                    .getString(R.string.recorded_programs_preview_channel))
589                    .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
590                    .setInternalProviderFlag1(previewChannelType);
591            return builder.build();
592        }
593
594        /** Creates a preview program. */
595        public static PreviewProgram createPreviewProgramFromContent(
596                PreviewProgramContent program) {
597            PreviewProgram.Builder builder = new PreviewProgram.Builder();
598            builder.setChannelId(program.getPreviewChannelId())
599                    .setType(program.getType())
600                    .setLive(program.getLive())
601                    .setTitle(program.getTitle())
602                    .setDescription(program.getDescription())
603                    .setPosterArtUri(program.getPosterArtUri())
604                    .setIntentUri(program.getIntentUri())
605                    .setPreviewVideoUri(program.getPreviewVideoUri())
606                    .setInternalProviderId(Long.toString(program.getId()))
607                    .setContentId(program.getIntentUri().toString());
608            return builder.build();
609        }
610
611        /** Appends query parameters to a Uri. */
612        public static Uri addQueryParamToUri(Uri uri, Pair<String, String> param) {
613            return uri.buildUpon().appendQueryParameter(param.first, param.second).build();
614        }
615    }
616}
617