Skip to content

Commit

Permalink
Add an app widget with next artwork action
Browse files Browse the repository at this point in the history
Allow quick access to a next artwork action through an expandable app widget that shows a non-blurred version of your background.
  • Loading branch information
ianhanniballake committed Feb 24, 2017
1 parent b251ec6 commit 057062f
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 0 deletions.
21 changes: 21 additions & 0 deletions main/src/main/AndroidManifest.xml
Expand Up @@ -96,6 +96,27 @@
</intent-filter>
</service>

<!-- AppWidget -->

<receiver android:name="com.google.android.apps.muzei.widget.MuzeiAppWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info"/>
</receiver>
<!-- Receive update broadcasts. Disabled by default until you add at least one widget instance -->
<receiver
android:name="com.google.android.apps.muzei.widget.AppWidgetUpdateReceiver"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.apps.muzei.ACTION_ARTWORK_CHANGED"/>
<action android:name="com.google.android.apps.muzei.ACTION_SOURCE_CHANGED"/>
</intent-filter>
</receiver>

<!-- Target for "Set As" intent -->

<activity android:name="com.google.android.apps.muzei.PhotoSetAsTargetActivity"
Expand Down
@@ -0,0 +1,46 @@
/*
* Copyright 2014 Google Inc.
*
* 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.google.android.apps.muzei.widget;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;

import com.google.android.apps.muzei.api.MuzeiContract;

/**
* Receive update broadcasts from Muzei and refreshes the widget.
*/
public class AppWidgetUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
String action = intent.getAction();
if (TextUtils.equals(action, MuzeiContract.Artwork.ACTION_ARTWORK_CHANGED) ||
TextUtils.equals(action, MuzeiContract.Sources.ACTION_SOURCE_CHANGED)) {
final PendingResult result = goAsync();
new AppWidgetUpdateTask(context) {
@Override
protected void onPostExecute(Boolean success) {
result.finish();
}
}.execute();
}
}
}
}
@@ -0,0 +1,146 @@
/*
* Copyright 2014 Google Inc.
*
* 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.google.android.apps.muzei.widget;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.BaseColumns;
import android.support.annotation.LayoutRes;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.widget.RemoteViews;

import com.google.android.apps.muzei.api.MuzeiContract;
import com.google.android.apps.muzei.render.ImageUtil;

import net.nurik.roman.muzei.R;

import java.io.FileNotFoundException;

/**
* Async operation used to update the AppWidget.
*/
class AppWidgetUpdateTask extends AsyncTask<Void,Void,Boolean> {
private static final String TAG = "AppWidgetUpdateTask";

private final Context mContext;

AppWidgetUpdateTask(Context context) {
mContext = context;
}

@Override
protected Boolean doInBackground(Void... params) {
ComponentName widget = new ComponentName(mContext, MuzeiAppWidgetProvider.class);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(widget);
if (appWidgetIds.length == 0) {
// No app widgets, nothing to do
Log.i(TAG, "No AppWidgets found");
return true;
}
String[] projection = new String[] {BaseColumns._ID,
MuzeiContract.Artwork.COLUMN_NAME_TITLE,
MuzeiContract.Artwork.COLUMN_NAME_BYLINE,
MuzeiContract.Sources.COLUMN_NAME_SUPPORTS_NEXT_ARTWORK_COMMAND};
ContentResolver contentResolver = mContext.getContentResolver();
Cursor artwork = contentResolver.query(MuzeiContract.Artwork.CONTENT_URI, projection, null, null, null);
if (artwork == null || !artwork.moveToFirst()) {
Log.w(TAG, "No current artwork found");
if (artwork != null) {
artwork.close();
}
return false;
}
String title = artwork.getString(artwork.getColumnIndex(MuzeiContract.Artwork.COLUMN_NAME_TITLE));
String byline = artwork.getString(artwork.getColumnIndex(MuzeiContract.Artwork.COLUMN_NAME_BYLINE));
String contentDescription = !TextUtils.isEmpty(title)
? title
: byline;
Uri imageUri = ContentUris.withAppendedId(MuzeiContract.Artwork.CONTENT_URI,
artwork.getLong(artwork.getColumnIndex(BaseColumns._ID)));
boolean supportsNextArtwork = artwork.getInt(
artwork.getColumnIndex(MuzeiContract.Sources.COLUMN_NAME_SUPPORTS_NEXT_ARTWORK_COMMAND)) != 0;
artwork.close();

// Update the widget(s) with the new artwork information
PackageManager packageManager = mContext.getPackageManager();
Intent launchIntent = packageManager.getLaunchIntentForPackage(mContext.getPackageName());
PendingIntent launchPendingIntent = PendingIntent.getActivity(mContext,
0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent nextArtworkIntent = new Intent(mContext, MuzeiAppWidgetProvider.class);
nextArtworkIntent.setAction(MuzeiAppWidgetProvider.ACTION_NEXT_ARTWORK);
PendingIntent nextArtworkPendingIntent = PendingIntent.getBroadcast(mContext,
0, nextArtworkIntent, PendingIntent.FLAG_UPDATE_CURRENT);
DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
int smallWidgetHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.widget_small_height_breakpoint);
for (int widgetId : appWidgetIds) {
Bundle extras = appWidgetManager.getAppWidgetOptions(widgetId);
int widgetWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
extras.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH), displayMetrics);
int widgetHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
extras.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT), displayMetrics);
Bitmap image;
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri), null, options);
int width = options.outWidth;
int height = options.outHeight;
options.inJustDecodeBounds = false;
options.inSampleSize = Math.min(ImageUtil.calculateSampleSize(width, widgetWidth),
ImageUtil.calculateSampleSize(height, widgetHeight));
image = BitmapFactory.decodeStream(
contentResolver.openInputStream(imageUri), null, options);
} catch (FileNotFoundException e) {
Log.e(TAG, "Could not find current artwork image", e);
return false;
}
@LayoutRes int widgetLayout = widgetHeight < smallWidgetHeight
? R.layout.widget_small
: R.layout.widget;
RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), widgetLayout);
remoteViews.setContentDescription(R.id.widget_background, contentDescription);
remoteViews.setImageViewBitmap(R.id.widget_background, image);
remoteViews.setOnClickPendingIntent(R.id.widget_background, launchPendingIntent);
remoteViews.setOnClickPendingIntent(R.id.widget_next_artwork, nextArtworkPendingIntent);
if (supportsNextArtwork) {
remoteViews.setViewVisibility(R.id.widget_next_artwork, View.VISIBLE);
} else {
remoteViews.setViewVisibility(R.id.widget_next_artwork, View.GONE);
}
appWidgetManager.updateAppWidget(widgetId, remoteViews);
}
return true;
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2014 Google Inc.
*
* 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.google.android.apps.muzei.widget;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;

import com.google.android.apps.muzei.SourceManager;
import com.google.android.apps.muzei.api.MuzeiArtSource;

/**
* AppWidgetProvider for Muzei. The actual updating is done asynchronously in
* {@link AppWidgetUpdateTask}.
*/
public class MuzeiAppWidgetProvider extends AppWidgetProvider {
static final String ACTION_NEXT_ARTWORK = "com.google.android.apps.muzei.action.WIDGET_NEXT_ARTWORK";

@Override
public void onEnabled(final Context context) {
// Enable the AppWidgetUpdateReceiver as we now have widgets that need to be updated
PackageManager packageManager = context.getPackageManager();
ComponentName componentName = new ComponentName(context, AppWidgetUpdateReceiver.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}

@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if (intent != null && ACTION_NEXT_ARTWORK.equals(intent.getAction())) {
SourceManager.sendAction(context.getApplicationContext(), MuzeiArtSource.BUILTIN_COMMAND_ID_NEXT_ARTWORK);
}
}

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
updateWidgets(context);
}


@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager,
int appWidgetId, Bundle newOptions) {
updateWidgets(context);
}

private void updateWidgets(final Context context) {
final PendingResult result = goAsync();
new AppWidgetUpdateTask(context) {
@Override
protected void onPostExecute(Boolean success) {
result.finish();
}
}.execute();
}

@Override
public void onDisabled(final Context context) {
// Disable the AppWidgetUpdateReceiver as we no longer have widgets that need to be updated
PackageManager packageManager = context.getPackageManager();
ComponentName componentName = new ComponentName(context, AppWidgetUpdateReceiver.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
}
Binary file added main/src/main/res/drawable-nodpi/preview.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions main/src/main/res/drawable/widget_scrim.xml
@@ -0,0 +1,19 @@
<!--
Copyright 2014 Google Inc.
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.
-->

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:startColor="#0000"
android:endColor="#4000"
android:angle="270" />
</shape>
45 changes: 45 additions & 0 deletions main/src/main/res/layout/widget.xml
@@ -0,0 +1,45 @@
<!--
Copyright 2014 Google Inc.
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.
-->

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/widget_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:scaleType="centerCrop"
tools:ignore="ContentDescription" />

<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/widget_scrim_height"
android:layout_gravity="bottom"
android:background="@drawable/widget_scrim" />

<ImageButton
android:id="@+id/widget_next_artwork"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:background="@drawable/white_selectable_item_background"
android:contentDescription="@string/action_next_artwork"
android:padding="@dimen/widget_padding"
android:src="@drawable/ic_skip" />
</FrameLayout>

0 comments on commit 057062f

Please sign in to comment.