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