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 android.support.media.tv;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.database.sqlite.SQLiteException;
22import android.graphics.Bitmap;
23import android.graphics.BitmapFactory;
24import android.media.tv.TvContract;
25import android.net.Uri;
26import android.support.annotation.NonNull;
27import android.support.annotation.WorkerThread;
28import android.util.Log;
29
30import java.io.FileNotFoundException;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.OutputStream;
34import java.net.HttpURLConnection;
35import java.net.URL;
36import java.net.URLConnection;
37
38/** A utility class for conveniently storing and loading channel logos. */
39@WorkerThread
40public class ChannelLogoUtils {
41    private static final String TAG = "ChannelLogoUtils";
42
43    private static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000;  // 3 sec
44    private static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000;  // 10 sec
45
46    /**
47     * Stores channel logo in the system content provider from the given URI. The method will try
48     * to fetch the image file and decode it into {@link Bitmap}. Once the image is successfully
49     * fetched, it will be stored in the system content provider and associated with the given
50     * channel ID.
51     *
52     * <p>The URI provided to this method can be a URL referring to a image file residing in some
53     * remote site/server, or a URI in one of the following formats:
54     *
55     *    <ul>
56     *        <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
57     *        <li>android.resource ({@link android.content.ContentResolver
58     *                                     #SCHEME_ANDROID_RESOURCE})</li>
59     *        <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
60     *    </ul>
61     *
62     * <p>This method should be run in a worker thread since it may require network connection,
63     * which will raise an exception if it's running in the main thread.
64     *
65     * @param context the context used to access the system content provider
66     * @param channelId the ID of the target channel with which the fetched logo should be
67     *                  associated
68     * @param logoUri the {@link Uri} of the logo file to be fetched and stored in the system
69     *                provider
70     *
71     * @return {@code true} if successfully fetched the image file referred by the give logo URI
72     *         and stored it in the system content provider, or {@code false} if failed.
73     *
74     * @see #loadChannelLogo(Context, long)
75     */
76    public static boolean storeChannelLogo(@NonNull Context context, long channelId,
77            @NonNull Uri logoUri) {
78        String scheme = logoUri.normalizeScheme().getScheme();
79        URLConnection urlConnection = null;
80        InputStream inputStream = null;
81        Bitmap fetchedLogo = null;
82        try {
83            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
84                    || ContentResolver.SCHEME_FILE.equals(scheme)
85                    || ContentResolver.SCHEME_CONTENT.equals(scheme)) {
86                // A local resource
87                inputStream = context.getContentResolver().openInputStream(logoUri);
88            } else {
89                // A remote resource, should be an valid URL.
90                urlConnection = getUrlConnection(logoUri.toString());
91                inputStream = urlConnection.getInputStream();
92            }
93            fetchedLogo = BitmapFactory.decodeStream(inputStream);
94        } catch (IOException e) {
95            Log.i(TAG, "Failed to get logo from the URI: " + logoUri + "\n", e);
96        } finally {
97            if (inputStream != null) {
98                try {
99                    inputStream.close();
100                } catch (IOException e) {
101                    // Do nothing.
102                }
103            }
104            if (urlConnection instanceof HttpURLConnection) {
105                ((HttpURLConnection) urlConnection).disconnect();
106            }
107        }
108        return fetchedLogo != null && storeChannelLogo(context, channelId, fetchedLogo);
109    }
110
111    /**
112     * Stores the given channel logo {@link Bitmap} in the system content provider and associate
113     * it with the given channel ID.
114     *
115     * @param context the context used to access the system content provider
116     * @param channelId the ID of the target channel with which the given logo should be associated
117     * @param logo the logo image to be stored
118     *
119     * @return {@code true} if successfully stored the logo in the system content provider,
120     *         otherwise {@code false}.
121     *
122     * @see #loadChannelLogo(Context, long)
123     */
124    public static boolean storeChannelLogo(@NonNull Context context, long channelId,
125            @NonNull Bitmap logo) {
126        boolean result = false;
127        Uri localUri = TvContract.buildChannelLogoUri(channelId);
128        try (OutputStream outputStream = context.getContentResolver().openOutputStream(localUri)) {
129            result = logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
130            outputStream.flush();
131        } catch (SQLiteException | IOException e) {
132            Log.i(TAG, "Failed to store the logo to the system content provider.\n", e);
133        }
134        return result;
135    }
136
137    /**
138     * A convenient helper method to get the channel logo associated to the given channel ID from
139     * the system content provider.
140     *
141     * @param context the context used to access the system content provider
142     * @param channelId the ID of the channel whose logo is supposed to be loaded
143     *
144     * @return the requested channel logo in {@link Bitmap}, or {@code null} if not available.
145     *
146     * @see #storeChannelLogo(Context, long, Uri)
147     * @see #storeChannelLogo(Context, long, Bitmap)
148     */
149    public static Bitmap loadChannelLogo(@NonNull Context context, long channelId) {
150        Bitmap channelLogo = null;
151        try {
152            channelLogo = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(
153                    TvContract.buildChannelLogoUri(channelId)));
154        } catch (FileNotFoundException e) {
155            // Channel logo is not found in the content provider.
156            Log.i(TAG, "Channel logo for channel (ID:" + channelId + ") not found.", e);
157        }
158        return channelLogo;
159    }
160
161    private static URLConnection getUrlConnection(String uriString) throws IOException {
162        URLConnection urlConnection = new URL(uriString).openConnection();
163        urlConnection.setConnectTimeout(CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
164        urlConnection.setReadTimeout(READ_TIMEOUT_MS_FOR_URLCONNECTION);
165        return urlConnection;
166    }
167}
168