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.providers.media.scan;
18 
19 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
20 import static org.xmlpull.v1.XmlPullParser.START_TAG;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.ContentProviderOperation;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.provider.MediaStore;
30 import android.provider.MediaStore.MediaColumns;
31 import android.text.TextUtils;
32 import android.util.Xml;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.BufferedReader;
38 import java.io.File;
39 import java.io.FileInputStream;
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.InputStreamReader;
44 import java.nio.charset.StandardCharsets;
45 import java.nio.file.Path;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 
51 public class PlaylistResolver {
52     private static final Pattern PATTERN_PLS = Pattern.compile("File(\\d+)=(.+)");
53 
54     private static final String TAG_MEDIA = "media";
55     private static final String ATTR_SRC = "src";
56 
57     /**
58      * Resolve the contents of the given
59      * {@link android.provider.MediaStore.Audio.Playlists} item, returning a
60      * list of {@link ContentProviderOperation} that will update all members.
61      */
resolvePlaylist( @onNull ContentResolver resolver, @NonNull Uri uri)62     public static @NonNull List<ContentProviderOperation> resolvePlaylist(
63             @NonNull ContentResolver resolver, @NonNull Uri uri) throws IOException {
64         final String mimeType;
65         final File file;
66         try (Cursor cursor = resolver.query(uri, new String[] {
67                 MediaColumns.MIME_TYPE, MediaColumns.DATA
68         }, null, null, null)) {
69             if (!cursor.moveToFirst()) {
70                 throw new FileNotFoundException(uri.toString());
71             }
72             mimeType = cursor.getString(0);
73             file = new File(cursor.getString(1));
74         }
75 
76         switch (mimeType) {
77             case "audio/x-mpegurl":
78             case "audio/mpegurl":
79             case "application/x-mpegurl":
80             case "application/vnd.apple.mpegurl":
81                 return resolvePlaylistM3u(resolver, uri, file);
82             case "audio/x-scpls":
83                 return resolvePlaylistPls(resolver, uri, file);
84             case "application/vnd.ms-wpl":
85             case "video/x-ms-asf":
86                 return resolvePlaylistWpl(resolver, uri, file);
87             default:
88                 throw new IOException("Unsupported playlist of type " + mimeType);
89         }
90     }
91 
resolvePlaylistM3u( @onNull ContentResolver resolver, @NonNull Uri uri, @NonNull File file)92     private static @NonNull List<ContentProviderOperation> resolvePlaylistM3u(
93             @NonNull ContentResolver resolver, @NonNull Uri uri, @NonNull File file)
94             throws IOException {
95         final Path parentPath = file.getParentFile().toPath();
96         final List<ContentProviderOperation> res = new ArrayList<>();
97         res.add(ContentProviderOperation.newDelete(getPlaylistMembersUri(uri)).build());
98         try (BufferedReader reader = new BufferedReader(
99                 new InputStreamReader(new FileInputStream(file)))) {
100             String line;
101             while ((line = reader.readLine()) != null) {
102                 if (!TextUtils.isEmpty(line) && !line.startsWith("#")) {
103                     final int itemIndex = res.size() + 1;
104                     final File itemFile = parentPath.resolve(line).toFile();
105                     try {
106                         res.add(resolvePlaylistItem(resolver, uri, itemIndex, itemFile));
107                     } catch (FileNotFoundException ignored) {
108                     }
109                 }
110             }
111         }
112         return res;
113     }
114 
resolvePlaylistPls( @onNull ContentResolver resolver, @NonNull Uri uri, @NonNull File file)115     private static @NonNull List<ContentProviderOperation> resolvePlaylistPls(
116             @NonNull ContentResolver resolver, @NonNull Uri uri, @NonNull File file)
117             throws IOException {
118         final Path parentPath = file.getParentFile().toPath();
119         final List<ContentProviderOperation> res = new ArrayList<>();
120         res.add(ContentProviderOperation.newDelete(getPlaylistMembersUri(uri)).build());
121         try (BufferedReader reader = new BufferedReader(
122                 new InputStreamReader(new FileInputStream(file)))) {
123             String line;
124             while ((line = reader.readLine()) != null) {
125                 final Matcher matcher = PATTERN_PLS.matcher(line);
126                 if (matcher.matches()) {
127                     final int itemIndex = Integer.parseInt(matcher.group(1));
128                     final File itemFile = parentPath.resolve(matcher.group(2)).toFile();
129                     try {
130                         res.add(resolvePlaylistItem(resolver, uri, itemIndex, itemFile));
131                     } catch (FileNotFoundException ignored) {
132                     }
133                 }
134             }
135         }
136         return res;
137     }
138 
resolvePlaylistWpl( @onNull ContentResolver resolver, @NonNull Uri uri, @NonNull File file)139     private static @NonNull List<ContentProviderOperation> resolvePlaylistWpl(
140             @NonNull ContentResolver resolver, @NonNull Uri uri, @NonNull File file)
141             throws IOException {
142         final Path parentPath = file.getParentFile().toPath();
143         final List<ContentProviderOperation> res = new ArrayList<>();
144         res.add(ContentProviderOperation.newDelete(getPlaylistMembersUri(uri)).build());
145         try (InputStream in = new FileInputStream(file)) {
146             try {
147                 final XmlPullParser parser = Xml.newPullParser();
148                 parser.setInput(in, StandardCharsets.UTF_8.name());
149 
150                 int type;
151                 while ((type = parser.next()) != END_DOCUMENT) {
152                     if (type != START_TAG) continue;
153 
154                     if (TAG_MEDIA.equals(parser.getName())) {
155                         final String src = parser.getAttributeValue(null, ATTR_SRC);
156                         if (src != null) {
157                             final int itemIndex = res.size() + 1;
158                             final File itemFile = parentPath.resolve(src).toFile();
159                             try {
160                                 res.add(resolvePlaylistItem(resolver, uri, itemIndex, itemFile));
161                             } catch (FileNotFoundException ignored) {
162                             }
163                         }
164                     }
165                 }
166             } catch (XmlPullParserException e) {
167                 throw new IOException(e);
168             }
169         }
170         return res;
171     }
172 
resolvePlaylistItem( @onNull ContentResolver resolver, @NonNull Uri uri, int itemIndex, File itemFile)173     private static @Nullable ContentProviderOperation resolvePlaylistItem(
174             @NonNull ContentResolver resolver, @NonNull Uri uri, int itemIndex, File itemFile)
175             throws IOException {
176         final Uri audioUri = MediaStore.Audio.Media.getContentUri(MediaStore.getVolumeName(uri));
177         try (Cursor cursor = resolver.query(audioUri,
178                 new String[] { MediaColumns._ID }, MediaColumns.DATA + "=?",
179                 new String[] { itemFile.getCanonicalPath() }, null)) {
180             if (!cursor.moveToFirst()) {
181                 throw new FileNotFoundException(uri.toString());
182             }
183 
184             final ContentProviderOperation.Builder op = ContentProviderOperation
185                     .newInsert(getPlaylistMembersUri(uri));
186             op.withValue(MediaStore.Audio.Playlists.Members.PLAY_ORDER, itemIndex);
187             op.withValue(MediaStore.Audio.Playlists.Members.AUDIO_ID, cursor.getInt(0));
188             return op.build();
189         }
190     }
191 
getPlaylistMembersUri(@onNull Uri uri)192     private static @NonNull Uri getPlaylistMembersUri(@NonNull Uri uri) {
193         return MediaStore.Audio.Playlists.Members.getContentUri(MediaStore.getVolumeName(uri),
194                 ContentUris.parseId(uri));
195     }
196 }
197