1 /* 2 * Copyright (C) 2022 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; 18 19 import static android.os.Process.THREAD_PRIORITY_FOREGROUND; 20 21 import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAllAvailableCloudProviders; 22 import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAvailableCloudProviders; 23 24 import android.content.Context; 25 import android.os.Handler; 26 import android.os.HandlerThread; 27 import android.os.ParcelFileDescriptor; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import com.android.modules.utils.BasicShellCommandHandler; 33 import com.android.modules.utils.HandlerExecutor; 34 import com.android.providers.media.photopicker.PickerSyncController; 35 import com.android.providers.media.photopicker.data.CloudProviderInfo; 36 import com.android.providers.media.photopicker.data.PickerDatabaseHelper; 37 38 import java.io.OutputStream; 39 import java.io.PrintWriter; 40 import java.util.List; 41 import java.util.concurrent.Executor; 42 43 class MediaProviderShellCommand extends BasicShellCommandHandler { 44 private final @NonNull Context mAppContext; 45 private final @NonNull ConfigStore mConfigStore; 46 private final @NonNull PickerSyncController mPickerSyncController; 47 private final @NonNull OutputStream mOut; 48 MediaProviderShellCommand( @onNull Context context, @NonNull ConfigStore configStore, @NonNull PickerSyncController pickerSyncController, @NonNull ParcelFileDescriptor out)49 MediaProviderShellCommand( 50 @NonNull Context context, 51 @NonNull ConfigStore configStore, 52 @NonNull PickerSyncController pickerSyncController, 53 @NonNull ParcelFileDescriptor out) { 54 mAppContext = context.getApplicationContext(); 55 mPickerSyncController = pickerSyncController; 56 mConfigStore = configStore; 57 mOut = new ParcelFileDescriptor.AutoCloseOutputStream(out); 58 } 59 60 @Override onCommand(String cmd)61 public int onCommand(String cmd) { 62 try (PrintWriter pw = getOutPrintWriter()) { 63 if (cmd == null || cmd.isBlank()) { 64 cmd = "help"; 65 } 66 switch (cmd) { 67 case "version": 68 return runVersion(pw); 69 case "cloud-provider": 70 return runCloudProvider(pw); 71 default: 72 return handleDefaultCommands(cmd); 73 } 74 } 75 } 76 runVersion(@onNull PrintWriter pw)77 private int runVersion(@NonNull PrintWriter pw) { 78 pw.print('\'' + DatabaseHelper.INTERNAL_DATABASE_NAME + "' version: "); 79 pw.println(DatabaseHelper.VERSION_LATEST); 80 81 pw.print('\'' + DatabaseHelper.EXTERNAL_DATABASE_NAME + "' version: "); 82 pw.println(DatabaseHelper.VERSION_LATEST); 83 84 pw.print('\'' + PickerDatabaseHelper.PICKER_DATABASE_NAME + "' version: "); 85 pw.println(PickerDatabaseHelper.VERSION_LATEST); 86 87 return 0; 88 } 89 runCloudProvider(@onNull PrintWriter pw)90 private int runCloudProvider(@NonNull PrintWriter pw) { 91 final String subcommand = getNextArgRequired(); 92 switch (subcommand) { 93 case "list": 94 return runCloudProviderList(pw); 95 case "info": 96 return runCloudProviderInfo(pw); 97 case "set": 98 return runCloudProviderSet(pw); 99 case "unset": 100 return runCloudProviderUnset(pw); 101 case "sync-library": 102 return runCloudProviderSyncLibrary(pw); 103 case "reset-library": 104 return runCloudProviderResetLibrary(pw); 105 default: 106 pw.println("Error: unknown cloud-provider command '" + subcommand + "'"); 107 return 1; 108 } 109 } 110 runCloudProviderList(@onNull PrintWriter pw)111 private int runCloudProviderList(@NonNull PrintWriter pw) { 112 final String option = getNextOption(); 113 if ("--allowlist".equals(option)) { 114 final List<String> allowlist = mConfigStore.getAllowedCloudProviderPackages(); 115 if (allowlist.isEmpty()) { 116 pw.println("Allowlist is empty."); 117 } else { 118 for (var providerAuthority : allowlist) { 119 pw.println(providerAuthority); 120 } 121 } 122 } else { 123 final List<CloudProviderInfo> cloudProviders; 124 125 if ("--all".equals(option)) { 126 cloudProviders = getAllAvailableCloudProviders(mAppContext, mConfigStore); 127 } else if (option == null) { 128 cloudProviders = getAvailableCloudProviders(mAppContext, mConfigStore); 129 } else { 130 pw.println("Error: unknown cloud-provider list option '" + option + "'"); 131 return 1; 132 } 133 134 if (cloudProviders.isEmpty()) { 135 pw.println("No available CloudMediaProviders."); 136 } else { 137 for (var providerInfo : cloudProviders) { 138 pw.println(providerInfo.toShortString()); 139 } 140 } 141 } 142 return 0; 143 } 144 runCloudProviderInfo(@onNull PrintWriter pw)145 private int runCloudProviderInfo(@NonNull PrintWriter pw) { 146 pw.println("Current CloudMediaProvider:"); 147 pw.println(mPickerSyncController.getCurrentCloudProviderInfo().toShortString()); 148 return 0; 149 } 150 runCloudProviderSet(@onNull PrintWriter pw)151 private int runCloudProviderSet(@NonNull PrintWriter pw) { 152 final String authority = getNextArg(); 153 if (authority == null) { 154 pw.println("Error: authority not provided"); 155 pw.println("(usage: `media_provider cloud-provider set <authority>`)"); 156 return 1; 157 } 158 159 pw.println("Setting current CloudMediaProvider authority to '" + authority + "'..."); 160 final boolean success = mPickerSyncController.forceSetCloudProvider(authority); 161 162 pw.println(success ? "Succeed." : "Failed."); 163 return success ? 0 : 1; 164 } 165 runCloudProviderUnset(@onNull PrintWriter pw)166 private int runCloudProviderUnset(@NonNull PrintWriter pw) { 167 pw.println("Unsetting current CloudMediaProvider (disabling CMP integration)..."); 168 final boolean success = mPickerSyncController.forceSetCloudProvider(null); 169 170 pw.println(success ? "Succeed." : "Failed."); 171 return success ? 0 : 1; 172 } 173 runCloudProviderSyncLibrary(@onNull PrintWriter pw)174 private int runCloudProviderSyncLibrary(@NonNull PrintWriter pw) { 175 pw.println("Syncing PhotoPicker's library (CMP and local)..."); 176 177 // TODO(b/242550131): add PickerSyncController's API to make it possible to sync from only 178 // one provider at a time (i.e. either CMP or local) 179 mPickerSyncController.syncAllMedia(); 180 181 pw.println("Done."); 182 return 0; 183 } 184 runCloudProviderResetLibrary(@onNull PrintWriter pw)185 private int runCloudProviderResetLibrary(@NonNull PrintWriter pw) { 186 pw.println("Resetting PhotoPicker's library (CMP and local)..."); 187 188 // TODO(b/242550131): add PickerSyncController's API to make it possible to reset just one 189 // provider's library at a time (i.e. either CMP or local). 190 mPickerSyncController.resetAllMedia(); 191 192 pw.println("Done."); 193 return 0; 194 } 195 196 @Override onHelp()197 public void onHelp() { 198 final PrintWriter pw = getOutPrintWriter(); 199 pw.println("MediaProvider (media_provider) commands:"); 200 pw.println(" help"); 201 pw.println(" Print this help text."); 202 pw.println(); 203 pw.println(" version"); 204 pw.println(" Print databases (internal/external/picker) versions."); 205 pw.println(); 206 pw.println(" cloud-provider [list | info | set | unset] [...]"); 207 pw.println(" Configure and audit CloudMediaProvider-s (CMPs)."); 208 pw.println(); 209 pw.println(" list [--all | --allowlist]"); 210 pw.println(" List installed and allowlisted CMPs."); 211 pw.println(" --all: ignore allowlist, list all installed CMPs."); 212 pw.println(" --allowlisted: print allowlist of CMP authorities."); 213 pw.println(); 214 pw.println(" info"); 215 pw.println(" Print current CloudMediaProvider."); 216 pw.println(); 217 pw.println(" set <AUTHORITY>"); 218 pw.println(" Set current CloudMediaProvider."); 219 pw.println(); 220 pw.println(" unset"); 221 pw.println(" Unset CloudMediaProvider (disables CMP integration)."); 222 pw.println(); 223 pw.println(" sync-library"); 224 pw.println(" Sync media from the current CloudMediaProvider and local provider."); 225 pw.println(); 226 pw.println(" reset-library"); 227 pw.println(" Reset media previously synced from the CloudMediaProvider and"); 228 pw.println(" the local provider."); 229 pw.println(); 230 } 231 exec(@ullable String[] args)232 public void exec(@Nullable String[] args) { 233 getExecutor().execute(() -> exec( 234 /* Binder target */ null, 235 /* FileDescriptor in */ null, 236 /* FileDescriptor out */ null, 237 /* FileDescriptor err */ null, 238 args)); 239 } 240 241 242 @Override getRawOutputStream()243 public OutputStream getRawOutputStream() { 244 return mOut; 245 } 246 247 @Override getRawErrorStream()248 public OutputStream getRawErrorStream() { 249 return mOut; 250 } 251 252 @Nullable 253 private static Executor sExecutor; 254 255 @NonNull getExecutor()256 private static synchronized Executor getExecutor() { 257 if (sExecutor == null) { 258 final HandlerThread thread = new HandlerThread("cli", THREAD_PRIORITY_FOREGROUND); 259 thread.start(); 260 final Handler handler = new Handler(thread.getLooper()); 261 sExecutor = new HandlerExecutor(handler); 262 } 263 return sExecutor; 264 } 265 } 266