1 /* 2 * Copyright (C) 2023 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.photopicker; 18 19 import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI; 20 21 import android.content.ContentResolver; 22 import android.database.ContentObserver; 23 import android.net.Uri; 24 import android.os.Handler; 25 import android.util.Log; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 29 import java.util.Arrays; 30 import java.util.HashMap; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.regex.Pattern; 34 35 /** 36 * {@link ContentObserver} to listen to notification on database update 37 * (for e.g. cloud sync completion of a batch). 38 * 39 * <p> This observer listens to below uris: 40 * <ul> 41 * <li>content://media/picker_internal/update</li> 42 * <li>content://media/picker_internal/update/media</li> 43 * <li>content://media/picker_internal/update/album_content/ALBUM_ID</li> 44 * </ul> 45 * 46 * <p> The notification received will contain date_taken_ms 47 * {@link android.provider.CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS} or 48 * {@link android.provider.CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}. 49 * In case of album content, it will also contain 50 * {@link android.provider.CloudMediaProviderContract#EXTRA_ALBUM_ID} 51 */ 52 public class NotificationContentObserver extends ContentObserver { 53 private static final String TAG = "NotificationContentObserver"; 54 55 /** 56 * Callback triggered upon receiving notification. 57 */ 58 public interface ContentObserverCallback{ 59 /** 60 * Callers must implement this to handle the notification received. 61 * 62 * @param dateTakenMs date_taken_ms of the update 63 * @param albumId album_id in case of album_content update. Null in case of media update 64 */ onNotificationReceived(String dateTakenMs, String albumId)65 void onNotificationReceived(String dateTakenMs, String albumId); 66 } 67 68 // Key: Collection of preference keys, Value: onChange callback for keys 69 private final Map<List<String>, ContentObserverCallback> mUrisToCallback = new HashMap<>(); 70 71 public static final String UPDATE = "update"; 72 public static final String MEDIA = "media"; 73 public static final String ALBUM_CONTENT = "album_content"; 74 75 private final List<String> mKeys; 76 private final List<Uri> mUris; 77 78 private static final Uri URI_UPDATE = PICKER_INTERNAL_URI.buildUpon() 79 .appendPath(UPDATE).build(); 80 81 private static final Uri URI_UPDATE_MEDIA = URI_UPDATE.buildUpon() 82 .appendPath(MEDIA).build(); 83 84 private static final Uri URI_UPDATE_ALBUM_CONTENT = URI_UPDATE.buildUpon() 85 .appendPath(ALBUM_CONTENT).build(); 86 87 public static final String REGEX_MEDIA = URI_UPDATE_MEDIA + "/[0-9]*$"; 88 public static final Pattern PATTERN_MEDIA = Pattern.compile(REGEX_MEDIA); 89 public static final String REGEX_ALBUM_CONTENT = URI_UPDATE_ALBUM_CONTENT + "/[0-9]*/[0-9]*$"; 90 public static final Pattern PATTERN_ALBUM_CONTENT = Pattern.compile(REGEX_ALBUM_CONTENT); 91 92 /** 93 * Creates a content observer. 94 * 95 * @param handler The handler to run {@link #onChange} on, or null if none. 96 */ NotificationContentObserver(Handler handler)97 public NotificationContentObserver(Handler handler) { 98 super(handler); 99 mKeys = Arrays.asList(MEDIA, ALBUM_CONTENT); 100 mUris = Arrays.asList(URI_UPDATE_MEDIA, URI_UPDATE_ALBUM_CONTENT); 101 } 102 103 /** 104 * Registers {@link ContentObserver} instance of this class to the resolver for {@link #mUris}. 105 */ register(ContentResolver contentResolver)106 public void register(ContentResolver contentResolver) { 107 for (Uri uri : mUris) { 108 contentResolver.registerContentObserver(uri, /* notifyForDescendants */ true, 109 /* observer */ this); 110 } 111 } 112 113 /** 114 * Unregisters ContentObserver 115 */ unregister(ContentResolver contentResolver)116 public void unregister(ContentResolver contentResolver) { 117 contentResolver.unregisterContentObserver(this); 118 } 119 120 /** 121 * {@link ContentObserverCallback} is added to {@link ContentObserver} to handle the 122 * onNotificationReceived event triggered by the key collection of {@code keysToObserve}. 123 * 124 * <p> Note: Observer can observe the keys present in {@link #mKeys}. 125 * 126 * @param observerCallback A callback which is used to handle the onNotificationReceived event 127 * triggered by the key collection of {@code keysToObserve}. 128 */ registerKeysToObserverCallback(List<String> keysToObserve, ContentObserverCallback observerCallback)129 public void registerKeysToObserverCallback(List<String> keysToObserve, 130 ContentObserverCallback observerCallback) { 131 boolean hasValidKey = false; 132 for (String key : keysToObserve) { 133 if (!mKeys.contains(key)) { 134 Log.w(TAG, "NotificationContentObserver can not observer the key: " + key 135 + ". Please pass valid keys from " + mKeys); 136 continue; 137 } 138 hasValidKey = true; 139 } 140 if (hasValidKey) { 141 mUrisToCallback.put(keysToObserve, observerCallback); 142 } 143 } 144 145 @Override onChange(boolean selfChange, Uri uri)146 public final void onChange(boolean selfChange, Uri uri) { 147 String albumId = null; 148 String key = null; 149 150 if (PATTERN_MEDIA.matcher(uri.toString()).find()) { 151 key = MEDIA; 152 } else if (PATTERN_ALBUM_CONTENT.matcher(uri.toString()).find()) { 153 key = ALBUM_CONTENT; 154 albumId = uri.getPathSegments().get(3); 155 } else { 156 Log.w(TAG, "NotificationContentObserver cannot parse uri: " + uri 157 + " . Please send correct uri path."); 158 return; 159 } 160 161 String dateTakenMs = uri.getLastPathSegment(); 162 163 for (List<String> keys : mUrisToCallback.keySet()) { 164 if (keys.contains(key)) { 165 mUrisToCallback.get(keys).onNotificationReceived(dateTakenMs, albumId); 166 } 167 } 168 } 169 170 @VisibleForTesting getUrisToCallback()171 public Map<List<String>, ContentObserverCallback> getUrisToCallback() { 172 return mUrisToCallback; 173 } 174 } 175