SysUiTileUpdateRequester.java
/*
* Copyright 2020 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 androidx.wear.tiles;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Variant of {@link TileUpdateRequester} which requests an update from the Wear SysUI app. */
// TODO(b/173688156): Renovate this whole class, and especially work around all the locks.
class SysUiTileUpdateRequester implements TileUpdateRequester {
private static final String TAG = "HTUpdateRequester";
private static final String DEFAULT_TARGET_SYSUI = "com.google.android.wearable.app";
private static final String SYSUI_SETTINGS_KEY = "clockwork_sysui_package";
public static final String ACTION_BIND_UPDATE_REQUESTER =
"androidx.wear.tiles.action.BIND_UPDATE_REQUESTER";
final Context mAppContext;
final Object mLock = new Object();
@GuardedBy("mLock")
boolean mBindInProgress = false;
@GuardedBy("mLock")
final Set<Class<? extends Service>> mPendingServices = new HashSet<>();
public SysUiTileUpdateRequester(@NonNull Context appContext) {
this.mAppContext = appContext;
}
@Override
public void requestUpdate(@NonNull Class<? extends TileService> tileService) {
synchronized (mLock) {
mPendingServices.add(tileService);
if (mBindInProgress) {
// Something else kicked off the bind; let that carry on binding.
return;
} else {
mBindInProgress = true;
}
}
Intent bindIntent = buildUpdateBindIntent();
if (bindIntent == null) {
Log.e(TAG, "Could not build bind intent");
synchronized (mLock) {
mBindInProgress = false;
}
return;
}
bindAndUpdate(bindIntent);
}
private String getSysUiPackageName() {
String sysUiPackageName =
Settings.Global.getString(mAppContext.getContentResolver(), SYSUI_SETTINGS_KEY);
if (sysUiPackageName == null || sysUiPackageName.isEmpty()) {
return DEFAULT_TARGET_SYSUI;
} else {
return sysUiPackageName;
}
}
@Nullable
private Intent buildUpdateBindIntent() {
Intent bindIntent = new Intent(ACTION_BIND_UPDATE_REQUESTER);
bindIntent.setPackage(getSysUiPackageName());
// Find the concrete ComponentName of the service that implements what we need.
PackageManager pm = mAppContext.getPackageManager();
List<ResolveInfo> services =
pm.queryIntentServices(
bindIntent,
PackageManager.GET_META_DATA | PackageManager.GET_RESOLVED_FILTER);
if (services.isEmpty()) {
Log.w(TAG, "Couldn't find any services filtering on " + ACTION_BIND_UPDATE_REQUESTER);
return null;
}
ServiceInfo serviceInfo = services.get(0).serviceInfo;
bindIntent.setClassName(serviceInfo.packageName, serviceInfo.name);
return bindIntent;
}
private void bindAndUpdate(Intent i) {
mAppContext.bindService(
i,
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// Copy so we can shorten the lock duration.
List<Class<? extends Service>> pendingServicesCopy;
synchronized (mLock) {
pendingServicesCopy = new ArrayList<>(mPendingServices);
mPendingServices.clear();
mBindInProgress = false;
}
// This is a little suboptimal, as if an update is requested in this lock,
// we'll unbind, then immediately rebind. That said, this class should be
// used pretty rarely
// (and it'll be rare to have two in-flight update requests at once
// regardless), so
// it's probably fine.
TileUpdateRequesterService updateRequesterService =
TileUpdateRequesterService.Stub.asInterface(service);
for (Class<? extends Service> tileProvider : pendingServicesCopy) {
sendTileUpdateRequest(tileProvider, updateRequesterService);
}
mAppContext.unbindService(this);
}
@Override
public void onServiceDisconnected(ComponentName name) {}
},
Context.BIND_AUTO_CREATE);
}
void sendTileUpdateRequest(
Class<? extends Service> tileProvider,
TileUpdateRequesterService updateRequesterService) {
try {
ComponentName cn = new ComponentName(mAppContext, tileProvider);
updateRequesterService.requestUpdate(cn, new TileUpdateRequestData());
} catch (RemoteException ex) {
Log.w(TAG, "RemoteException while requesting tile update");
}
}
}