Themes: Broker the theme service

Change-Id: I429936f63d52eddcb1653515bc94e82f758b57d6
This commit is contained in:
d34d 2016-07-15 09:56:19 -07:00 committed by Gerrit Code Review
parent 4cf18a3e36
commit c4ed8c84cd
6 changed files with 473 additions and 1419 deletions

View File

@ -1,116 +0,0 @@
/*
* Copyright (C) 2016 The CyanogenMod 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 org.cyanogenmod.platform.internal;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Binder;
import android.os.FileUtils;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.android.server.SystemService;
import cyanogenmod.app.CMContextConstants;
import org.cyanogenmod.internal.themes.IIconCacheManager;
import org.cyanogenmod.internal.util.ThemeUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.Comparator;
/** @hide */
public class IconCacheManagerService extends CMSystemService {
private static final String TAG = IconCacheManagerService.class.getSimpleName();
private static final long MAX_ICON_CACHE_SIZE = 33554432L; // 32MB
private static final long PURGED_ICON_CACHE_SIZE = 25165824L; // 24 MB
private long mIconCacheSize = 0L;
private Context mContext;
public IconCacheManagerService(Context context) {
super(context);
mContext = context;
}
@Override
public String getFeatureDeclaration() {
return CMContextConstants.Features.THEMES;
}
@Override
public void onStart() {
Log.d(TAG, "registerIconCache cmiconcache: " + this);
publishBinderService(CMContextConstants.CM_ICON_CACHE_SERVICE, mService);
}
private void purgeIconCache() {
Log.d(TAG, "Purging icon cahe of size " + mIconCacheSize);
File cacheDir = new File(ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR);
File[] files = cacheDir.listFiles();
Arrays.sort(files, mOldestFilesFirstComparator);
for (File f : files) {
if (!f.isDirectory()) {
final long size = f.length();
if(f.delete()) mIconCacheSize -= size;
}
if (mIconCacheSize <= PURGED_ICON_CACHE_SIZE) break;
}
}
private Comparator<File> mOldestFilesFirstComparator = new Comparator<File>() {
@Override
public int compare(File lhs, File rhs) {
return (int) (lhs.lastModified() - rhs.lastModified());
}
};
private IBinder mService = new IIconCacheManager.Stub() {
@Override
public boolean cacheComposedIcon(Bitmap icon, String fileName) throws RemoteException {
final long token = Binder.clearCallingIdentity();
boolean success;
FileOutputStream os;
final File cacheDir = new File(ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR);
if (cacheDir.listFiles().length == 0) {
mIconCacheSize = 0;
}
try {
File outFile = new File(cacheDir, fileName);
os = new FileOutputStream(outFile);
icon.compress(Bitmap.CompressFormat.PNG, 90, os);
os.close();
FileUtils.setPermissions(outFile,
FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH,
-1, -1);
mIconCacheSize += outFile.length();
if (mIconCacheSize > MAX_ICON_CACHE_SIZE) {
purgeIconCache();
}
success = true;
} catch (Exception e) {
success = false;
Log.w(TAG, "Unable to cache icon " + fileName, e);
}
Binder.restoreCallingIdentity(token);
return success;
}
};
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (C) 2016 The CyanogenMod 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 org.cyanogenmod.platform.internal;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.IBinder;
import android.os.RemoteException;
import cyanogenmod.app.CMContextConstants;
import org.cyanogenmod.internal.themes.IIconCacheManager;
import org.cyanogenmod.platform.internal.common.BrokeredServiceConnection;
/**
* Icon cache service broker for connecting clients to a backing icon cache manager service.
*
* @hide
*/
public class IconCacheManagerServiceBroker extends BrokerableCMSystemService<IIconCacheManager> {
private static final ComponentName SERVICE_COMPONENT =
new ComponentName("org.cyanogenmod.themeservice",
"org.cyanogenmod.themeservice.IconCacheManagerService");
private final IIconCacheManager mServiceStubForFailure = new IIconCacheManager.Stub() {
@Override
public boolean cacheComposedIcon(Bitmap icon, String path) throws RemoteException {
return false;
}
};
private BrokeredServiceConnection mServiceConnection = new BrokeredServiceConnection() {
@Override
public void onBrokeredServiceConnected() {
}
@Override
public void onBrokeredServiceDisconnected() {
}
};
private final class BinderService extends IIconCacheManager.Stub {
@Override
public boolean cacheComposedIcon(Bitmap icon, String path) throws RemoteException {
return getBrokeredService().cacheComposedIcon(icon, path);
}
}
public IconCacheManagerServiceBroker(Context context) {
super(context);
setBrokeredServiceConnection(mServiceConnection);
}
@Override
public String getFeatureDeclaration() {
return CMContextConstants.Features.THEMES;
}
@Override
protected IIconCacheManager getIBinderAsIInterface(@NonNull IBinder service) {
return IIconCacheManager.Stub.asInterface(service);
}
@Override
protected IIconCacheManager getDefaultImplementation() {
return mServiceStubForFailure;
}
@Override
protected ComponentName getServiceComponent() {
return SERVICE_COMPONENT;
}
@Override
public void onStart() {
publishBinderService(CMContextConstants.CM_ICON_CACHE_SERVICE, new BinderService());
}
@Override
protected String getComponentFilteringPermission() {
return cyanogenmod.platform.Manifest.permission.ACCESS_THEME_MANAGER;
}
}

View File

@ -0,0 +1,366 @@
/*
* Copyright (C) 2016 The CyanogenMod 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 org.cyanogenmod.platform.internal;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.os.FileUtils;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import android.util.Log;
import android.util.Slog;
import cyanogenmod.app.CMContextConstants;
import cyanogenmod.platform.Manifest;
import cyanogenmod.themes.IThemeChangeListener;
import cyanogenmod.themes.IThemeProcessingListener;
import cyanogenmod.themes.IThemeService;
import cyanogenmod.themes.ThemeChangeRequest;
import org.cyanogenmod.internal.util.ThemeUtils;
import org.cyanogenmod.platform.internal.common.BrokeredServiceConnection;
import java.io.File;
import static cyanogenmod.platform.Manifest.permission.ACCESS_THEME_MANAGER;
/**
* Theme service broker for connecting clients to a backing theme manager service.
*
* @hide
*/
public class ThemeManagerServiceBroker extends BrokerableCMSystemService<IThemeService> {
private static final String TAG = ThemeManagerServiceBroker.class.getSimpleName();
private static final boolean DEBUG = false;
private static final ComponentName SERVICE_COMPONENT =
new ComponentName("org.cyanogenmod.themeservice",
"org.cyanogenmod.themeservice.ThemeManagerService");
// Cached change listeners
private final RemoteCallbackList<IThemeChangeListener> mChangeListeners =
new RemoteCallbackList<>();
private final RemoteCallbackList<IThemeProcessingListener> mProcessingListeners =
new RemoteCallbackList<>();
private Context mContext;
private final IThemeService mServiceStubForFailure = new IThemeService.Stub() {
@Override
public void requestThemeChangeUpdates(IThemeChangeListener listener) throws RemoteException {
}
@Override
public void removeUpdates(IThemeChangeListener listener) throws RemoteException {
}
@Override
public void requestThemeChange(ThemeChangeRequest request,
boolean removePerAppThemes) throws RemoteException {
}
@Override
public void applyDefaultTheme() throws RemoteException {
}
@Override
public boolean isThemeApplying() throws RemoteException {
return false;
}
@Override
public int getProgress() throws RemoteException {
return 0;
}
@Override
public boolean processThemeResources(String themePkgName) throws RemoteException {
return false;
}
@Override
public boolean isThemeBeingProcessed(String themePkgName) throws RemoteException {
return false;
}
@Override
public void registerThemeProcessingListener(
IThemeProcessingListener listener) throws RemoteException {
}
@Override
public void unregisterThemeProcessingListener(
IThemeProcessingListener listener) throws RemoteException {
}
@Override
public void rebuildResourceCache() throws RemoteException {
}
@Override
public long getLastThemeChangeTime() throws RemoteException {
return 0;
}
@Override
public int getLastThemeChangeRequestType() throws RemoteException {
return 0;
}
};
private final class BinderService extends IThemeService.Stub {
@Override
public void requestThemeChangeUpdates(IThemeChangeListener listener)
throws RemoteException {
enforcePermission();
getBrokeredService().requestThemeChangeUpdates(listener);
mChangeListeners.register(listener);
}
@Override
public void removeUpdates(IThemeChangeListener listener) throws RemoteException {
enforcePermission();
getBrokeredService().removeUpdates(listener);
mChangeListeners.unregister(listener);
}
@Override
public void requestThemeChange(ThemeChangeRequest request,
boolean removePerAppThemes) throws RemoteException {
enforcePermission();
getBrokeredService().requestThemeChange(request, removePerAppThemes);
}
@Override
public void applyDefaultTheme() throws RemoteException {
enforcePermission();
getBrokeredService().applyDefaultTheme();
}
@Override
public boolean isThemeApplying() throws RemoteException {
enforcePermission();
return getBrokeredService().isThemeApplying();
}
@Override
public int getProgress() throws RemoteException {
enforcePermission();
return getBrokeredService().getProgress();
}
@Override
public boolean processThemeResources(String themePkgName) throws RemoteException {
enforcePermission();
return getBrokeredService().processThemeResources(themePkgName);
}
@Override
public boolean isThemeBeingProcessed(String themePkgName) throws RemoteException {
enforcePermission();
return getBrokeredService().isThemeBeingProcessed(themePkgName);
}
@Override
public void registerThemeProcessingListener(
IThemeProcessingListener listener) throws RemoteException {
enforcePermission();
getBrokeredService().registerThemeProcessingListener(listener);
mProcessingListeners.register(listener);
}
@Override
public void unregisterThemeProcessingListener(
IThemeProcessingListener listener) throws RemoteException {
enforcePermission();
getBrokeredService().unregisterThemeProcessingListener(listener);
mProcessingListeners.unregister(listener);
}
@Override
public void rebuildResourceCache() throws RemoteException {
enforcePermission();
getBrokeredService().rebuildResourceCache();
}
@Override
public long getLastThemeChangeTime() throws RemoteException {
enforcePermission();
return getBrokeredService().getLastThemeChangeTime();
}
@Override
public int getLastThemeChangeRequestType() throws RemoteException {
enforcePermission();
return getBrokeredService().getLastThemeChangeRequestType();
}
}
public ThemeManagerServiceBroker(Context context) {
super(context);
mContext = context;
setBrokeredServiceConnection(mServiceConnection);
}
@Override
protected IThemeService getIBinderAsIInterface(@NonNull IBinder service) {
return IThemeService.Stub.asInterface(service);
}
@Override
protected IThemeService getDefaultImplementation() {
return mServiceStubForFailure;
}
@Override
protected ComponentName getServiceComponent() {
return SERVICE_COMPONENT;
}
@Override
public String getFeatureDeclaration() {
return CMContextConstants.Features.THEMES;
}
@Override
public void onStart() {
if (DEBUG) Slog.d(TAG, "service started");
publishBinderService(CMContextConstants.CM_THEME_SERVICE, new BinderService());
}
@Override
protected String getComponentFilteringPermission() {
return Manifest.permission.ACCESS_THEME_MANAGER;
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_SYSTEM_SERVICES_READY) {
// create the main theme directory for brokered service
createDirIfNotExists(ThemeUtils.SYSTEM_THEME_PATH);
if (shouldMigrateFilePermissions()) {
migrateFilePermissions();
}
} else if (phase == PHASE_ACTIVITY_MANAGER_READY) {
tryConnecting();
}
super.onBootPhase(phase);
}
private BrokeredServiceConnection mServiceConnection = new BrokeredServiceConnection() {
@Override
public void onBrokeredServiceConnected() {
// If any change listeners are cached, register them with the newly connected
// service.
IThemeService themeService =
getBrokeredService();
try {
int N = mChangeListeners.beginBroadcast();
if (themeService != null && N > 0) {
for (int i = 0; i < N; i++) {
themeService.requestThemeChangeUpdates(
mChangeListeners.getBroadcastItem(i));
}
}
} catch (RemoteException e) {
/* ignore */
} finally {
mChangeListeners.finishBroadcast();
}
try {
int N = mProcessingListeners.beginBroadcast();
if (themeService != null && N > 0) {
for (int i = 0; i < N; i++) {
themeService.registerThemeProcessingListener(
mProcessingListeners.getBroadcastItem(i));
}
}
} catch (RemoteException e) {
/* ignore */
} finally {
mProcessingListeners.finishBroadcast();
}
}
@Override
public void onBrokeredServiceDisconnected() {
}
};
private void enforcePermission() {
mContext.enforceCallingOrSelfPermission(ACCESS_THEME_MANAGER, null);
}
/**
* @return True if {@link ThemeUtils#SYSTEM_THEME_ALARM_PATH} is not writable by other users
*/
private boolean shouldMigrateFilePermissions() {
return isUserWritable(ThemeUtils.SYSTEM_THEME_ALARM_PATH);
}
/**
* Ensures other users can write files in /data/system/theme/* which is necessary for the
* brokered service to write theme data files. SELinux policies will not allow random third
* party apps to write to this location.
*/
private void migrateFilePermissions() {
File[] files = new File(ThemeUtils.SYSTEM_THEME_PATH).listFiles();
for (File file : files) {
setAllUsersWritable(file, true);
}
}
private void setAllUsersWritable(File file, boolean recursive) {
if (file.isDirectory() && recursive) {
File[] files = file.listFiles();
for (File childFile : files) {
setAllUsersWritable(childFile, recursive);
}
}
if (!isUserWritable(file.getAbsolutePath())) {
file.setWritable(true, false);
}
}
private boolean isUserWritable(String path) {
try {
StructStat stat = Os.stat(path);
return (stat.st_mode & 2) == 0;
} catch (ErrnoException e) {
Log.w(TAG, "Cannot stat " + path);
}
return false;
}
private static void createDirIfNotExists(String dirPath) {
final File dir = new File(dirPath);
if (!dir.exists()) {
if (dir.mkdir()) {
FileUtils.setPermissions(dir, FileUtils.S_IRWXU |
FileUtils.S_IRWXG| FileUtils.S_IRWXO, -1, -1);
}
}
}
}

View File

@ -248,6 +248,11 @@
<permission android:name="cyanogenmod.permission.BIND_CORE_SERVICE"
android:protectionLevel="signature|privileged" />
<!-- Not meant for third parties.
@hide -->
<permission android:name="cyanogenmod.permission.SEND_PROTECTED_THEME_BROADCAST"
android:protectionLevel="signature|privileged" />
<application android:process="system"
android:persistent="true"
android:hasCode="false"

View File

@ -107,8 +107,8 @@
<item>org.cyanogenmod.platform.internal.CMHardwareService</item>
<item>org.cyanogenmod.platform.internal.AppSuggestManagerService</item>
<item>org.cyanogenmod.platform.internal.PerformanceManagerService</item>
<item>org.cyanogenmod.platform.internal.ThemeManagerService</item>
<item>org.cyanogenmod.platform.internal.IconCacheManagerService</item>
<item>org.cyanogenmod.platform.internal.ThemeManagerServiceBroker</item>
<item>org.cyanogenmod.platform.internal.IconCacheManagerServiceBroker</item>
<item>org.cyanogenmod.platform.internal.LiveLockScreenServiceBroker</item>
<item>org.cyanogenmod.platform.internal.CMWeatherManagerService</item>
<item>org.cyanogenmod.platform.internal.display.LiveDisplayService</item>