diff --git a/res/layout/preference_memoryusage.xml b/res/layout/preference_memoryusage.xml new file mode 100644 index 00000000000..7972de9d41a --- /dev/null +++ b/res/layout/preference_memoryusage.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index 193388c8aea..8a2b68c30bf 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -17,5 +17,10 @@ #000 #F00 + + #333 + #F33 + #3F3 + #FFF diff --git a/res/values/strings.xml b/res/values/strings.xml index 7c09c711b3d..4c62fe49e17 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1456,6 +1456,12 @@ Available space Total space + + Calculating… + + Application usage + + Media usage Unmount shared storage diff --git a/res/xml/device_info_memory.xml b/res/xml/device_info_memory.xml index 56fe0876920..0c05213ad84 100644 --- a/res/xml/device_info_memory.xml +++ b/res/xml/device_info_memory.xml @@ -36,8 +36,21 @@ - + + + + mPendingApps = new ArrayList(); + + private volatile boolean mBound = false; + + private long mAppsSize = 0; + + final private ServiceConnection mDefContainerConn = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder service) { + mBound = true; + IMediaContainerService imcs = IMediaContainerService.Stub.asInterface(service); + mMeasurementHandler.sendMessage(mMeasurementHandler.obtainMessage( + MemoryMeasurementHandler.MSG_CONNECTED, imcs)); + } + + public void onServiceDisconnected(ComponentName name) { + mBound = false; + } + }; + + MemoryMeasurementHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_MEASURE_ALL: { + updateExternalStorage(); + updateApproximateInternalStorage(); + + Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); + getActivity().bindService(service, mDefContainerConn, Context.BIND_AUTO_CREATE); + + mUpdateHandler.sendEmptyMessage(MSG_UI_UPDATE_APPROXIMATE); + break; + } + case MSG_CONNECTED: { + IMediaContainerService imcs = (IMediaContainerService) msg.obj; + updateExactInternalStorage(imcs); + } + } + } + + public void cleanUp() { + if (mBound) { + getActivity().unbindService(mDefContainerConn); + } + } + + public void queuePackageMeasurementLocked(String packageName) { + mPendingApps.add(packageName); + } + + public void requestQueuedMeasurementsLocked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + final PackageManager pm = activity.getPackageManager(); + if (pm == null) { + return; + } + + final int N = mPendingApps.size(); + for (int i = 0; i < N; i++) { + pm.getPackageSizeInfo(mPendingApps.get(i), mStatsObserver); + } + } + + final IPackageStatsObserver.Stub mStatsObserver = new IPackageStatsObserver.Stub() { + public void onGetStatsCompleted(PackageStats stats, boolean succeeded) { + if (succeeded) { + mAppsSize += stats.codeSize + stats.dataSize; + } + + synchronized (mPendingApps) { + mPendingApps.remove(stats.packageName); + + if (mPendingApps.size() == 0) { + mInternalAppsSize = mAppsSize; + + mUpdateHandler.sendEmptyMessage(MSG_UI_UPDATE_EXACT); + } + } + } + }; + + private void updateApproximateInternalStorage() { + final File dataPath = Environment.getDataDirectory(); + final StatFs stat = new StatFs(dataPath.getPath()); + final long blockSize = stat.getBlockSize(); + final long totalBlocks = stat.getBlockCount(); + final long availableBlocks = stat.getAvailableBlocks(); + + final long totalSize = totalBlocks * blockSize; + final long availSize = availableBlocks * blockSize; + mInternalSize.setSummary(formatSize(totalSize)); + mInternalAvail.setSummary(formatSize(availSize)); + + mInternalTotalSize = totalSize; + mInternalUsedSize = totalSize - availSize; + } + + private void updateExactInternalStorage(IMediaContainerService imcs) { + long mediaSize; + try { + // TODO get these directories from somewhere + mediaSize = imcs.calculateDirectorySize("/data/media"); + } catch (Exception e) { + Log.i(TAG, "Could not read memory from default container service"); + return; + } + + mInternalMediaSize = mediaSize; + + // We have to get installd to measure the package sizes. + PackageManager pm = getPackageManager(); + List apps = pm + .getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES + | PackageManager.GET_DISABLED_COMPONENTS); + if (apps != null) { + synchronized (mPendingApps) { + for (int i = 0; i < apps.size(); i++) { + final ApplicationInfo info = apps.get(i); + queuePackageMeasurementLocked(info.packageName); + } + + requestQueuedMeasurementsLocked(); + } + } + } + + private void updateExternalStorage() { + if (Environment.isExternalStorageEmulated()) { + return; + } + + String status = Environment.getExternalStorageState(); + String readOnly = ""; + if (status.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { + status = Environment.MEDIA_MOUNTED; + readOnly = mRes.getString(R.string.read_only); + } + + if (status.equals(Environment.MEDIA_MOUNTED)) { + if (!Environment.isExternalStorageRemovable()) { + // This device has built-in storage that is not removable. + // There is no reason for the user to unmount it. + if (mSdMountToggleAdded) { + mSdMountPreferenceGroup.removePreference(mSdMountToggle); + mSdMountToggleAdded = false; + } + } + try { + File path = Environment.getExternalStorageDirectory(); + StatFs stat = new StatFs(path.getPath()); + long blockSize = stat.getBlockSize(); + long totalBlocks = stat.getBlockCount(); + long availableBlocks = stat.getAvailableBlocks(); + + mSdSize.setSummary(formatSize(totalBlocks * blockSize)); + mSdAvail.setSummary(formatSize(availableBlocks * blockSize) + readOnly); + + mSdMountToggle.setEnabled(true); + mSdMountToggle.setTitle(mRes.getString(R.string.sd_eject)); + mSdMountToggle.setSummary(mRes.getString(R.string.sd_eject_summary)); + + } catch (IllegalArgumentException e) { + // this can occur if the SD card is removed, but we haven't + // received the + // ACTION_MEDIA_REMOVED Intent yet. + status = Environment.MEDIA_REMOVED; + } + } else { + mSdSize.setSummary(mRes.getString(R.string.sd_unavailable)); + mSdAvail.setSummary(mRes.getString(R.string.sd_unavailable)); + + if (!Environment.isExternalStorageRemovable()) { + if (status.equals(Environment.MEDIA_UNMOUNTED)) { + if (!mSdMountToggleAdded) { + mSdMountPreferenceGroup.addPreference(mSdMountToggle); + mSdMountToggleAdded = true; + } + } + } + + if (status.equals(Environment.MEDIA_UNMOUNTED) || + status.equals(Environment.MEDIA_NOFS) || + status.equals(Environment.MEDIA_UNMOUNTABLE) ) { + mSdMountToggle.setEnabled(true); + mSdMountToggle.setTitle(mRes.getString(R.string.sd_mount)); + mSdMountToggle.setSummary(mRes.getString(R.string.sd_mount_summary)); + } else { + mSdMountToggle.setEnabled(false); + mSdMountToggle.setTitle(mRes.getString(R.string.sd_mount)); + mSdMountToggle.setSummary(mRes.getString(R.string.sd_insert_summary)); + } + } + } + } + + private MemoryMeasurementHandler mMeasurementHandler; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -99,7 +377,7 @@ public class Memory extends SettingsPreferenceFragment implements OnCancelListen } addPreferencesFromResource(R.xml.device_info_memory); - + mRes = getResources(); mSdSize = findPreference(MEMORY_SD_SIZE); mSdAvail = findPreference(MEMORY_SD_AVAIL); @@ -107,6 +385,12 @@ public class Memory extends SettingsPreferenceFragment implements OnCancelListen mSdFormat = findPreference(MEMORY_SD_FORMAT); mSdMountPreferenceGroup = (PreferenceGroup)findPreference(MEMORY_SD_GROUP); + if (Environment.isExternalStorageEmulated()) { + mSdMountPreferenceGroup.removePreference(mSdSize); + mSdMountPreferenceGroup.removePreference(mSdAvail); + mSdMountPreferenceGroup.removePreference(mSdMountToggle); + } + mPtpModeToggle = (CheckBoxPreference)findPreference(PTP_MODE_TOGGLE); if (Usb.isFunctionSupported(Usb.USB_FUNCTION_MTP)) { mPtpModeToggle.setChecked(Settings.System.getInt( @@ -116,35 +400,53 @@ public class Memory extends SettingsPreferenceFragment implements OnCancelListen // hide the PTP mode toggle checkbox if MTP is not supported getPreferenceScreen().removePreference(mPtpModeToggle); } + + mInternalSize = findPreference(MEMORY_INTERNAL_SIZE); + mInternalAvail = findPreference(MEMORY_INTERNAL_AVAIL); + mInternalMediaUsage = findPreference(MEMORY_INTERNAL_MEDIA); + mInternalAppsUsage = findPreference(MEMORY_INTERNAL_APPS); + + mInternalMediaColor = mRes.getColor(R.color.memory_media_usage); + mInternalAppsColor = mRes.getColor(R.color.memory_apps_usage); + mInternalUsedColor = mRes.getColor(R.color.memory_used); + + mInternalUsageChart = (UsageBarPreference) findPreference(MEMORY_INTERNAL_CHART); + + // Start the thread that will measure the disk usage. + final HandlerThread t = new HandlerThread("MeasurementHandler"); + t.start(); + mMeasurementHandler = new MemoryMeasurementHandler(t.getLooper()); } - + @Override public void onResume() { super.onResume(); - + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_SCANNER_STARTED); intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); intentFilter.addDataScheme("file"); getActivity().registerReceiver(mReceiver, intentFilter); - updateMemoryStatus(); + if (!mMeasured) { + mMeasurementHandler.sendEmptyMessage(MemoryMeasurementHandler.MSG_MEASURE_ALL); + } } StorageEventListener mStorageListener = new StorageEventListener() { - @Override public void onStorageStateChanged(String path, String oldState, String newState) { Log.i(TAG, "Received storage state changed notification that " + path + " changed state from " + oldState + " to " + newState); - updateMemoryStatus(); + mMeasurementHandler.sendEmptyMessage(MemoryMeasurementHandler.MSG_MEASURE_ALL); } }; - + @Override public void onPause() { super.onPause(); getActivity().unregisterReceiver(mReceiver); + mMeasurementHandler.removeMessages(MemoryMeasurementHandler.MSG_MEASURE_ALL); } @Override @@ -152,6 +454,7 @@ public class Memory extends SettingsPreferenceFragment implements OnCancelListen if (mStorageManager != null && mStorageListener != null) { mStorageManager.unregisterListener(mStorageListener); } + mMeasurementHandler.cleanUp(); super.onDestroy(); } @@ -195,7 +498,7 @@ public class Memory extends SettingsPreferenceFragment implements OnCancelListen private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - updateMemoryStatus(); + mMeasurementHandler.sendEmptyMessage(MemoryMeasurementHandler.MSG_MEASURE_ALL); } }; @@ -290,77 +593,35 @@ public class Memory extends SettingsPreferenceFragment implements OnCancelListen } } - private void updateMemoryStatus() { - String status = Environment.getExternalStorageState(); - String readOnly = ""; - if (status.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { - status = Environment.MEDIA_MOUNTED; - readOnly = mRes.getString(R.string.read_only); + private void updateUiExact() { + final float totalSize = mInternalTotalSize; + + final long mediaSize = mInternalMediaSize; + final long appsSize = mInternalAppsSize; + + mInternalUsageChart.clear(); + mInternalUsageChart.addEntry(mediaSize / totalSize, mInternalMediaColor); + mInternalUsageChart.addEntry(appsSize / totalSize, mInternalAppsColor); + + // There are other things that can take up storage, but we didn't + // measure it. + final long remaining = mInternalUsedSize - (mediaSize + appsSize); + if (remaining > 0) { + mInternalUsageChart.addEntry(remaining / totalSize, mInternalUsedColor); } - - if (status.equals(Environment.MEDIA_MOUNTED)) { - if (!Environment.isExternalStorageRemovable()) { - // This device has built-in storage that is not removable. - // There is no reason for the user to unmount it. - if (mSdMountToggleAdded) { - mSdMountPreferenceGroup.removePreference(mSdMountToggle); - mSdMountToggleAdded = false; - } - } - try { - File path = Environment.getExternalStorageDirectory(); - StatFs stat = new StatFs(path.getPath()); - long blockSize = stat.getBlockSize(); - long totalBlocks = stat.getBlockCount(); - long availableBlocks = stat.getAvailableBlocks(); - - mSdSize.setSummary(formatSize(totalBlocks * blockSize)); - mSdAvail.setSummary(formatSize(availableBlocks * blockSize) + readOnly); + mInternalUsageChart.commit(); - mSdMountToggle.setEnabled(true); - mSdMountToggle.setTitle(mRes.getString(R.string.sd_eject)); - mSdMountToggle.setSummary(mRes.getString(R.string.sd_eject_summary)); - - } catch (IllegalArgumentException e) { - // this can occur if the SD card is removed, but we haven't received the - // ACTION_MEDIA_REMOVED Intent yet. - status = Environment.MEDIA_REMOVED; - } - - } else { - mSdSize.setSummary(mRes.getString(R.string.sd_unavailable)); - mSdAvail.setSummary(mRes.getString(R.string.sd_unavailable)); - - - if (!Environment.isExternalStorageRemovable()) { - if (status.equals(Environment.MEDIA_UNMOUNTED)) { - if (!mSdMountToggleAdded) { - mSdMountPreferenceGroup.addPreference(mSdMountToggle); - mSdMountToggleAdded = true; - } - } - } - - if (status.equals(Environment.MEDIA_UNMOUNTED) || - status.equals(Environment.MEDIA_NOFS) || - status.equals(Environment.MEDIA_UNMOUNTABLE) ) { - mSdMountToggle.setEnabled(true); - mSdMountToggle.setTitle(mRes.getString(R.string.sd_mount)); - mSdMountToggle.setSummary(mRes.getString(R.string.sd_mount_summary)); - } else { - mSdMountToggle.setEnabled(false); - mSdMountToggle.setTitle(mRes.getString(R.string.sd_mount)); - mSdMountToggle.setSummary(mRes.getString(R.string.sd_insert_summary)); - } - } - - File path = Environment.getDataDirectory(); - StatFs stat = new StatFs(path.getPath()); - long blockSize = stat.getBlockSize(); - long availableBlocks = stat.getAvailableBlocks(); - findPreference("memory_internal_avail").setSummary(formatSize(availableBlocks * blockSize)); + mInternalMediaUsage.setSummary(formatSize(mediaSize)); + mInternalAppsUsage.setSummary(formatSize(appsSize)); } - + + private void updateUiApproximate() { + mInternalUsageChart.clear(); + mInternalUsageChart.addEntry(mInternalUsedSize / (float) mInternalTotalSize, getResources() + .getColor(R.color.memory_used)); + mInternalUsageChart.commit(); + } + private String formatSize(long size) { return Formatter.formatFileSize(getActivity(), size); } @@ -369,5 +630,4 @@ public class Memory extends SettingsPreferenceFragment implements OnCancelListen // TODO: Is this really required? // finish(); } - } diff --git a/src/com/android/settings/deviceinfo/PercentageBarChart.java b/src/com/android/settings/deviceinfo/PercentageBarChart.java new file mode 100644 index 00000000000..e8fb62adf68 --- /dev/null +++ b/src/com/android/settings/deviceinfo/PercentageBarChart.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 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.settings.deviceinfo; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Collection; + +/** + * + */ +public class PercentageBarChart extends View { + private final Paint mBackgroundPaint = new Paint(); + + private Collection mEntries; + + public static class Entry { + public final float percentage; + public final Paint paint; + + protected Entry(float percentage, Paint paint) { + this.percentage = percentage; + this.paint = paint; + } + } + + public PercentageBarChart(Context context, AttributeSet attrs) { + super(context, attrs); + + mBackgroundPaint.setARGB(255, 64, 64, 64); + mBackgroundPaint.setStyle(Paint.Style.FILL); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final int width = getWidth(); + final int height = getHeight(); + + canvas.drawPaint(mBackgroundPaint); + + int lastX = 0; + + if (mEntries != null) { + for (final Entry e : mEntries) { + final int entryWidth; + if (e.percentage == 0f) { + entryWidth = 0; + } else { + entryWidth = Math.max(1, (int) (width * e.percentage)); + } + + final int nextX = lastX + entryWidth; + if (nextX >= width) { + break; + } + + canvas.drawRect(lastX, 0, nextX, height, e.paint); + lastX = nextX; + } + } + } + + /** + * Sets the background for this chart. Callers are responsible for later + * calling {@link #invalidate()}. + */ + public void setBackgroundColor(int color) { + mBackgroundPaint.setColor(color); + } + + /** + * Adds a new slice to the percentage bar chart. Callers are responsible for + * later calling {@link #invalidate()}. + * + * @param percentage the total width that + * @param color the color to draw the entry + */ + public static Entry createEntry(float percentage, int color) { + final Paint p = new Paint(); + p.setColor(color); + p.setStyle(Paint.Style.FILL); + + return new Entry(percentage, p); + } + + public void setEntries(Collection entries) { + mEntries = entries; + } +} diff --git a/src/com/android/settings/deviceinfo/UsageBarPreference.java b/src/com/android/settings/deviceinfo/UsageBarPreference.java new file mode 100644 index 00000000000..e9909f19a4e --- /dev/null +++ b/src/com/android/settings/deviceinfo/UsageBarPreference.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2010 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.settings.deviceinfo; + +import com.android.settings.R; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Creates a percentage bar chart inside a preference. + */ +public class UsageBarPreference extends Preference { + private PercentageBarChart mChart = null; + + private final Collection mEntries = new ArrayList(); + + public UsageBarPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setWidgetLayoutResource(R.layout.preference_memoryusage); + } + + public UsageBarPreference(Context context) { + super(context); + setWidgetLayoutResource(R.layout.preference_memoryusage); + } + + public UsageBarPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setWidgetLayoutResource(R.layout.preference_memoryusage); + } + + public void addEntry(float percentage, int color) { + mEntries.add(PercentageBarChart.createEntry(percentage, color)); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + mChart = (PercentageBarChart) view.findViewById(R.id.percentage_bar_chart); + + mChart.setEntries(mEntries); + } + + public void commit() { + if (mChart != null) { + mChart.invalidate(); + } + } + + public void clear() { + mEntries.clear(); + } +}