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