blob: c2050028928284c7b68dff1d7272abea019db7a1 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.media.photopicker;
import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI;
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* {@link ContentObserver} to listen to notification on database update
* (for e.g. cloud sync completion of a batch).
*
* <p> This observer listens to below uris:
* <ul>
* <li>content://media/picker_internal/update</li>
* <li>content://media/picker_internal/update/media</li>
* <li>content://media/picker_internal/update/album_content/ALBUM_ID</li>
* </ul>
*
* <p> The notification received will contain date_taken_ms
* {@link android.provider.CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS} or
* {@link android.provider.CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}.
* In case of album content, it will also contain
* {@link android.provider.CloudMediaProviderContract#EXTRA_ALBUM_ID}
*/
public class NotificationContentObserver extends ContentObserver {
private static final String TAG = "NotificationContentObserver";
/**
* Callback triggered upon receiving notification.
*/
public interface ContentObserverCallback{
/**
* Callers must implement this to handle the notification received.
*
* @param dateTakenMs date_taken_ms of the update
* @param albumId album_id in case of album_content update. Null in case of media update
*/
void onNotificationReceived(String dateTakenMs, String albumId);
}
// Key: Collection of preference keys, Value: onChange callback for keys
private final Map<List<String>, ContentObserverCallback> mUrisToCallback = new HashMap<>();
public static final String UPDATE = "update";
public static final String MEDIA = "media";
public static final String ALBUM_CONTENT = "album_content";
private final List<String> mKeys;
private final List<Uri> mUris;
private static final Uri URI_UPDATE = PICKER_INTERNAL_URI.buildUpon()
.appendPath(UPDATE).build();
private static final Uri URI_UPDATE_MEDIA = URI_UPDATE.buildUpon()
.appendPath(MEDIA).build();
private static final Uri URI_UPDATE_ALBUM_CONTENT = URI_UPDATE.buildUpon()
.appendPath(ALBUM_CONTENT).build();
public static final String REGEX_MEDIA = URI_UPDATE_MEDIA + "/[0-9]*$";
public static final Pattern PATTERN_MEDIA = Pattern.compile(REGEX_MEDIA);
public static final String REGEX_ALBUM_CONTENT = URI_UPDATE_ALBUM_CONTENT + "/[0-9]*/[0-9]*$";
public static final Pattern PATTERN_ALBUM_CONTENT = Pattern.compile(REGEX_ALBUM_CONTENT);
/**
* Creates a content observer.
*
* @param handler The handler to run {@link #onChange} on, or null if none.
*/
public NotificationContentObserver(Handler handler) {
super(handler);
mKeys = Arrays.asList(MEDIA, ALBUM_CONTENT);
mUris = Arrays.asList(URI_UPDATE_MEDIA, URI_UPDATE_ALBUM_CONTENT);
}
/**
* Registers {@link ContentObserver} instance of this class to the resolver for {@link #mUris}.
*/
public void register(ContentResolver contentResolver) {
for (Uri uri : mUris) {
contentResolver.registerContentObserver(uri, /* notifyForDescendants */ true,
/* observer */ this);
}
}
/**
* Unregisters ContentObserver
*/
public void unregister(ContentResolver contentResolver) {
contentResolver.unregisterContentObserver(this);
}
/**
* {@link ContentObserverCallback} is added to {@link ContentObserver} to handle the
* onNotificationReceived event triggered by the key collection of {@code keysToObserve}.
*
* <p> Note: Observer can observe the keys present in {@link #mKeys}.
*
* @param observerCallback A callback which is used to handle the onNotificationReceived event
* triggered by the key collection of {@code keysToObserve}.
*/
public void registerKeysToObserverCallback(List<String> keysToObserve,
ContentObserverCallback observerCallback) {
boolean hasValidKey = false;
for (String key : keysToObserve) {
if (!mKeys.contains(key)) {
Log.w(TAG, "NotificationContentObserver can not observer the key: " + key
+ ". Please pass valid keys from " + mKeys);
continue;
}
hasValidKey = true;
}
if (hasValidKey) {
mUrisToCallback.put(keysToObserve, observerCallback);
}
}
@Override
public final void onChange(boolean selfChange, Uri uri) {
String albumId = null;
String key = null;
if (PATTERN_MEDIA.matcher(uri.toString()).find()) {
key = MEDIA;
} else if (PATTERN_ALBUM_CONTENT.matcher(uri.toString()).find()) {
key = ALBUM_CONTENT;
albumId = uri.getPathSegments().get(3);
} else {
Log.w(TAG, "NotificationContentObserver cannot parse uri: " + uri
+ " . Please send correct uri path.");
return;
}
String dateTakenMs = uri.getLastPathSegment();
for (List<String> keys : mUrisToCallback.keySet()) {
if (keys.contains(key)) {
mUrisToCallback.get(keys).onNotificationReceived(dateTakenMs, albumId);
}
}
}
@VisibleForTesting
public Map<List<String>, ContentObserverCallback> getUrisToCallback() {
return mUrisToCallback;
}
}