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