Home | History | Annotate | Download | only in clock
      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 
     70     public ClockOptionsProvider() {
     71         this(() -> Dependency.get(ClockManager.class).getClockInfos());
     72     }
     73 
     74     @VisibleForTesting
     75     ClockOptionsProvider(Supplier<List<ClockInfo>> clocksSupplier) {
     76         mClocksSupplier = clocksSupplier;
     77     }
     78 
     79     @Override
     80     public boolean onCreate() {
     81         return true;
     82     }
     83 
     84     @Override
     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
     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
    116     public Uri insert(Uri uri, ContentValues initialValues) {
    117         return null;
    118     }
    119 
    120     @Override
    121     public int delete(Uri uri, String selection, String[] selectionArgs) {
    122         return 0;
    123     }
    124 
    125     @Override
    126     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    127         return 0;
    128     }
    129 
    130     @Override
    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 
    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 
    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
    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