Merge "Add ClockOptionsProvider for more realistic previews."

This commit is contained in:
TreeHugger Robot
2019-02-05 14:31:30 +00:00
committed by Android (Google) Code Review
15 changed files with 637 additions and 0 deletions

View File

@@ -631,6 +631,14 @@
android:exported="true">
</provider>
<!-- Provides list and realistic previews of clock faces for the picker app. -->
<provider
android:name="com.android.keyguard.clock.ClockOptionsProvider"
android:authorities="com.android.keyguard.clock"
android:exported="true"
android:grantUriPermissions="true">
</provider>
<receiver
android:name=".statusbar.KeyboardShortcutsReceiver">
<intent-filter>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -485,4 +485,17 @@ number">%d</xliff:g> remaining attempts before SIM becomes permanently unusable.
<item>Fifty\nNine</item>
</string-array>
<!-- Title for default clock face that will appear in the picker app next to a preview image of
the clock face. [CHAR LIMIT=8] -->
<string name="clock_title_default" translatable="false">Default</string>
<!-- Title for Bubble clock face that will appear in the picker app next to a preview image of
the clock face. [CHAR LIMIT=8] -->
<string name="clock_title_bubble" translatable="false">Bubble</string>
<!-- Title for Stretch clock face that will appear in the picker app next to a preview image of
the clock face. [CHAR LIMIT=8] -->
<string name="clock_title_stretch" translatable="false">Stretch</string>
<!-- Title for Typographic clock face that will appear in the picker app next to a preview image of
the clock face. [CHAR LIMIT=8] -->
<string name="clock_title_type" translatable="false">Type</string>
</resources>

View File

@@ -0,0 +1,117 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keyguard.clock;
import android.graphics.Bitmap;
import java.util.function.Supplier;
/**
* Metadata about an available clock face.
*/
final class ClockInfo {
private final String mName;
private final String mTitle;
private final String mId;
private final Supplier<Bitmap> mThumbnail;
private final Supplier<Bitmap> mPreview;
private ClockInfo(String name, String title, String id,
Supplier<Bitmap> thumbnail, Supplier<Bitmap> preview) {
mName = name;
mTitle = title;
mId = id;
mThumbnail = thumbnail;
mPreview = preview;
}
/**
* Gets the non-internationalized name for the clock face.
*/
String getName() {
return mName;
}
/**
* Gets the name (title) of the clock face to be shown in the picker app.
*/
String getTitle() {
return mTitle;
}
/**
* Gets the ID of the clock face, used by the picker to set the current selection.
*/
String getId() {
return mId;
}
/**
* Gets a thumbnail image of the clock.
*/
Bitmap getThumbnail() {
return mThumbnail.get();
}
/**
* Gets a potentially realistic preview image of the clock face.
*/
Bitmap getPreview() {
return mPreview.get();
}
static Builder builder() {
return new Builder();
}
static class Builder {
private String mName;
private String mTitle;
private String mId;
private Supplier<Bitmap> mThumbnail;
private Supplier<Bitmap> mPreview;
public ClockInfo build() {
return new ClockInfo(mName, mTitle, mId, mThumbnail, mPreview);
}
public Builder setName(String name) {
mName = name;
return this;
}
public Builder setTitle(String title) {
mTitle = title;
return this;
}
public Builder setId(String id) {
mId = id;
return this;
}
public Builder setThumbnail(Supplier<Bitmap> thumbnail) {
mThumbnail = thumbnail;
return this;
}
public Builder setPreview(Supplier<Bitmap> preview) {
mPreview = preview;
return this;
}
}
}

View File

@@ -17,12 +17,15 @@ package com.android.keyguard.clock;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.view.LayoutInflater;
import com.android.keyguard.R;
import com.android.systemui.plugins.ClockPlugin;
import com.android.systemui.statusbar.policy.ExtensionController;
import com.android.systemui.statusbar.policy.ExtensionController.Extension;
@@ -45,6 +48,7 @@ public final class ClockManager {
private final LayoutInflater mLayoutInflater;
private final ContentResolver mContentResolver;
private final List<ClockInfo> mClockInfos = new ArrayList<>();
/**
* Observe settings changes to know when to switch the clock face.
*/
@@ -76,6 +80,36 @@ public final class ClockManager {
mExtensionController = extensionController;
mLayoutInflater = LayoutInflater.from(context);
mContentResolver = context.getContentResolver();
Resources res = context.getResources();
mClockInfos.add(ClockInfo.builder()
.setName("default")
.setTitle(res.getString(R.string.clock_title_default))
.setId("default")
.setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.default_thumbnail))
.setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.default_preview))
.build());
mClockInfos.add(ClockInfo.builder()
.setName("bubble")
.setTitle(res.getString(R.string.clock_title_bubble))
.setId(BubbleClockController.class.getName())
.setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.bubble_thumbnail))
.setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.bubble_preview))
.build());
mClockInfos.add(ClockInfo.builder()
.setName("stretch")
.setTitle(res.getString(R.string.clock_title_stretch))
.setId(StretchAnalogClockController.class.getName())
.setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.stretch_thumbnail))
.setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.stretch_preview))
.build());
mClockInfos.add(ClockInfo.builder()
.setName("type")
.setTitle(res.getString(R.string.clock_title_type))
.setId(TypeClockController.class.getName())
.setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.type_thumbnail))
.setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.type_preview))
.build());
}
/**
@@ -101,6 +135,13 @@ public final class ClockManager {
}
}
/**
* Get information about available clock faces.
*/
List<ClockInfo> getClockInfos() {
return mClockInfos;
}
private void setClockPlugin(ClockPlugin plugin) {
for (int i = 0; i < mListeners.size(); i++) {
// It probably doesn't make sense to supply the same plugin instances to multiple

View File

@@ -0,0 +1,185 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keyguard.clock;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dependency;
import java.io.FileNotFoundException;
import java.util.List;
import java.util.function.Supplier;
/**
* Exposes custom clock face options and provides realistic preview images.
*
* APIs:
*
* /list_options: List the available clock faces, which has the following columns
* name: name of the clock face
* title: title of the clock face
* id: value used to set the clock face
* thumbnail: uri of the thumbnail image, should be /thumbnail/{name}
* preview: uri of the preview image, should be /preview/{name}
*
* /thumbnail/{id}: Opens a file stream for the thumbnail image for clock face {id}.
*
* /preview/{id}: Opens a file stream for the preview image for clock face {id}.
*/
public final class ClockOptionsProvider extends ContentProvider {
private static final String TAG = "ClockOptionsProvider";
private static final String KEY_LIST_OPTIONS = "/list_options";
private static final String KEY_PREVIEW = "preview";
private static final String KEY_THUMBNAIL = "thumbnail";
private static final String COLUMN_NAME = "name";
private static final String COLUMN_TITLE = "title";
private static final String COLUMN_ID = "id";
private static final String COLUMN_THUMBNAIL = "thumbnail";
private static final String COLUMN_PREVIEW = "preview";
private static final String MIME_TYPE_PNG = "image/png";
private static final String CONTENT_SCHEME = "content";
private static final String AUTHORITY = "com.android.keyguard.clock";
private final Supplier<List<ClockInfo>> mClocksSupplier;
public ClockOptionsProvider() {
this(() -> Dependency.get(ClockManager.class).getClockInfos());
}
@VisibleForTesting
ClockOptionsProvider(Supplier<List<ClockInfo>> clocksSupplier) {
mClocksSupplier = clocksSupplier;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
List<String> segments = uri.getPathSegments();
if (segments.size() > 0 && (KEY_PREVIEW.equals(segments.get(0))
|| KEY_THUMBNAIL.equals(segments.get(0)))) {
return MIME_TYPE_PNG;
}
return "vnd.android.cursor.dir/clock_faces";
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (!KEY_LIST_OPTIONS.equals(uri.getPath())) {
return null;
}
MatrixCursor cursor = new MatrixCursor(new String[] {
COLUMN_NAME, COLUMN_TITLE, COLUMN_ID, COLUMN_THUMBNAIL, COLUMN_PREVIEW});
List<ClockInfo> clocks = mClocksSupplier.get();
for (int i = 0; i < clocks.size(); i++) {
ClockInfo clock = clocks.get(i);
cursor.newRow()
.add(COLUMN_NAME, clock.getName())
.add(COLUMN_TITLE, clock.getTitle())
.add(COLUMN_ID, clock.getId())
.add(COLUMN_THUMBNAIL, createThumbnailUri(clock))
.add(COLUMN_PREVIEW, createPreviewUri(clock));
}
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
if (segments.size() != 2 || !(KEY_PREVIEW.equals(segments.get(0))
|| KEY_THUMBNAIL.equals(segments.get(0)))) {
throw new FileNotFoundException("Invalid preview url");
}
String id = segments.get(1);
if (TextUtils.isEmpty(id)) {
throw new FileNotFoundException("Invalid preview url, missing id");
}
ClockInfo clock = null;
List<ClockInfo> clocks = mClocksSupplier.get();
for (int i = 0; i < clocks.size(); i++) {
if (id.equals(clocks.get(i).getId())) {
clock = clocks.get(i);
break;
}
}
if (clock == null) {
throw new FileNotFoundException("Invalid preview url, id not found");
}
return openPipeHelper(uri, MIME_TYPE_PNG, null, KEY_PREVIEW.equals(segments.get(0))
? clock.getPreview() : clock.getThumbnail(), new MyWriter());
}
private Uri createThumbnailUri(ClockInfo clock) {
return new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(KEY_THUMBNAIL)
.appendPath(clock.getId())
.build();
}
private Uri createPreviewUri(ClockInfo clock) {
return new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(KEY_PREVIEW)
.appendPath(clock.getId())
.build();
}
private static class MyWriter implements ContentProvider.PipeDataWriter<Bitmap> {
@Override
public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
Bundle opts, Bitmap bitmap) {
try (AutoCloseOutputStream os = new AutoCloseOutputStream(output)) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
} catch (Exception e) {
Log.w(TAG, "fail to write to pipe", e);
}
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keyguard.clock;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
import android.graphics.Bitmap;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.function.Supplier;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
public final class ClockInfoTest extends SysuiTestCase {
@Mock
private Supplier<Bitmap> mMockSupplier;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testGetName() {
final String name = "name";
ClockInfo info = ClockInfo.builder().setName(name).build();
assertThat(info.getName()).isEqualTo(name);
}
@Test
public void testGetTitle() {
final String title = "title";
ClockInfo info = ClockInfo.builder().setTitle(title).build();
assertThat(info.getTitle()).isEqualTo(title);
}
@Test
public void testGetId() {
final String id = "id";
ClockInfo info = ClockInfo.builder().setId(id).build();
assertThat(info.getId()).isEqualTo(id);
}
@Test
public void testGetThumbnail() {
ClockInfo info = ClockInfo.builder().setThumbnail(mMockSupplier).build();
info.getThumbnail();
verify(mMockSupplier).get();
}
@Test
public void testGetPreview() {
ClockInfo info = ClockInfo.builder().setPreview(mMockSupplier).build();
info.getPreview();
verify(mMockSupplier).get();
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keyguard.clock;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
public final class ClockOptionsProviderTest extends SysuiTestCase {
private static final String CONTENT_SCHEME = "content";
private static final String AUTHORITY = "com.android.keyguard.clock";
private static final String LIST_OPTIONS = "list_options";
private static final String PREVIEW = "preview";
private static final String THUMBNAIL = "thumbnail";
private static final String MIME_TYPE_LIST_OPTIONS = "vnd.android.cursor.dir/clock_faces";
private static final String MIME_TYPE_PNG = "image/png";
private static final String NAME_COLUMN = "name";
private static final String TITLE_COLUMN = "title";
private static final String ID_COLUMN = "id";
private static final String PREVIEW_COLUMN = "preview";
private static final String THUMBNAIL_COLUMN = "thumbnail";
private ClockOptionsProvider mProvider;
private Supplier<List<ClockInfo>> mMockSupplier;
private List<ClockInfo> mClocks;
private Uri mListOptionsUri;
@Mock
private Supplier<Bitmap> mMockBitmapSupplier;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mClocks = new ArrayList<>();
mProvider = new ClockOptionsProvider(() -> mClocks);
mListOptionsUri = new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(LIST_OPTIONS)
.build();
}
@Test
public void testGetType_listOptions() {
Uri uri = new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(LIST_OPTIONS)
.build();
assertThat(mProvider.getType(uri)).isEqualTo(MIME_TYPE_LIST_OPTIONS);
}
@Test
public void testGetType_preview() {
Uri uri = new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(PREVIEW)
.appendPath("id")
.build();
assertThat(mProvider.getType(uri)).isEqualTo(MIME_TYPE_PNG);
}
@Test
public void testGetType_thumbnail() {
Uri uri = new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(THUMBNAIL)
.appendPath("id")
.build();
assertThat(mProvider.getType(uri)).isEqualTo(MIME_TYPE_PNG);
}
@Test
public void testQuery_noClocks() {
Cursor cursor = mProvider.query(mListOptionsUri, null, null, null);
assertThat(cursor.getCount()).isEqualTo(0);
}
@Test
public void testQuery_listOptions() {
mClocks.add(ClockInfo.builder()
.setName("name_a")
.setTitle("title_a")
.setId("id_a")
.build());
mClocks.add(ClockInfo.builder()
.setName("name_b")
.setTitle("title_b")
.setId("id_b")
.build());
Cursor cursor = mProvider.query(mListOptionsUri, null, null, null);
assertThat(cursor.getCount()).isEqualTo(2);
cursor.moveToFirst();
assertThat(cursor.getString(
cursor.getColumnIndex(NAME_COLUMN))).isEqualTo("name_a");
assertThat(cursor.getString(
cursor.getColumnIndex(TITLE_COLUMN))).isEqualTo("title_a");
assertThat(cursor.getString(
cursor.getColumnIndex(ID_COLUMN))).isEqualTo("id_a");
assertThat(cursor.getString(
cursor.getColumnIndex(PREVIEW_COLUMN)))
.isEqualTo("content://com.android.keyguard.clock/preview/id_a");
assertThat(cursor.getString(
cursor.getColumnIndex(THUMBNAIL_COLUMN)))
.isEqualTo("content://com.android.keyguard.clock/thumbnail/id_a");
cursor.moveToNext();
assertThat(cursor.getString(
cursor.getColumnIndex(NAME_COLUMN))).isEqualTo("name_b");
assertThat(cursor.getString(
cursor.getColumnIndex(TITLE_COLUMN))).isEqualTo("title_b");
assertThat(cursor.getString(
cursor.getColumnIndex(ID_COLUMN))).isEqualTo("id_b");
assertThat(cursor.getString(
cursor.getColumnIndex(PREVIEW_COLUMN)))
.isEqualTo("content://com.android.keyguard.clock/preview/id_b");
assertThat(cursor.getString(
cursor.getColumnIndex(THUMBNAIL_COLUMN)))
.isEqualTo("content://com.android.keyguard.clock/thumbnail/id_b");
}
@Test
public void testOpenFile_preview() throws Exception {
mClocks.add(ClockInfo.builder()
.setId("id")
.setPreview(mMockBitmapSupplier)
.build());
Uri uri = new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(PREVIEW)
.appendPath("id")
.build();
mProvider.openFile(uri, "r").close();
verify(mMockBitmapSupplier).get();
}
@Test
public void testOpenFile_thumbnail() throws Exception {
mClocks.add(ClockInfo.builder()
.setId("id")
.setThumbnail(mMockBitmapSupplier)
.build());
Uri uri = new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(AUTHORITY)
.appendPath(THUMBNAIL)
.appendPath("id")
.build();
mProvider.openFile(uri, "r").close();
verify(mMockBitmapSupplier).get();
}
}