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