1package net.paperclover.snowbound.providerlab;
2
3import android.database.Cursor;
4import android.database.MatrixCursor;
5import android.os.CancellationSignal;
6import android.os.ParcelFileDescriptor;
7import android.provider.DocumentsContract.Document;
8import android.provider.DocumentsContract.Root;
9import android.provider.DocumentsProvider;
10import java.io.ByteArrayOutputStream;
11import java.io.File;
12import java.io.FileNotFoundException;
13import java.io.IOException;
14import java.nio.charset.StandardCharsets;
15import java.nio.file.Files;
16import java.util.Arrays;
17import java.util.concurrent.CountDownLatch;
18import java.util.concurrent.TimeUnit;
19
20public final class LabProvider extends DocumentsProvider {
21 static final String AUTHORITY = "net.paperclover.snowbound.providerlab.documents";
22 static final String ROOT = "root:opaque/α";
23 static final String LOCAL = "note:opaque/β";
24 static final String RENAMED = "identity:new/γ";
25 static final String PIPE = "stream:opaque/δ";
26 static final String FAIL = "failed:opaque/ε";
27 static final String WRITER = "view:writer/ζ";
28 static final String OBSERVER = "view:observer/η";
29 /** A second root: a real notebook folder, its documents named by path below it. */
30 static final String NOTEBOOK = "notebook:";
31 static final String[] COLUMNS = {Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME,
32 Document.COLUMN_MIME_TYPE, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, Document.COLUMN_LAST_MODIFIED};
33 static volatile boolean offline;
34 static volatile byte[] pipeBytes;
35 static volatile CountDownLatch pipeDone = new CountDownLatch(0);
36 static volatile String pipeError;
37 static File directory;
38
39 @Override public boolean onCreate() {
40 directory = new File(getContext().getFilesDir(), "synthetic-documents");
41 directory.mkdirs();
42 return true;
43 }
44
45 static File file(String name) { return new File(directory, name); }
46
47 /** The notebook folder, which `adb shell run-as` fills with a disposable notebook. */
48 static File notebook(String id) {
49 return new File(new File(directory.getParentFile(), "notebook"), id.substring(NOTEBOOK.length()));
50 }
51
52 private static void notebookRow(MatrixCursor cursor, String id) {
53 File file = notebook(id);
54 MatrixCursor.RowBuilder row = cursor.newRow();
55 for (String column : cursor.getColumnNames()) {
56 Object value = null;
57 if (column.equals(Document.COLUMN_DOCUMENT_ID)) value = id;
58 if (column.equals(Document.COLUMN_DISPLAY_NAME)) value = id.equals(NOTEBOOK) ? "Lab notebook" : file.getName();
59 if (column.equals(Document.COLUMN_MIME_TYPE)) value = file.isDirectory() ? Document.MIME_TYPE_DIR : "application/octet-stream";
60 if (column.equals(Document.COLUMN_FLAGS)) value = file.isDirectory() ? 0 : Document.FLAG_SUPPORTS_WRITE;
61 if (column.equals(Document.COLUMN_SIZE)) value = file.isDirectory() ? null : file.length();
62 if (column.equals(Document.COLUMN_LAST_MODIFIED)) value = file.lastModified();
63 row.add(value);
64 }
65 }
66
67 static void reset() throws IOException {
68 offline = false;
69 Files.deleteIfExists(file("renamed").toPath());
70 pipeBytes = "STREAM".getBytes(StandardCharsets.UTF_8);
71 pipeError = null;
72 pipeDone = new CountDownLatch(0);
73 Files.write(file("local").toPath(), "ORIGINAL".getBytes(StandardCharsets.UTF_8));
74 Files.write(file("writer").toPath(), "OLD-VIEW".getBytes(StandardCharsets.UTF_8));
75 Files.write(file("observer").toPath(), "OLD-VIEW".getBytes(StandardCharsets.UTF_8));
76 }
77
78 static boolean renamed() { return file("renamed").exists(); }
79
80 static void check(String id) throws FileNotFoundException {
81 if (id.startsWith(NOTEBOOK)) {
82 if (offline) throw new FileNotFoundException("Injected provider unavailable");
83 if (!notebook(id).exists()) throw new FileNotFoundException("No such notebook document");
84 return;
85 }
86 if (!(id.equals(ROOT) || id.equals(renamed() ? RENAMED : LOCAL) || id.equals(PIPE)
87 || id.equals(FAIL) || id.equals(WRITER) || id.equals(OBSERVER))) {
88 throw new FileNotFoundException("Unknown opaque identity");
89 }
90 }
91
92 private static void row(MatrixCursor cursor, String id) {
93 MatrixCursor.RowBuilder row = cursor.newRow();
94 for (String column : cursor.getColumnNames()) {
95 Object value = null;
96 if (column.equals(Document.COLUMN_DOCUMENT_ID)) value = id;
97 if (column.equals(Document.COLUMN_DISPLAY_NAME)) value = id.equals(ROOT) ? "Disposable lab" : id.equals(RENAMED) ? "renamed.one" : "same-name.one";
98 if (column.equals(Document.COLUMN_MIME_TYPE)) value = id.equals(ROOT) ? Document.MIME_TYPE_DIR : "application/octet-stream";
99 if (column.equals(Document.COLUMN_FLAGS)) value = id.equals(ROOT) ? 0 : Document.FLAG_SUPPORTS_WRITE | (id.equals(LOCAL) ? Document.FLAG_SUPPORTS_RENAME : 0);
100 row.add(value);
101 }
102 }
103
104 @Override public Cursor queryRoots(String[] projection) {
105 MatrixCursor cursor = new MatrixCursor(projection != null ? projection : new String[] {
106 Root.COLUMN_ROOT_ID, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_TITLE, Root.COLUMN_FLAGS});
107 MatrixCursor.RowBuilder row = cursor.newRow();
108 for (String column : cursor.getColumnNames()) {
109 Object value = null;
110 if (column.equals(Root.COLUMN_ROOT_ID)) value = "synthetic";
111 if (column.equals(Root.COLUMN_DOCUMENT_ID)) value = ROOT;
112 if (column.equals(Root.COLUMN_TITLE)) value = "Disposable provider lab";
113 if (column.equals(Root.COLUMN_FLAGS)) value = Root.FLAG_SUPPORTS_IS_CHILD;
114 row.add(value);
115 }
116 row = cursor.newRow();
117 for (String column : cursor.getColumnNames()) {
118 Object value = null;
119 if (column.equals(Root.COLUMN_ROOT_ID)) value = "notebook";
120 if (column.equals(Root.COLUMN_DOCUMENT_ID)) value = NOTEBOOK;
121 if (column.equals(Root.COLUMN_TITLE)) value = "Disposable lab notebook";
122 if (column.equals(Root.COLUMN_FLAGS)) value = Root.FLAG_SUPPORTS_IS_CHILD;
123 row.add(value);
124 }
125 return cursor;
126 }
127
128 @Override public Cursor queryDocument(String id, String[] projection) throws FileNotFoundException {
129 check(id);
130 MatrixCursor cursor = new MatrixCursor(projection != null ? projection : COLUMNS);
131 if (id.startsWith(NOTEBOOK)) notebookRow(cursor, id);
132 else row(cursor, id);
133 return cursor;
134 }
135
136 @Override public Cursor queryChildDocuments(String parent, String[] projection, String order) throws FileNotFoundException {
137 if (parent.startsWith(NOTEBOOK)) {
138 check(parent);
139 File[] files = notebook(parent).listFiles();
140 if (files == null) throw new FileNotFoundException("Not a directory");
141 MatrixCursor cursor = new MatrixCursor(projection != null ? projection : COLUMNS);
142 String prefix = parent.equals(NOTEBOOK) ? NOTEBOOK : parent + "/";
143 for (File file : files) notebookRow(cursor, prefix + file.getName());
144 return cursor;
145 }
146 if (!parent.equals(ROOT)) throw new FileNotFoundException("Not a directory");
147 MatrixCursor cursor = new MatrixCursor(projection != null ? projection : COLUMNS);
148 for (String id : Arrays.asList(renamed() ? RENAMED : LOCAL, PIPE, FAIL, WRITER, OBSERVER)) row(cursor, id);
149 return cursor;
150 }
151
152 @Override public boolean isChildDocument(String parent, String child) {
153 if (parent.startsWith(NOTEBOOK)) return child.startsWith(parent) && !child.equals(parent);
154 try { check(child); } catch (FileNotFoundException e) { return false; }
155 return parent.equals(ROOT) && !child.equals(ROOT);
156 }
157
158 @Override public String renameDocument(String id, String name) throws FileNotFoundException {
159 check(id);
160 if (!id.equals(LOCAL)) throw new UnsupportedOperationException("Only local fixture renames");
161 if (!name.equals("renamed.one")) throw new UnsupportedOperationException("Fixture rename name");
162 if (!file("local").renameTo(file("renamed"))) throw new FileNotFoundException("Rename failed");
163 return RENAMED;
164 }
165
166 @Override public ParcelFileDescriptor openDocument(String id, String mode, CancellationSignal signal) throws FileNotFoundException {
167 check(id);
168 if (offline) throw new FileNotFoundException("Injected provider unavailable");
169 if (signal != null) signal.throwIfCanceled();
170 try {
171 if (id.equals(PIPE) || id.equals(FAIL)) {
172 if (!(mode.equals("r") || mode.equals("w"))) throw new UnsupportedOperationException("Streaming fixture rejects seeking modes");
173 ParcelFileDescriptor[] ends = ParcelFileDescriptor.createReliablePipe();
174 boolean reading = mode.equals("r");
175 pipeDone = new CountDownLatch(1);
176 pipeError = null;
177 new Thread(() -> {
178 try {
179 if (id.equals(FAIL)) {
180 ends[reading ? 1 : 0].closeWithError("Injected reliable peer failure");
181 } else if (reading) {
182 try (ParcelFileDescriptor.AutoCloseOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(ends[1])) {
183 out.write(pipeBytes);
184 }
185 } else {
186 try (ParcelFileDescriptor.AutoCloseInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(ends[0])) {
187 ByteArrayOutputStream bytes = new ByteArrayOutputStream();
188 byte[] buffer = new byte[1024];
189 int length;
190 while ((length = in.read(buffer)) != -1) bytes.write(buffer, 0, length);
191 pipeBytes = bytes.toByteArray();
192 }
193 }
194 } catch (IOException e) {
195 pipeError = e.toString();
196 } finally {
197 pipeDone.countDown();
198 }
199 }, "provider-lab-pipe").start();
200 return ends[reading ? 0 : 1];
201 }
202 if (id.startsWith(NOTEBOOK)) return ParcelFileDescriptor.open(notebook(id), ParcelFileDescriptor.parseMode(mode));
203 if (id.equals(ROOT)) throw new FileNotFoundException("Directory has no byte stream");
204 File source = file(id.equals(WRITER) ? "writer" : id.equals(OBSERVER) ? "observer" : id.equals(RENAMED) ? "renamed" : "local");
205 return ParcelFileDescriptor.open(source, ParcelFileDescriptor.parseMode(mode));
206 } catch (IOException e) {
207 throw new FileNotFoundException(e.toString());
208 }
209 }
210
211 static void awaitPipe() throws Exception {
212 if (!pipeDone.await(5, TimeUnit.SECONDS)) throw new IOException("Pipe worker did not finish");
213 if (pipeError != null) throw new IOException(pipeError);
214 }
215}