1 /* 2 * Copyright (C) 2024 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 android.provider.mediacognitionutils; 18 19 import static android.provider.MediaCognitionService.ProcessingTypes; 20 21 import android.annotation.NonNull; 22 import android.database.CursorWindow; 23 import android.os.RemoteException; 24 import android.provider.MediaCognitionProcessingCallback; 25 import android.provider.MediaCognitionProcessingRequest; 26 import android.provider.MediaCognitionProcessingResponse; 27 import android.provider.MediaCognitionService; 28 import android.util.Log; 29 30 import java.util.ArrayList; 31 import java.util.List; 32 33 /** 34 * @hide 35 */ 36 public final class ProcessMediaCallbackImpl implements MediaCognitionProcessingCallback { 37 38 private final ICognitionProcessMediaCallbackInternal mBinderCallback; 39 40 private static final String RESPONSE_CURSOR_NAME = "MediaCognitionResponseCursorWindow"; 41 42 /** 43 * To ensure a callback instance is called only once. 44 */ 45 private boolean mCalled; 46 ProcessMediaCallbackImpl(ICognitionProcessMediaCallbackInternal binderCallback)47 public ProcessMediaCallbackImpl(ICognitionProcessMediaCallbackInternal binderCallback) { 48 mBinderCallback = binderCallback; 49 mCalled = false; 50 } 51 52 @Override onSuccess(@onNull List<MediaCognitionProcessingResponse> responses)53 public void onSuccess(@NonNull List<MediaCognitionProcessingResponse> responses) { 54 if (mCalled) { 55 Log.w(MediaCognitionService.TAG, "The callback can only be called once"); 56 return; 57 } 58 mCalled = true; 59 if (responses.size() == 0) { 60 Log.w(MediaCognitionService.TAG, "Empty response"); 61 return; 62 } 63 try { 64 mBinderCallback.onProcessMediaSuccess(wrapInCursorWindows(responses)); 65 } catch (RemoteException e) { 66 Log.w(MediaCognitionService.TAG, 67 "Unable to send callback of Process Media result", e); 68 } 69 70 } 71 72 @Override onFailure(@onNull String message)73 public void onFailure(@NonNull String message) { 74 if (mCalled) { 75 Log.w(MediaCognitionService.TAG, "The callback can only be called once"); 76 return; 77 } 78 mCalled = true; 79 try { 80 mBinderCallback.onProcessMediaFailure(message); 81 } catch (RemoteException e) { 82 Log.w(MediaCognitionService.TAG, 83 "Unable to send callback of Process Media failure", e); 84 } 85 } 86 87 88 /** 89 * This runs in the app's process which has implemented {@link MediaCognitionService} 90 * Before passing the response to MediaProvider, the responses should be wrapped 91 * using {@link CursorWindow} to avoid risk of parcelable limit of an ipc. 92 * 93 */ wrapInCursorWindows(List<MediaCognitionProcessingResponse> responses)94 private CursorWindow[] wrapInCursorWindows(List<MediaCognitionProcessingResponse> responses) { 95 ArrayList<CursorWindow> windows = new ArrayList<CursorWindow>(); 96 CursorWindow window = new CursorWindow(RESPONSE_CURSOR_NAME); 97 // number of columns = number of processing available + Id of the media 98 final int numColumns = ProcessingTypes.class.getDeclaredFields().length + 1; 99 window.setNumColumns(numColumns); 100 windows.add(window); 101 /* 102 In the following loop, we iterate through responses and add them to current CursorWindow. 103 If the window is full then addition of a row to that window will fail. 104 At this point we create a new window. 105 */ 106 try { 107 boolean retryingRowAddition = false; 108 for (int row = 0; row < responses.size(); row++) { 109 // Allocate a new row for each entry. 110 if (!window.allocRow()) { 111 // This window is full. Allocate a new CursorWindow. 112 Log.d(MediaCognitionService.TAG, "Allocating additional cursor" 113 + " window for large data set (row " + row + ")"); 114 window = new CursorWindow(RESPONSE_CURSOR_NAME); 115 window.setStartPosition(row); 116 window.setNumColumns(numColumns); 117 windows.add(window); 118 if (!window.allocRow()) { 119 // If we couldn't allocate a row even after creating a new CursorWindow, we 120 // can't proceed. 121 Log.e(MediaCognitionService.TAG, "Unable to allocate row" 122 + " to hold data."); 123 windows.remove(window); 124 return windows.toArray(new CursorWindow[windows.size()]); 125 } 126 } 127 128 // current window is ready to be populated with data 129 boolean dataAdded = true; 130 131 // Adding ID of the media 132 final MediaCognitionProcessingRequest request = responses.get(row).getRequest(); 133 if (request == null || request.getUri() == null 134 || request.getUri().getLastPathSegment() == null) { 135 Log.w(MediaCognitionService.TAG, "Correct request not" 136 + " set in the response"); 137 window.freeLastRow(); 138 retryingRowAddition = false; 139 continue; 140 } 141 dataAdded &= window.putString(request.getUri().getLastPathSegment(), 142 row, 0); 143 dataAdded &= addImageOcrLatin(window, responses.get(row), request, row); 144 dataAdded &= addImageLabels(window, responses.get(row), request, row); 145 146 if (!dataAdded) { 147 // if data addition failed, retry with a new window 148 if (retryingRowAddition) { 149 // if already retrying and failed again, skip this response 150 Log.w(MediaCognitionService.TAG, "Could not add value to cursorWindow" 151 + " The size may be larger than limit "); 152 // prepare for next item 153 window.clear(); 154 window.setStartPosition(row + 1); 155 window.setNumColumns(numColumns); 156 retryingRowAddition = false; 157 continue; 158 } 159 Log.d(MediaCognitionService.TAG, "Couldn't populate window " 160 + "data for row " + row + " - allocating new window."); 161 window.freeLastRow(); 162 window = new CursorWindow(RESPONSE_CURSOR_NAME); 163 window.setStartPosition(row); 164 window.setNumColumns(numColumns); 165 windows.add(window); 166 row--; 167 retryingRowAddition = true; 168 } else { 169 retryingRowAddition = false; 170 } 171 } 172 } catch (RuntimeException re) { 173 // Something went wrong while filling the window. Close all windows, and re-throw. 174 for (int i = 0, count = windows.size(); i < count; i++) { 175 windows.get(i).close(); 176 } 177 throw re; 178 } 179 return windows.toArray(new CursorWindow[windows.size()]); 180 } 181 addImageOcrLatin(CursorWindow window, MediaCognitionProcessingResponse response, MediaCognitionProcessingRequest request, int row)182 private boolean addImageOcrLatin(CursorWindow window, MediaCognitionProcessingResponse response, 183 MediaCognitionProcessingRequest request, int row) { 184 final int column = Integer.numberOfTrailingZeros(ProcessingTypes.IMAGE_OCR_LATIN) + 1; 185 // check if ImageOcrLatin request was set 186 if (request.checkProcessingRequired(ProcessingTypes.IMAGE_OCR_LATIN) 187 && response.getImageOcrLatin() != null) { 188 return window.putString(response.getImageOcrLatin(), row, column); 189 } 190 return window.putNull(row, column); 191 } 192 addImageLabels(CursorWindow window, MediaCognitionProcessingResponse response, MediaCognitionProcessingRequest request, int row)193 private boolean addImageLabels(CursorWindow window, MediaCognitionProcessingResponse response, 194 MediaCognitionProcessingRequest request, int row) { 195 final int column = Integer.numberOfTrailingZeros(ProcessingTypes.IMAGE_LABEL) + 1; 196 if (request.checkProcessingRequired(ProcessingTypes.IMAGE_LABEL) 197 && response.getImageLabels() != null) { 198 return window.putString(String.join("|", response.getImageLabels()), 199 row, column); 200 } 201 return window.putNull(row, column); 202 } 203 } 204 205