1 /*
2  * Copyright (C) 2019 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 package com.android.keyguard.clock;
17 
18 import android.content.ContentProvider;
19 import android.content.ContentValues;
20 import android.database.Cursor;
21 import android.database.MatrixCursor;
22 import android.graphics.Bitmap;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.ParcelFileDescriptor;
26 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
27 import android.text.TextUtils;
28 import android.util.Log;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.systemui.Dependency;
32 
33 import java.io.FileNotFoundException;
34 import java.util.List;
35 import java.util.function.Supplier;
36 
37 /**
38  * Exposes custom clock face options and provides realistic preview images.
39  *
40  * APIs:
41  *
42  *   /list_options: List the available clock faces, which has the following columns
43  *     name: name of the clock face
44  *     title: title of the clock face
45  *     id: value used to set the clock face
46  *     thumbnail: uri of the thumbnail image, should be /thumbnail/{name}
47  *     preview: uri of the preview image, should be /preview/{name}
48  *
49  *   /thumbnail/{id}: Opens a file stream for the thumbnail image for clock face {id}.
50  *
51  *   /preview/{id}: Opens a file stream for the preview image for clock face {id}.
52  */
53 public final class ClockOptionsProvider extends ContentProvider {
54 
55     private static final String TAG = "ClockOptionsProvider";
56     private static final String KEY_LIST_OPTIONS = "/list_options";
57     private static final String KEY_PREVIEW = "preview";
58     private static final String KEY_THUMBNAIL = "thumbnail";
59     private static final String COLUMN_NAME = "name";
60     private static final String COLUMN_TITLE = "title";
61     private static final String COLUMN_ID = "id";
62     private static final String COLUMN_THUMBNAIL = "thumbnail";
63     private static final String COLUMN_PREVIEW = "preview";
64     private static final String MIME_TYPE_PNG = "image/png";
65     private static final String CONTENT_SCHEME = "content";
66     private static final String AUTHORITY = "com.android.keyguard.clock";
67 
68     private final Supplier<List<ClockInfo>> mClocksSupplier;
69 
ClockOptionsProvider()70     public ClockOptionsProvider() {
71         this(() -> Dependency.get(ClockManager.class).getClockInfos());
72     }
73 
74     @VisibleForTesting
ClockOptionsProvider(Supplier<List<ClockInfo>> clocksSupplier)75     ClockOptionsProvider(Supplier<List<ClockInfo>> clocksSupplier) {
76         mClocksSupplier = clocksSupplier;
77     }
78 
79     @Override
onCreate()80     public boolean onCreate() {
81         return true;
82     }
83 
84     @Override
getType(Uri uri)85     public String getType(Uri uri) {
86         List<String> segments = uri.getPathSegments();
87         if (segments.size() > 0 && (KEY_PREVIEW.equals(segments.get(0))
88                 || KEY_THUMBNAIL.equals(segments.get(0)))) {
89             return MIME_TYPE_PNG;
90         }
91         return "vnd.android.cursor.dir/clock_faces";
92     }
93 
94     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)95     public Cursor query(Uri uri, String[] projection, String selection,
96             String[] selectionArgs, String sortOrder) {
97         if (!KEY_LIST_OPTIONS.equals(uri.getPath())) {
98             return null;
99         }
100         MatrixCursor cursor = new MatrixCursor(new String[] {
101                 COLUMN_NAME, COLUMN_TITLE, COLUMN_ID, COLUMN_THUMBNAIL, COLUMN_PREVIEW});
102         List<ClockInfo> clocks = mClocksSupplier.get();
103         for (int i = 0; i < clocks.size(); i++) {
104             ClockInfo clock = clocks.get(i);
105             cursor.newRow()
106                     .add(COLUMN_NAME, clock.getName())
107                     .add(COLUMN_TITLE, clock.getTitle())
108                     .add(COLUMN_ID, clock.getId())
109                     .add(COLUMN_THUMBNAIL, createThumbnailUri(clock))
110                     .add(COLUMN_PREVIEW, createPreviewUri(clock));
111         }
112         return cursor;
113     }
114 
115     @Override
insert(Uri uri, ContentValues initialValues)116     public Uri insert(Uri uri, ContentValues initialValues) {
117         return null;
118     }
119 
120     @Override
delete(Uri uri, String selection, String[] selectionArgs)121     public int delete(Uri uri, String selection, String[] selectionArgs) {
122         return 0;
123     }
124 
125     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)126     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
127         return 0;
128     }
129 
130     @Override
openFile(Uri uri, String mode)131     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
132         List<String> segments = uri.getPathSegments();
133         if (segments.size() != 2 || !(KEY_PREVIEW.equals(segments.get(0))
134                 || KEY_THUMBNAIL.equals(segments.get(0)))) {
135             throw new FileNotFoundException("Invalid preview url");
136         }
137         String id = segments.get(1);
138         if (TextUtils.isEmpty(id)) {
139             throw new FileNotFoundException("Invalid preview url, missing id");
140         }
141         ClockInfo clock = null;
142         List<ClockInfo> clocks = mClocksSupplier.get();
143         for (int i = 0; i < clocks.size(); i++) {
144             if (id.equals(clocks.get(i).getId())) {
145                 clock = clocks.get(i);
146                 break;
147             }
148         }
149         if (clock == null) {
150             throw new FileNotFoundException("Invalid preview url, id not found");
151         }
152         return openPipeHelper(uri, MIME_TYPE_PNG, null, KEY_PREVIEW.equals(segments.get(0))
153                 ? clock.getPreview() : clock.getThumbnail(), new MyWriter());
154     }
155 
createThumbnailUri(ClockInfo clock)156     private Uri createThumbnailUri(ClockInfo clock) {
157         return new Uri.Builder()
158                 .scheme(CONTENT_SCHEME)
159                 .authority(AUTHORITY)
160                 .appendPath(KEY_THUMBNAIL)
161                 .appendPath(clock.getId())
162                 .build();
163     }
164 
createPreviewUri(ClockInfo clock)165     private Uri createPreviewUri(ClockInfo clock) {
166         return new Uri.Builder()
167                 .scheme(CONTENT_SCHEME)
168                 .authority(AUTHORITY)
169                 .appendPath(KEY_PREVIEW)
170                 .appendPath(clock.getId())
171                 .build();
172     }
173 
174     private static class MyWriter implements ContentProvider.PipeDataWriter<Bitmap> {
175         @Override
writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, Bitmap bitmap)176         public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
177                 Bundle opts, Bitmap bitmap) {
178             try (AutoCloseOutputStream os = new AutoCloseOutputStream(output)) {
179                 bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
180             } catch (Exception e) {
181                 Log.w(TAG, "fail to write to pipe", e);
182             }
183         }
184     }
185 }
186