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