diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-18 17:38:50 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-18 17:38:50 -0400 |
commit | bc81c7ada5aab3806dd0b17498f5c9672c9b33c4 (patch) | |
tree | 7fdcc541a9ac9e92134f1a80cec557fee772bcf8 /android | |
parent | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (diff) | |
download | android-28-bc81c7ada5aab3806dd0b17498f5c9672c9b33c4.tar.gz |
Import Android SDK Platform P [4344336]
/google/data/ro/projects/android/fetch_artifact \
--bid 4344336 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4344336.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: If482fcd4cfaf6c5e544e5574926be25a293e9a6d
Diffstat (limited to 'android')
96 files changed, 4083 insertions, 7011 deletions
diff --git a/android/app/FragmentManager.java b/android/app/FragmentManager.java index ba5ea218..0d5cd021 100644 --- a/android/app/FragmentManager.java +++ b/android/app/FragmentManager.java @@ -1380,8 +1380,13 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate @Override public void onAnimationEnd(Animator anim) { container.endViewTransition(view); - if (fragment.getAnimatingAway() != null) { - fragment.setAnimatingAway(null); + Animator animator = f.getAnimatingAway(); + f.setAnimatingAway(null); + // If the animation finished immediately, the fragment's + // view will still be there. If so, we can just pretend + // there was no animation and skip the moveToState() + if (container.indexOfChild(view) == -1 + && animator != null) { moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, false); } diff --git a/android/app/LoadedApk.java b/android/app/LoadedApk.java index b38be662..f6d9710d 100644 --- a/android/app/LoadedApk.java +++ b/android/app/LoadedApk.java @@ -625,17 +625,31 @@ public final class LoadedApk { final List<String> zipPaths = new ArrayList<>(10); final List<String> libPaths = new ArrayList<>(10); - final boolean isBundledApp = mApplicationInfo.isSystemApp() + boolean isBundledApp = mApplicationInfo.isSystemApp() && !mApplicationInfo.isUpdatedSystemApp(); + // Vendor apks are treated as bundled only when /vendor/lib is in the default search + // paths. If not, they are treated as unbundled; access to system libs is limited. + // Having /vendor/lib in the default search paths means that all system processes + // are allowed to use any vendor library, which in turn means that system is dependent + // on vendor partition. In the contrary, not having /vendor/lib in the default search + // paths mean that the two partitions are separated and thus we can treat vendor apks + // as unbundled. + final String defaultSearchPaths = System.getProperty("java.library.path"); + final boolean treatVendorApkAsUnbundled = !defaultSearchPaths.contains("/vendor/lib"); + if (mApplicationInfo.getCodePath() != null + && mApplicationInfo.getCodePath().startsWith("/vendor/") + && treatVendorApkAsUnbundled) { + isBundledApp = false; + } + makePaths(mActivityThread, isBundledApp, mApplicationInfo, zipPaths, libPaths); String libraryPermittedPath = mDataDir; if (isBundledApp) { // This is necessary to grant bundled apps access to // libraries located in subdirectories of /system/lib - libraryPermittedPath += File.pathSeparator + - System.getProperty("java.library.path"); + libraryPermittedPath += File.pathSeparator + defaultSearchPaths; } final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths); diff --git a/android/app/Notification.java b/android/app/Notification.java index 841b9611..ee6c1cba 100644 --- a/android/app/Notification.java +++ b/android/app/Notification.java @@ -67,6 +67,7 @@ import android.text.style.TextAppearanceSpan; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; +import android.util.TypedValue; import android.view.Gravity; import android.view.NotificationHeaderView; import android.view.View; @@ -3905,6 +3906,7 @@ public class Notification implements Parcelable if (p.title != null) { contentView.setViewVisibility(R.id.title, View.VISIBLE); contentView.setTextViewText(R.id.title, processTextSpans(p.title)); + updateTextSizePrimary(contentView, R.id.title); if (!p.ambient) { setTextViewColorPrimary(contentView, R.id.title); } @@ -3916,6 +3918,7 @@ public class Notification implements Parcelable int textId = showProgress ? com.android.internal.R.id.text_line_1 : com.android.internal.R.id.text; contentView.setTextViewText(textId, processTextSpans(p.text)); + updateTextSizeSecondary(contentView, textId); if (!p.ambient) { setTextViewColorSecondary(contentView, textId); } @@ -3927,6 +3930,25 @@ public class Notification implements Parcelable return contentView; } + private void updateTextSizeSecondary(RemoteViews contentView, int textId) { + updateTextSizeColorized(contentView, textId, + com.android.internal.R.dimen.notification_text_size_colorized, + com.android.internal.R.dimen.notification_text_size); + } + + private void updateTextSizePrimary(RemoteViews contentView, int textId) { + updateTextSizeColorized(contentView, textId, + com.android.internal.R.dimen.notification_title_text_size_colorized, + com.android.internal.R.dimen.notification_title_text_size); + } + + private void updateTextSizeColorized(RemoteViews contentView, int textId, + int colorizedDimen, int normalDimen) { + int size = mContext.getResources().getDimensionPixelSize(isColorized() + ? colorizedDimen : normalDimen); + contentView.setTextViewTextSize(textId, TypedValue.COMPLEX_UNIT_PX, size); + } + private CharSequence processTextSpans(CharSequence text) { if (hasForegroundColor()) { return NotificationColorUtil.clearColorSpans(text); @@ -5852,6 +5874,7 @@ public class Notification implements Parcelable builder.setTextViewColorSecondary(contentView, R.id.big_text); contentView.setViewVisibility(R.id.big_text, TextUtils.isEmpty(bigTextText) ? View.GONE : View.VISIBLE); + builder.updateTextSizeSecondary(contentView, R.id.big_text); contentView.setBoolean(R.id.big_text, "setHasImage", builder.mN.hasLargeIcon()); } } @@ -6185,6 +6208,7 @@ public class Notification implements Parcelable contentView.setViewVisibility(rowId, View.VISIBLE); contentView.setTextViewText(rowId, mBuilder.processTextSpans( makeMessageLine(m, mBuilder))); + mBuilder.updateTextSizeSecondary(contentView, rowId); mBuilder.setTextViewColorSecondary(contentView, rowId); if (contractedMessage == m) { @@ -6552,6 +6576,7 @@ public class Notification implements Parcelable contentView.setViewVisibility(rowIds[i], View.VISIBLE); contentView.setTextViewText(rowIds[i], mBuilder.processTextSpans(mBuilder.processLegacyText(str))); + mBuilder.updateTextSizeSecondary(contentView, rowIds[i]); mBuilder.setTextViewColorSecondary(contentView, rowIds[i]); contentView.setViewPadding(rowIds[i], 0, topPadding, 0, 0); handleInboxImageMargin(contentView, rowIds[i], first); @@ -8522,8 +8547,15 @@ public class Notification implements Parcelable final StandardTemplateParams fillTextsFrom(Builder b) { Bundle extras = b.mN.extras; - title = b.processLegacyText(extras.getCharSequence(EXTRA_TITLE), ambient); - text = b.processLegacyText(extras.getCharSequence(EXTRA_TEXT), ambient); + this.title = b.processLegacyText(extras.getCharSequence(EXTRA_TITLE), ambient); + + // Big text notifications should contain their content when viewed in ambient mode. + CharSequence text = extras.getCharSequence(EXTRA_BIG_TEXT); + if (!ambient || TextUtils.isEmpty(text)) { + text = extras.getCharSequence(EXTRA_TEXT); + } + this.text = b.processLegacyText(text, ambient); + return this; } } diff --git a/android/app/RemoteInput.java b/android/app/RemoteInput.java index 8ab19c06..02a01242 100644 --- a/android/app/RemoteInput.java +++ b/android/app/RemoteInput.java @@ -33,8 +33,8 @@ import java.util.Set; * an intent inside a {@link android.app.PendingIntent} that is sent. * Always use {@link RemoteInput.Builder} to create instances of this class. * <p class="note"> See - * <a href="{@docRoot}wear/notifications/remote-input.html">Receiving Voice Input from - * a Notification</a> for more information on how to use this class. + * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html#direct">Replying + * to notifications</a> for more information on how to use this class. * * <p>The following example adds a {@code RemoteInput} to a {@link Notification.Action}, * sets the result key as {@code quick_reply}, and sets the label as {@code Quick reply}. diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java index f1352446..fe18243f 100644 --- a/android/arch/lifecycle/ComputableLiveData.java +++ b/android/arch/lifecycle/ComputableLiveData.java @@ -1,9 +1,136 @@ -//ComputableLiveData interface for tests +/* + * Copyright (C) 2017 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 android.arch.lifecycle; -import android.arch.lifecycle.LiveData; + +import android.arch.core.executor.AppToolkitTaskExecutor; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A LiveData class that can be invalidated & computed on demand. + * <p> + * This is an internal class for now, might be public if we see the necessity. + * + * @param <T> The type of the live data + * @hide internal + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class ComputableLiveData<T> { - public ComputableLiveData(){} - abstract protected T compute(); - public LiveData<T> getLiveData() {return null;} - public void invalidate() {} + + private final LiveData<T> mLiveData; + + private AtomicBoolean mInvalid = new AtomicBoolean(true); + private AtomicBoolean mComputing = new AtomicBoolean(false); + + /** + * Creates a computable live data which is computed when there are active observers. + * <p> + * It can also be invalidated via {@link #invalidate()} which will result in a call to + * {@link #compute()} if there are active observers (or when they start observing) + */ + @SuppressWarnings("WeakerAccess") + public ComputableLiveData() { + mLiveData = new LiveData<T>() { + @Override + protected void onActive() { + // TODO if we make this class public, we should accept an executor + AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); + } + }; + } + + /** + * Returns the LiveData managed by this class. + * + * @return A LiveData that is controlled by ComputableLiveData. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public LiveData<T> getLiveData() { + return mLiveData; + } + + @VisibleForTesting + final Runnable mRefreshRunnable = new Runnable() { + @WorkerThread + @Override + public void run() { + boolean computed; + do { + computed = false; + // compute can happen only in 1 thread but no reason to lock others. + if (mComputing.compareAndSet(false, true)) { + // as long as it is invalid, keep computing. + try { + T value = null; + while (mInvalid.compareAndSet(true, false)) { + computed = true; + value = compute(); + } + if (computed) { + mLiveData.postValue(value); + } + } finally { + // release compute lock + mComputing.set(false); + } + } + // check invalid after releasing compute lock to avoid the following scenario. + // Thread A runs compute() + // Thread A checks invalid, it is false + // Main thread sets invalid to true + // Thread B runs, fails to acquire compute lock and skips + // Thread A releases compute lock + // We've left invalid in set state. The check below recovers. + } while (computed && mInvalid.get()); + } + }; + + // invalidation check always happens on the main thread + @VisibleForTesting + final Runnable mInvalidationRunnable = new Runnable() { + @MainThread + @Override + public void run() { + boolean isActive = mLiveData.hasActiveObservers(); + if (mInvalid.compareAndSet(false, true)) { + if (isActive) { + // TODO if we make this class public, we should accept an executor. + AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); + } + } + } + }; + + /** + * Invalidates the LiveData. + * <p> + * When there are active observers, this will trigger a call to {@link #compute()}. + */ + public void invalidate() { + AppToolkitTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); + } + + @SuppressWarnings("WeakerAccess") + @WorkerThread + protected abstract T compute(); } diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java index 3aea6acb..99d859c4 100644 --- a/android/arch/lifecycle/LiveData.java +++ b/android/arch/lifecycle/LiveData.java @@ -1,4 +1,411 @@ -//LiveData interface for tests +/* + * Copyright (C) 2017 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 android.arch.lifecycle; -public class LiveData<T> { + +import static android.arch.lifecycle.Lifecycle.State.DESTROYED; +import static android.arch.lifecycle.Lifecycle.State.STARTED; + +import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.internal.SafeIterableMap; +import android.arch.lifecycle.Lifecycle.State; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; + +import java.util.Iterator; +import java.util.Map; + +/** + * LiveData is a data holder class that can be observed within a given lifecycle. + * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and + * this observer will be notified about modifications of the wrapped data only if the paired + * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is + * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via + * {@link #observeForever(Observer)} is considered as always active and thus will be always notified + * about modifications. For those observers, you should manually call + * {@link #removeObserver(Observer)}. + * + * <p> An observer added with a Lifecycle will be automatically removed if the corresponding + * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for + * activities and fragments where they can safely observe LiveData and not worry about leaks: + * they will be instantly unsubscribed when they are destroyed. + * + * <p> + * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods + * to get notified when number of active {@link Observer}s change between 0 and 1. + * This allows LiveData to release any heavy resources when it does not have any Observers that + * are actively observing. + * <p> + * This class is designed to hold individual data fields of {@link ViewModel}, + * but can also be used for sharing data between different modules in your application + * in a decoupled fashion. + * + * @param <T> The type of data hold by this instance + * @see ViewModel + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +// TODO: Thread checks are too strict right now, we may consider automatically moving them to main +// thread. +public abstract class LiveData<T> { + private final Object mDataLock = new Object(); + static final int START_VERSION = -1; + private static final Object NOT_SET = new Object(); + + private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() { + + private LifecycleRegistry mRegistry = init(); + + private LifecycleRegistry init() { + LifecycleRegistry registry = new LifecycleRegistry(this); + registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + registry.handleLifecycleEvent(Lifecycle.Event.ON_START); + registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + return registry; + } + + @Override + public Lifecycle getLifecycle() { + return mRegistry; + } + }; + + private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers = + new SafeIterableMap<>(); + + // how many observers are in active state + private int mActiveCount = 0; + private volatile Object mData = NOT_SET; + // when setData is called, we set the pending data and actual data swap happens on the main + // thread + private volatile Object mPendingData = NOT_SET; + private int mVersion = START_VERSION; + + private boolean mDispatchingValue; + @SuppressWarnings("FieldCanBeLocal") + private boolean mDispatchInvalidated; + private final Runnable mPostValueRunnable = new Runnable() { + @Override + public void run() { + Object newValue; + synchronized (mDataLock) { + newValue = mPendingData; + mPendingData = NOT_SET; + } + //noinspection unchecked + setValue((T) newValue); + } + }; + + private void considerNotify(LifecycleBoundObserver observer) { + if (!observer.active) { + return; + } + // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet. + // + // we still first check observer.active to keep it as the entrance for events. So even if + // the observer moved to an active state, if we've not received that event, we better not + // notify for a more predictable notification order. + if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) { + return; + } + if (observer.lastVersion >= mVersion) { + return; + } + observer.lastVersion = mVersion; + //noinspection unchecked + observer.observer.onChanged((T) mData); + } + + private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) { + if (mDispatchingValue) { + mDispatchInvalidated = true; + return; + } + mDispatchingValue = true; + do { + mDispatchInvalidated = false; + if (initiator != null) { + considerNotify(initiator); + initiator = null; + } else { + for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator = + mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { + considerNotify(iterator.next().getValue()); + if (mDispatchInvalidated) { + break; + } + } + } + } while (mDispatchInvalidated); + mDispatchingValue = false; + } + + /** + * Adds the given observer to the observers list within the lifespan of the given + * owner. The events are dispatched on the main thread. If LiveData already has data + * set, it will be delivered to the observer. + * <p> + * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED} + * or {@link Lifecycle.State#RESUMED} state (active). + * <p> + * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will + * automatically be removed. + * <p> + * When data changes while the {@code owner} is not active, it will not receive any updates. + * If it becomes active again, it will receive the last available data automatically. + * <p> + * LiveData keeps a strong reference to the observer and the owner as long as the + * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to + * the observer & the owner. + * <p> + * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData + * ignores the call. + * <p> + * If the given owner, observer tuple is already in the list, the call is ignored. + * If the observer is already in the list with another owner, LiveData throws an + * {@link IllegalArgumentException}. + * + * @param owner The LifecycleOwner which controls the observer + * @param observer The observer that will receive the events + */ + @MainThread + public void observe(LifecycleOwner owner, Observer<T> observer) { + if (owner.getLifecycle().getCurrentState() == DESTROYED) { + // ignore + return; + } + LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); + LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper); + if (existing != null && existing.owner != wrapper.owner) { + throw new IllegalArgumentException("Cannot add the same observer" + + " with different lifecycles"); + } + if (existing != null) { + return; + } + owner.getLifecycle().addObserver(wrapper); + wrapper.activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())); + } + + /** + * Adds the given observer to the observers list. This call is similar to + * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which + * is always active. This means that the given observer will receive all events and will never + * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop + * observing this LiveData. + * While LiveData has one of such observers, it will be considered + * as active. + * <p> + * If the observer was already added with an owner to this LiveData, LiveData throws an + * {@link IllegalArgumentException}. + * + * @param observer The observer that will receive the events + */ + @MainThread + public void observeForever(Observer<T> observer) { + observe(ALWAYS_ON, observer); + } + + /** + * Removes the given observer from the observers list. + * + * @param observer The Observer to receive events. + */ + @MainThread + public void removeObserver(final Observer<T> observer) { + assertMainThread("removeObserver"); + LifecycleBoundObserver removed = mObservers.remove(observer); + if (removed == null) { + return; + } + removed.owner.getLifecycle().removeObserver(removed); + removed.activeStateChanged(false); + } + + /** + * Removes all observers that are tied to the given {@link LifecycleOwner}. + * + * @param owner The {@code LifecycleOwner} scope for the observers to be removed. + */ + @MainThread + public void removeObservers(final LifecycleOwner owner) { + assertMainThread("removeObservers"); + for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) { + if (entry.getValue().owner == owner) { + removeObserver(entry.getKey()); + } + } + } + + /** + * Posts a task to a main thread to set the given value. So if you have a following code + * executed in the main thread: + * <pre class="prettyprint"> + * liveData.postValue("a"); + * liveData.setValue("b"); + * </pre> + * The value "b" would be set at first and later the main thread would override it with + * the value "a". + * <p> + * If you called this method multiple times before a main thread executed a posted task, only + * the last value would be dispatched. + * + * @param value The new value + */ + protected void postValue(T value) { + boolean postTask; + synchronized (mDataLock) { + postTask = mPendingData == NOT_SET; + mPendingData = value; + } + if (!postTask) { + return; + } + AppToolkitTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); + } + + /** + * Sets the value. If there are active observers, the value will be dispatched to them. + * <p> + * This method must be called from the main thread. If you need set a value from a background + * thread, you can use {@link #postValue(Object)} + * + * @param value The new value + */ + @MainThread + protected void setValue(T value) { + assertMainThread("setValue"); + mVersion++; + mData = value; + dispatchingValue(null); + } + + /** + * Returns the current value. + * Note that calling this method on a background thread does not guarantee that the latest + * value set will be received. + * + * @return the current value + */ + @Nullable + public T getValue() { + Object data = mData; + if (data != NOT_SET) { + //noinspection unchecked + return (T) data; + } + return null; + } + + int getVersion() { + return mVersion; + } + + /** + * Called when the number of active observers change to 1 from 0. + * <p> + * This callback can be used to know that this LiveData is being used thus should be kept + * up to date. + */ + protected void onActive() { + + } + + /** + * Called when the number of active observers change from 1 to 0. + * <p> + * This does not mean that there are no observers left, there may still be observers but their + * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED} + * (like an Activity in the back stack). + * <p> + * You can check if there are observers via {@link #hasObservers()}. + */ + protected void onInactive() { + + } + + /** + * Returns true if this LiveData has observers. + * + * @return true if this LiveData has observers + */ + public boolean hasObservers() { + return mObservers.size() > 0; + } + + /** + * Returns true if this LiveData has active observers. + * + * @return true if this LiveData has active observers + */ + public boolean hasActiveObservers() { + return mActiveCount > 0; + } + + class LifecycleBoundObserver implements LifecycleObserver { + public final LifecycleOwner owner; + public final Observer<T> observer; + public boolean active; + public int lastVersion = START_VERSION; + + LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) { + this.owner = owner; + this.observer = observer; + } + + @SuppressWarnings("unused") + @OnLifecycleEvent(Lifecycle.Event.ON_ANY) + void onStateChange() { + if (owner.getLifecycle().getCurrentState() == DESTROYED) { + removeObserver(observer); + return; + } + // immediately set active state, so we'd never dispatch anything to inactive + // owner + activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())); + + } + + void activeStateChanged(boolean newActive) { + if (newActive == active) { + return; + } + active = newActive; + boolean wasInactive = LiveData.this.mActiveCount == 0; + LiveData.this.mActiveCount += active ? 1 : -1; + if (wasInactive && active) { + onActive(); + } + if (LiveData.this.mActiveCount == 0 && !active) { + onInactive(); + } + if (active) { + dispatchingValue(this); + } + } + } + + static boolean isActiveState(State state) { + return state.isAtLeast(STARTED); + } + + private void assertMainThread(String methodName) { + if (!AppToolkitTaskExecutor.getInstance().isMainThread()) { + throw new IllegalStateException("Cannot invoke " + methodName + " on a background" + + " thread"); + } + } } diff --git a/android/arch/paging/LivePagedListProvider.java b/android/arch/paging/LivePagedListProvider.java index 821eac20..b7c68dd6 100644 --- a/android/arch/paging/LivePagedListProvider.java +++ b/android/arch/paging/LivePagedListProvider.java @@ -16,112 +16,5 @@ package android.arch.paging; -import android.arch.core.executor.AppToolkitTaskExecutor; -import android.arch.lifecycle.ComputableLiveData; -import android.arch.lifecycle.LiveData; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; - -/** - * Provides a {@code LiveData<PagedList>}, given a means to construct a DataSource. - * <p> - * Return type for data-loading system of an application or library to produce a - * {@code LiveData<PagedList>}, while leaving the details of the paging mechanism up to the - * consumer. - * - * @param <Key> Tyep of input valued used to load data from the DataSource. Must be integer if - * you're using TiledDataSource. - * @param <Value> Data type produced by the DataSource, and held by the PagedLists. - * - * @see PagedListAdapter - * @see DataSource - * @see PagedList - */ -public abstract class LivePagedListProvider<Key, Value> { - - /** - * Construct a new data source to be wrapped in a new PagedList, which will be returned - * through the LiveData. - * - * @return The data source. - */ - @WorkerThread - protected abstract DataSource<Key, Value> createDataSource(); - - /** - * Creates a LiveData of PagedLists, given the page size. - * <p> - * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a - * {@link android.support.v7.widget.RecyclerView}. - * - * @param initialLoadKey Initial key used to load initial data from the data source. - * @param pageSize Page size defining how many items are loaded from a data source at a time. - * Recommended to be multiple times the size of item displayed at once. - * - * @return The LiveData of PagedLists. - */ - public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey, int pageSize) { - return create(initialLoadKey, - new PagedList.Config.Builder() - .setPageSize(pageSize) - .build()); - } - - /** - * Creates a LiveData of PagedLists, given the PagedList.Config. - * <p> - * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a - * {@link android.support.v7.widget.RecyclerView}. - * - * @param initialLoadKey Initial key to pass to the data source to initialize data with. - * @param config PagedList.Config to use with created PagedLists. This specifies how the - * lists will load data. - * - * @return The LiveData of PagedLists. - */ - public LiveData<PagedList<Value>> create(@Nullable final Key initialLoadKey, - final PagedList.Config config) { - return new ComputableLiveData<PagedList<Value>>() { - @Nullable - private PagedList<Value> mList; - @Nullable - private DataSource<Key, Value> mDataSource; - - private final DataSource.InvalidatedCallback mCallback = - new DataSource.InvalidatedCallback() { - @Override - public void onInvalidated() { - invalidate(); - } - }; - - @Override - protected PagedList<Value> compute() { - @Nullable Key initializeKey = initialLoadKey; - if (mList != null) { - //noinspection unchecked - initializeKey = (Key) mList.getLastKey(); - } - - do { - if (mDataSource != null) { - mDataSource.removeInvalidatedCallback(mCallback); - } - - mDataSource = createDataSource(); - mDataSource.addInvalidatedCallback(mCallback); - - mList = new PagedList.Builder<Key, Value>() - .setDataSource(mDataSource) - .setMainThreadExecutor(AppToolkitTaskExecutor.getMainThreadExecutor()) - .setBackgroundThreadExecutor( - AppToolkitTaskExecutor.getIOThreadExecutor()) - .setConfig(config) - .setInitialKey(initializeKey) - .build(); - } while (mList.isDetached()); - return mList; - } - }.getLiveData(); - } -} +abstract public class LivePagedListProvider<K, T> { +}
\ No newline at end of file diff --git a/android/arch/paging/PagedListAdapter.java b/android/arch/paging/PagedListAdapter.java index 73c1b647..19a0c558 100644 --- a/android/arch/paging/PagedListAdapter.java +++ b/android/arch/paging/PagedListAdapter.java @@ -44,7 +44,7 @@ import android.support.v7.widget.RecyclerView; * {@literal @}Dao * interface UserDao { * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC") - * public abstract LivePagedListProvider<User> usersByLastName(); + * public abstract LivePagedListProvider<Integer, User> usersByLastName(); * } * * class MyViewModel extends ViewModel { @@ -58,7 +58,7 @@ import android.support.v7.widget.RecyclerView; * } * } * - * class MyActivity extends Activity implements LifecycleRegistryOwner { + * class MyActivity extends AppCompatActivity { * {@literal @}Override * public void onCreate(Bundle savedState) { * super.onCreate(savedState); diff --git a/android/arch/paging/PagedListAdapterHelper.java b/android/arch/paging/PagedListAdapterHelper.java index 7d7a9222..4bff5fcf 100644 --- a/android/arch/paging/PagedListAdapterHelper.java +++ b/android/arch/paging/PagedListAdapterHelper.java @@ -50,7 +50,7 @@ import java.util.List; * {@literal @}Dao * interface UserDao { * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC") - * public abstract LivePagedListProvider<User> usersByLastName(); + * public abstract LivePagedListProvider<Integer, User> usersByLastName(); * } * * class MyViewModel extends ViewModel { @@ -64,7 +64,7 @@ import java.util.List; * } * } * - * class MyActivity extends Activity implements LifecycleRegistryOwner { + * class MyActivity extends AppCompatActivity { * {@literal @}Override * public void onCreate(Bundle savedState) { * super.onCreate(savedState); diff --git a/android/arch/paging/integration/testapp/PagedListSampleActivity.java b/android/arch/paging/integration/testapp/PagedListSampleActivity.java index f0022186..5d0117d7 100644 --- a/android/arch/paging/integration/testapp/PagedListSampleActivity.java +++ b/android/arch/paging/integration/testapp/PagedListSampleActivity.java @@ -17,7 +17,6 @@ package android.arch.paging.integration.testapp; import android.arch.lifecycle.LifecycleRegistry; -import android.arch.lifecycle.LifecycleRegistryOwner; import android.arch.lifecycle.Observer; import android.arch.lifecycle.ViewModelProviders; import android.arch.paging.PagedList; @@ -31,7 +30,7 @@ import android.widget.Button; /** * Sample NullPaddedList activity with artificial data source. */ -public class PagedListSampleActivity extends AppCompatActivity implements LifecycleRegistryOwner { +public class PagedListSampleActivity extends AppCompatActivity { @Override protected void onCreate(final Bundle savedInstanceState) { diff --git a/android/arch/persistence/db/SimpleSQLiteQuery.java b/android/arch/persistence/db/SimpleSQLiteQuery.java index b8211760..e2a38294 100644 --- a/android/arch/persistence/db/SimpleSQLiteQuery.java +++ b/android/arch/persistence/db/SimpleSQLiteQuery.java @@ -20,7 +20,7 @@ package android.arch.persistence.db; * A basic implemtation of {@link SupportSQLiteQuery} which receives a query and its args and binds * args based on the passed in Object type. */ -public class SimpleSQLiteQuery implements SupportSQLiteQuery { +public final class SimpleSQLiteQuery implements SupportSQLiteQuery { private final String mQuery; private final Object[] mBindArgs; diff --git a/android/arch/persistence/db/SupportSQLiteQueryBuilder.java b/android/arch/persistence/db/SupportSQLiteQueryBuilder.java index 183fb0a3..52957a31 100644 --- a/android/arch/persistence/db/SupportSQLiteQueryBuilder.java +++ b/android/arch/persistence/db/SupportSQLiteQueryBuilder.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; * A simple query builder to create SQL SELECT queries. */ @SuppressWarnings("unused") -public class SupportSQLiteQueryBuilder { +public final class SupportSQLiteQueryBuilder { private static final Pattern sLimitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java index 7b4245b7..2268f45f 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java @@ -23,7 +23,7 @@ import android.arch.persistence.db.SupportSQLiteOpenHelper; * framework. */ @SuppressWarnings("unused") -public class FrameworkSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { +public final class FrameworkSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { @Override public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) { return new FrameworkSQLiteOpenHelper( diff --git a/android/arch/persistence/room/ColumnInfo.java b/android/arch/persistence/room/ColumnInfo.java index 84f5844e..65da379c 100644 --- a/android/arch/persistence/room/ColumnInfo.java +++ b/android/arch/persistence/room/ColumnInfo.java @@ -33,6 +33,7 @@ import java.lang.annotation.Target; public @interface ColumnInfo { /** * Name of the column in the database. Defaults to the field name if not set. + * * @return Name of the column in the database. */ String name() default INHERIT_FIELD_NAME; @@ -40,13 +41,14 @@ public @interface ColumnInfo { /** * The type affinity for the column, which will be used when constructing the database. * <p> - * If it is not specified, Room resolves it based on the field's type and available - * TypeConverters. + * If it is not specified, the value defaults to {@link #UNDEFINED} and Room resolves it based + * on the field's type and available TypeConverters. * <p> * See <a href="https://www.sqlite.org/datatype3.html">SQLite types documentation</a> for * details. * - * @return The type affinity of the column. + * @return The type affinity of the column. This is either {@link #UNDEFINED}, {@link #TEXT}, + * {@link #INTEGER}, {@link #REAL}, or {@link #BLOB}. */ @SuppressWarnings("unused") @SQLiteTypeAffinity int typeAffinity() default UNDEFINED; @@ -60,6 +62,17 @@ public @interface ColumnInfo { boolean index() default false; /** + * The collation sequence for the column, which will be used when constructing the database. + * <p> + * The default value is {@link #UNSPECIFIED}. In that case, Room does not add any + * collation sequence to the column, and SQLite treats it like {@link #BINARY}. + * + * @return The collation sequence of the column. This is either {@link #UNSPECIFIED}, + * {@link #BINARY}, {@link #NOCASE}, or {@link #RTRIM}. + */ + @Collate int collate() default UNSPECIFIED; + + /** * Constant to let Room inherit the field name as the column name. If used, Room will use the * field name as the column name. */ @@ -67,22 +80,32 @@ public @interface ColumnInfo { /** * Undefined type affinity. Will be resolved based on the type. + * + * @see #typeAffinity() */ int UNDEFINED = 1; /** * Column affinity constant for strings. + * + * @see #typeAffinity() */ int TEXT = 2; /** * Column affinity constant for integers or booleans. + * + * @see #typeAffinity() */ int INTEGER = 3; /** * Column affinity constant for floats or doubles. + * + * @see #typeAffinity() */ int REAL = 4; /** * Column affinity constant for binary data. + * + * @see #typeAffinity() */ int BLOB = 5; @@ -92,4 +115,34 @@ public @interface ColumnInfo { @IntDef({UNDEFINED, TEXT, INTEGER, REAL, BLOB}) @interface SQLiteTypeAffinity { } + + /** + * Collation sequence is not specified. The match will behave like {@link #BINARY}. + * + * @see #collate() + */ + int UNSPECIFIED = 1; + /** + * Collation sequence for case-sensitive match. + * + * @see #collate() + */ + int BINARY = 2; + /** + * Collation sequence for case-insensitive match. + * + * @see #collate() + */ + int NOCASE = 3; + /** + * Collation sequence for case-sensitive match except that trailing space characters are + * ignored. + * + * @see #collate() + */ + int RTRIM = 4; + + @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM}) + @interface Collate { + } } diff --git a/android/arch/persistence/room/Relation.java b/android/arch/persistence/room/Relation.java index 0d2f1527..72066992 100644 --- a/android/arch/persistence/room/Relation.java +++ b/android/arch/persistence/room/Relation.java @@ -41,7 +41,7 @@ import java.lang.annotation.Target; * * {@literal @}Dao * public interface UserPetDao { - * {@literal @}Query("SELECT id, name from User WHERE age >e; ?") + * {@literal @}Query("SELECT id, name from User WHERE age > :minAge") * public List<UserNameAndAllPets> loadUserAndPets(int minAge); * } * </pre> @@ -67,7 +67,7 @@ import java.lang.annotation.Target; * } * {@literal @}Dao * public interface UserPetDao { - * {@literal @}Query("SELECT * from User WHERE age >e; ?") + * {@literal @}Query("SELECT * from User WHERE age > :minAge") * public List<UserAllPets> loadUserAndPets(int minAge); * } * </pre> diff --git a/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java b/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java index 56508292..818c46b4 100644 --- a/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java +++ b/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java @@ -17,7 +17,6 @@ package android.arch.persistence.room.integration.testapp; import android.arch.lifecycle.LifecycleRegistry; -import android.arch.lifecycle.LifecycleRegistryOwner; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.Observer; import android.arch.lifecycle.ViewModelProviders; @@ -35,7 +34,7 @@ import android.widget.Button; /** * Sample PagedList activity which uses Room. */ -public class RoomPagedListActivity extends AppCompatActivity implements LifecycleRegistryOwner { +public class RoomPagedListActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private PagedListCustomerAdapter mAdapter; diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java index 428d87c3..337c233f 100644 --- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java @@ -59,6 +59,9 @@ public abstract class UserDao { @Query("select * from user where mId IN(:ids)") public abstract User[] loadByIds(int... ids); + @Query("select * from user where custommm = :customField") + public abstract List<User> findByCustomField(String customField); + @Insert public abstract void insert(User user); diff --git a/android/arch/persistence/room/integration/testapp/migration/MigrationDb.java b/android/arch/persistence/room/integration/testapp/migration/MigrationDb.java index 4a95ad85..ef207cf2 100644 --- a/android/arch/persistence/room/integration/testapp/migration/MigrationDb.java +++ b/android/arch/persistence/room/integration/testapp/migration/MigrationDb.java @@ -17,6 +17,7 @@ package android.arch.persistence.room.integration.testapp.migration; import android.arch.persistence.db.SupportSQLiteDatabase; +import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Database; import android.arch.persistence.room.Entity; @@ -75,6 +76,7 @@ public abstract class MigrationDb extends RoomDatabase { public static final String TABLE_NAME = "Entity4"; @PrimaryKey public int id; + @ColumnInfo(collate = ColumnInfo.NOCASE) public String name; } diff --git a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java index aa297ed4..725d53f8 100644 --- a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java +++ b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java @@ -315,7 +315,7 @@ public class MigrationTest { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE IF NOT EXISTS " + MigrationDb.Entity4.TABLE_NAME - + " (`id` INTEGER NOT NULL, `name` TEXT, PRIMARY KEY(`id`)," + + " (`id` INTEGER NOT NULL, `name` TEXT COLLATE NOCASE, PRIMARY KEY(`id`)," + " FOREIGN KEY(`name`) REFERENCES `Entity1`(`name`)" + " ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)"); } diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java index 2b4a0e99..8861adbc 100644 --- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java +++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java @@ -265,6 +265,16 @@ public class SimpleEntityReadWriteTest { } @Test + public void findByCollateNoCase() { + User user = TestUtil.createUser(3); + user.setCustomField("abc"); + mUserDao.insert(user); + List<User> users = mUserDao.findByCustomField("ABC"); + assertThat(users, hasSize(1)); + assertThat(users.get(0).getId(), is(3)); + } + + @Test public void deleteByAge() { User user1 = TestUtil.createUser(3); user1.setAge(30); diff --git a/android/arch/persistence/room/integration/testapp/vo/User.java b/android/arch/persistence/room/integration/testapp/vo/User.java index 57cf585e..a5b88394 100644 --- a/android/arch/persistence/room/integration/testapp/vo/User.java +++ b/android/arch/persistence/room/integration/testapp/vo/User.java @@ -43,7 +43,7 @@ public class User { private Date mBirthday; - @ColumnInfo(name = "custommm") + @ColumnInfo(name = "custommm", collate = ColumnInfo.NOCASE) private String mCustomField; public int getId() { diff --git a/android/bluetooth/BluetoothGatt.java b/android/bluetooth/BluetoothGatt.java index 759d7729..a2af3422 100644 --- a/android/bluetooth/BluetoothGatt.java +++ b/android/bluetooth/BluetoothGatt.java @@ -42,7 +42,7 @@ public final class BluetoothGatt implements BluetoothProfile { private static final boolean VDBG = false; private IBluetoothGatt mService; - private BluetoothGattCallback mCallback; + private volatile BluetoothGattCallback mCallback; private Handler mHandler; private int mClientIf; private BluetoothDevice mDevice; @@ -164,8 +164,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onConnectionStateChange(BluetoothGatt.this, + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onConnectionStateChange(BluetoothGatt.this, GATT_FAILURE, BluetoothProfile.STATE_DISCONNECTED); } @@ -203,8 +204,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onPhyUpdate(BluetoothGatt.this, txPhy, rxPhy, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onPhyUpdate(BluetoothGatt.this, txPhy, rxPhy, status); } } }); @@ -227,8 +229,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onPhyRead(BluetoothGatt.this, txPhy, rxPhy, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onPhyRead(BluetoothGatt.this, txPhy, rxPhy, status); } } }); @@ -254,8 +257,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onConnectionStateChange(BluetoothGatt.this, status, + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onConnectionStateChange(BluetoothGatt.this, status, profileState); } } @@ -307,8 +311,7 @@ public final class BluetoothGatt implements BluetoothProfile { for (BluetoothGattService brokenRef : includedServices) { BluetoothGattService includedService = getService(mDevice, - brokenRef.getUuid(), brokenRef.getInstanceId(), - brokenRef.getType()); + brokenRef.getUuid(), brokenRef.getInstanceId()); if (includedService != null) { fixedService.addIncludedService(includedService); } else { @@ -320,8 +323,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onServicesDiscovered(BluetoothGatt.this, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onServicesDiscovered(BluetoothGatt.this, status); } } }); @@ -371,13 +375,13 @@ public final class BluetoothGatt implements BluetoothProfile { return; } - if (status == 0) characteristic.setValue(value); - runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onCharacteristicRead(BluetoothGatt.this, characteristic, + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + if (status == 0) characteristic.setValue(value); + callback.onCharacteristicRead(BluetoothGatt.this, characteristic, status); } } @@ -429,8 +433,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onCharacteristicWrite(BluetoothGatt.this, characteristic, + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onCharacteristicWrite(BluetoothGatt.this, characteristic, status); } } @@ -454,13 +459,13 @@ public final class BluetoothGatt implements BluetoothProfile { handle); if (characteristic == null) return; - characteristic.setValue(value); - runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onCharacteristicChanged(BluetoothGatt.this, + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + characteristic.setValue(value); + callback.onCharacteristicChanged(BluetoothGatt.this, characteristic); } } @@ -489,7 +494,6 @@ public final class BluetoothGatt implements BluetoothProfile { BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle); if (descriptor == null) return; - if (status == 0) descriptor.setValue(value); if ((status == GATT_INSUFFICIENT_AUTHENTICATION || status == GATT_INSUFFICIENT_ENCRYPTION) @@ -510,8 +514,10 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onDescriptorRead(BluetoothGatt.this, descriptor, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + if (status == 0) descriptor.setValue(value); + callback.onDescriptorRead(BluetoothGatt.this, descriptor, status); } } }); @@ -559,8 +565,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onDescriptorWrite(BluetoothGatt.this, descriptor, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onDescriptorWrite(BluetoothGatt.this, descriptor, status); } } }); @@ -587,8 +594,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onReliableWriteCompleted(BluetoothGatt.this, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onReliableWriteCompleted(BluetoothGatt.this, status); } } }); @@ -610,8 +618,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onReadRemoteRssi(BluetoothGatt.this, rssi, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onReadRemoteRssi(BluetoothGatt.this, rssi, status); } } }); @@ -634,8 +643,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onMtuChanged(BluetoothGatt.this, mtu, status); + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onMtuChanged(BluetoothGatt.this, mtu, status); } } }); @@ -660,8 +670,9 @@ public final class BluetoothGatt implements BluetoothProfile { runOrQueueCallback(new Runnable() { @Override public void run() { - if (mCallback != null) { - mCallback.onConnectionUpdated(BluetoothGatt.this, interval, latency, + final BluetoothGattCallback callback = mCallback; + if (callback != null) { + callback.onConnectionUpdated(BluetoothGatt.this, interval, latency, timeout, status); } } @@ -702,10 +713,9 @@ public final class BluetoothGatt implements BluetoothProfile { * @hide */ /*package*/ BluetoothGattService getService(BluetoothDevice device, UUID uuid, - int instanceId, int type) { + int instanceId) { for (BluetoothGattService svc : mServices) { if (svc.getDevice().equals(device) - && svc.getType() == type && svc.getInstanceId() == instanceId && svc.getUuid().equals(uuid)) { return svc; @@ -901,7 +911,7 @@ public final class BluetoothGatt implements BluetoothProfile { /** * Set the preferred connection PHY for this app. Please note that this is just a - * recommendation, whether the PHY change will happen depends on other applications peferences, + * recommendation, whether the PHY change will happen depends on other applications preferences, * local and remote controller capabilities. Controller can override these settings. * <p> * {@link BluetoothGattCallback#onPhyUpdate} will be triggered as a result of this call, even diff --git a/android/bluetooth/BluetoothGattServerCallback.java b/android/bluetooth/BluetoothGattServerCallback.java index 22eba351..2c8114be 100644 --- a/android/bluetooth/BluetoothGattServerCallback.java +++ b/android/bluetooth/BluetoothGattServerCallback.java @@ -184,7 +184,7 @@ public abstract class BluetoothGattServerCallback { /** * Callback indicating the connection parameters were updated. * - * @param gatt The remote device involved + * @param device The remote device involved * @param interval Connection interval used on this connection, 1.25ms unit. Valid range is from * 6 (7.5ms) to 3200 (4000ms). * @param latency Slave latency for the connection in number of connection events. Valid range @@ -195,7 +195,7 @@ public abstract class BluetoothGattServerCallback { * successfully * @hide */ - public void onConnectionUpdated(BluetoothDevice gatt, int interval, int latency, int timeout, + public void onConnectionUpdated(BluetoothDevice device, int interval, int latency, int timeout, int status) { } diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java index 664bcbca..d73f8526 100644 --- a/android/content/pm/ApplicationInfo.java +++ b/android/content/pm/ApplicationInfo.java @@ -586,24 +586,32 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public static final int PRIVATE_FLAG_VIRTUAL_PRELOAD = 1 << 16; + /** + * Value for {@linl #privateFlags}: whether this app is pre-installed on the + * OEM partition of the system image. + * @hide + */ + public static final int PRIVATE_FLAG_OEM = 1 << 17; + /** @hide */ @IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = { - PRIVATE_FLAG_HIDDEN, + PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE, + PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION, + PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE, + PRIVATE_FLAG_BACKUP_IN_FOREGROUND, PRIVATE_FLAG_CANT_SAVE_STATE, - PRIVATE_FLAG_FORWARD_LOCK, - PRIVATE_FLAG_PRIVILEGED, - PRIVATE_FLAG_HAS_DOMAIN_URLS, PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE, PRIVATE_FLAG_DIRECT_BOOT_AWARE, + PRIVATE_FLAG_FORWARD_LOCK, + PRIVATE_FLAG_HAS_DOMAIN_URLS, + PRIVATE_FLAG_HIDDEN, PRIVATE_FLAG_INSTANT, + PRIVATE_FLAG_ISOLATED_SPLIT_LOADING, + PRIVATE_FLAG_OEM, PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE, + PRIVATE_FLAG_PRIVILEGED, PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER, - PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE, - PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE, - PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION, - PRIVATE_FLAG_BACKUP_IN_FOREGROUND, PRIVATE_FLAG_STATIC_SHARED_LIBRARY, - PRIVATE_FLAG_ISOLATED_SPLIT_LOADING, PRIVATE_FLAG_VIRTUAL_PRELOAD, }) @Retention(RetentionPolicy.SOURCE) @@ -1557,6 +1565,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { /** * @hide */ + public boolean isOem() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0; + } + + /** + * @hide + */ @Override protected ApplicationInfo getApplicationInfo() { return this; } diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java index 8b54b0d8..6c7c8a07 100644 --- a/android/content/pm/PackageParser.java +++ b/android/content/pm/PackageParser.java @@ -96,8 +96,8 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; import libcore.io.IoUtils; - import libcore.util.EmptyArray; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -815,21 +815,22 @@ public class PackageParser { } } - public final static int PARSE_IS_SYSTEM = 1<<0; - public final static int PARSE_CHATTY = 1<<1; - public final static int PARSE_MUST_BE_APK = 1<<2; - public final static int PARSE_IGNORE_PROCESSES = 1<<3; - public final static int PARSE_FORWARD_LOCK = 1<<4; - public final static int PARSE_EXTERNAL_STORAGE = 1<<5; - public final static int PARSE_IS_SYSTEM_DIR = 1<<6; - public final static int PARSE_IS_PRIVILEGED = 1<<7; - public final static int PARSE_COLLECT_CERTIFICATES = 1<<8; - public final static int PARSE_TRUSTED_OVERLAY = 1<<9; - public final static int PARSE_ENFORCE_CODE = 1<<10; + public static final int PARSE_IS_SYSTEM = 1 << 0; + public static final int PARSE_CHATTY = 1 << 1; + public static final int PARSE_MUST_BE_APK = 1 << 2; + public static final int PARSE_IGNORE_PROCESSES = 1 << 3; + public static final int PARSE_FORWARD_LOCK = 1 << 4; + public static final int PARSE_EXTERNAL_STORAGE = 1 << 5; + public static final int PARSE_IS_SYSTEM_DIR = 1 << 6; + public static final int PARSE_IS_PRIVILEGED = 1 << 7; + public static final int PARSE_COLLECT_CERTIFICATES = 1 << 8; + public static final int PARSE_TRUSTED_OVERLAY = 1 << 9; + public static final int PARSE_ENFORCE_CODE = 1 << 10; /** @deprecated remove when fixing b/34761192 */ @Deprecated - public final static int PARSE_IS_EPHEMERAL = 1<<11; - public final static int PARSE_FORCE_SDK = 1<<12; + public static final int PARSE_IS_EPHEMERAL = 1 << 11; + public static final int PARSE_FORCE_SDK = 1 << 12; + public static final int PARSE_IS_OEM = 1 << 13; private static final Comparator<String> sSplitNameComparator = new SplitNameComparator(); diff --git a/android/content/pm/PermissionInfo.java b/android/content/pm/PermissionInfo.java index b84c1b93..17b4f871 100644 --- a/android/content/pm/PermissionInfo.java +++ b/android/content/pm/PermissionInfo.java @@ -17,7 +17,6 @@ package android.content.pm; import android.annotation.SystemApi; -import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -135,6 +134,16 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { public static final int PROTECTION_FLAG_RUNTIME_ONLY = 0x2000; /** + * Additional flag for {@link #protectionLevel}, corresponding + * to the <code>oem</code> value of + * {@link android.R.attr#protectionLevel}. + * + * @hide + */ + @SystemApi + public static final int PROTECTION_FLAG_OEM = 0x4000; + + /** * Mask for {@link #protectionLevel}: the basic protection type. */ public static final int PROTECTION_MASK_BASE = 0xf; @@ -222,7 +231,7 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { /** @hide */ public static String protectionToString(int level) { String protLevel = "????"; - switch (level&PROTECTION_MASK_BASE) { + switch (level & PROTECTION_MASK_BASE) { case PermissionInfo.PROTECTION_DANGEROUS: protLevel = "dangerous"; break; @@ -236,36 +245,39 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { protLevel = "signatureOrSystem"; break; } - if ((level&PermissionInfo.PROTECTION_FLAG_PRIVILEGED) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_PRIVILEGED) != 0) { protLevel += "|privileged"; } - if ((level&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { protLevel += "|development"; } - if ((level&PermissionInfo.PROTECTION_FLAG_APPOP) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_APPOP) != 0) { protLevel += "|appop"; } - if ((level&PermissionInfo.PROTECTION_FLAG_PRE23) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_PRE23) != 0) { protLevel += "|pre23"; } - if ((level&PermissionInfo.PROTECTION_FLAG_INSTALLER) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_INSTALLER) != 0) { protLevel += "|installer"; } - if ((level&PermissionInfo.PROTECTION_FLAG_VERIFIER) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_VERIFIER) != 0) { protLevel += "|verifier"; } - if ((level&PermissionInfo.PROTECTION_FLAG_PREINSTALLED) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_PREINSTALLED) != 0) { protLevel += "|preinstalled"; } - if ((level&PermissionInfo.PROTECTION_FLAG_SETUP) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_SETUP) != 0) { protLevel += "|setup"; } - if ((level&PermissionInfo.PROTECTION_FLAG_INSTANT) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_INSTANT) != 0) { protLevel += "|instant"; } - if ((level&PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) != 0) { protLevel += "|runtime"; } + if ((level & PermissionInfo.PROTECTION_FLAG_OEM) != 0) { + protLevel += "|oem"; + } return protLevel; } diff --git a/android/graphics/BitmapFactory.java b/android/graphics/BitmapFactory.java index 3b272c8d..ffb39e33 100644 --- a/android/graphics/BitmapFactory.java +++ b/android/graphics/BitmapFactory.java @@ -433,10 +433,15 @@ public class BitmapFactory { static void validate(Options opts) { if (opts == null) return; - if (opts.inMutable && opts.inPreferredConfig == Bitmap.Config.HARDWARE) { + if (opts.inBitmap != null && opts.inBitmap.getConfig() == Bitmap.Config.HARDWARE) { throw new IllegalArgumentException("Bitmaps with Config.HARWARE are always immutable"); } + if (opts.inMutable && opts.inPreferredConfig == Bitmap.Config.HARDWARE) { + throw new IllegalArgumentException("Bitmaps with Config.HARDWARE cannot be " + + "decoded into - they are immutable"); + } + if (opts.inPreferredColorSpace != null) { if (!(opts.inPreferredColorSpace instanceof ColorSpace.Rgb)) { throw new IllegalArgumentException("The destination color space must use the " + diff --git a/android/graphics/drawable/VectorDrawable.java b/android/graphics/drawable/VectorDrawable.java index c3ef4508..ceac3253 100644 --- a/android/graphics/drawable/VectorDrawable.java +++ b/android/graphics/drawable/VectorDrawable.java @@ -31,6 +31,7 @@ import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.Shader; +import android.os.Trace; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -605,38 +606,44 @@ public class VectorDrawable extends Drawable { public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { - if (mVectorState.mRootGroup != null || mVectorState.mNativeTree != null) { - // This VD has been used to display other VD resource content, clean up. - if (mVectorState.mRootGroup != null) { - // Subtract the native allocation for all the nodes. - VMRuntime.getRuntime().registerNativeFree(mVectorState.mRootGroup.getNativeSize()); - // Remove child nodes' reference to tree - mVectorState.mRootGroup.setTree(null); - } - mVectorState.mRootGroup = new VGroup(); - if (mVectorState.mNativeTree != null) { - // Subtract the native allocation for the tree wrapper, which contains root node - // as well as rendering related data. - VMRuntime.getRuntime().registerNativeFree(mVectorState.NATIVE_ALLOCATION_SIZE); - mVectorState.mNativeTree.release(); + try { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "VectorDrawable#inflate"); + if (mVectorState.mRootGroup != null || mVectorState.mNativeTree != null) { + // This VD has been used to display other VD resource content, clean up. + if (mVectorState.mRootGroup != null) { + // Subtract the native allocation for all the nodes. + VMRuntime.getRuntime().registerNativeFree( + mVectorState.mRootGroup.getNativeSize()); + // Remove child nodes' reference to tree + mVectorState.mRootGroup.setTree(null); + } + mVectorState.mRootGroup = new VGroup(); + if (mVectorState.mNativeTree != null) { + // Subtract the native allocation for the tree wrapper, which contains root node + // as well as rendering related data. + VMRuntime.getRuntime().registerNativeFree(mVectorState.NATIVE_ALLOCATION_SIZE); + mVectorState.mNativeTree.release(); + } + mVectorState.createNativeTree(mVectorState.mRootGroup); } - mVectorState.createNativeTree(mVectorState.mRootGroup); - } - final VectorDrawableState state = mVectorState; - state.setDensity(Drawable.resolveDensity(r, 0)); + final VectorDrawableState state = mVectorState; + state.setDensity(Drawable.resolveDensity(r, 0)); - final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawable); - updateStateFromTypedArray(a); - a.recycle(); + final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawable); + updateStateFromTypedArray(a); + a.recycle(); - mDpiScaledDirty = true; + mDpiScaledDirty = true; - state.mCacheDirty = true; - inflateChildElements(r, parser, attrs, theme); + state.mCacheDirty = true; + inflateChildElements(r, parser, attrs, theme); - state.onTreeConstructionFinished(); - // Update local properties. - updateLocalState(r); + state.onTreeConstructionFinished(); + // Update local properties. + updateLocalState(r); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } } private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java index 15754574..186b2650 100644 --- a/android/media/AudioManager.java +++ b/android/media/AudioManager.java @@ -3058,7 +3058,11 @@ public class AudioManager { private final IPlaybackConfigDispatcher mPlayCb = new IPlaybackConfigDispatcher.Stub() { @Override - public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs) { + public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs, + boolean flush) { + if (flush) { + Binder.flushPendingCommands(); + } synchronized(mPlaybackCallbackLock) { if (mPlaybackCallbackList != null) { for (int i=0 ; i < mPlaybackCallbackList.size() ; i++) { diff --git a/android/media/AudioPlaybackConfiguration.java b/android/media/AudioPlaybackConfiguration.java index 14bc5551..8a36f91c 100644 --- a/android/media/AudioPlaybackConfiguration.java +++ b/android/media/AudioPlaybackConfiguration.java @@ -212,8 +212,10 @@ public final class AudioPlaybackConfiguration implements Parcelable { * @hide */ public void init() { - if (mIPlayerShell != null) { - mIPlayerShell.monitorDeath(); + synchronized (this) { + if (mIPlayerShell != null) { + mIPlayerShell.monitorDeath(); + } } } @@ -322,7 +324,11 @@ public final class AudioPlaybackConfiguration implements Parcelable { */ @SystemApi public PlayerProxy getPlayerProxy() { - return mIPlayerShell == null ? null : new PlayerProxy(this); + final IPlayerShell ips; + synchronized (this) { + ips = mIPlayerShell; + } + return ips == null ? null : new PlayerProxy(this); } /** @@ -330,7 +336,11 @@ public final class AudioPlaybackConfiguration implements Parcelable { * @return the IPlayer interface for the associated player */ IPlayer getIPlayer() { - return mIPlayerShell == null ? null : mIPlayerShell.getIPlayer(); + final IPlayerShell ips; + synchronized (this) { + ips = mIPlayerShell; + } + return ips == null ? null : ips.getIPlayer(); } /** @@ -351,10 +361,14 @@ public final class AudioPlaybackConfiguration implements Parcelable { * @return true if the state changed, false otherwise */ public boolean handleStateEvent(int event) { - final boolean changed = (mPlayerState != event); - mPlayerState = event; - if ((event == PLAYER_STATE_RELEASED) && (mIPlayerShell != null)) { - mIPlayerShell.release(); + final boolean changed; + synchronized (this) { + changed = (mPlayerState != event); + mPlayerState = event; + if (changed && (event == PLAYER_STATE_RELEASED) && (mIPlayerShell != null)) { + mIPlayerShell.release(); + mIPlayerShell = null; + } } return changed; } @@ -447,7 +461,11 @@ public final class AudioPlaybackConfiguration implements Parcelable { dest.writeInt(mClientPid); dest.writeInt(mPlayerState); mPlayerAttr.writeToParcel(dest, 0); - dest.writeStrongInterface(mIPlayerShell == null ? null : mIPlayerShell.getIPlayer()); + final IPlayerShell ips; + synchronized (this) { + ips = mIPlayerShell; + } + dest.writeStrongInterface(ips == null ? null : ips.getIPlayer()); } private AudioPlaybackConfiguration(Parcel in) { @@ -479,14 +497,17 @@ public final class AudioPlaybackConfiguration implements Parcelable { static final class IPlayerShell implements IBinder.DeathRecipient { final AudioPlaybackConfiguration mMonitor; // never null - private IPlayer mIPlayer; + private volatile IPlayer mIPlayer; IPlayerShell(@NonNull AudioPlaybackConfiguration monitor, @NonNull IPlayer iplayer) { mMonitor = monitor; mIPlayer = iplayer; } - void monitorDeath() { + synchronized void monitorDeath() { + if (mIPlayer == null) { + return; + } try { mIPlayer.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { @@ -509,8 +530,13 @@ public final class AudioPlaybackConfiguration implements Parcelable { } else if (DEBUG) { Log.i(TAG, "IPlayerShell binderDied"); } } - void release() { + synchronized void release() { + if (mIPlayer == null) { + return; + } mIPlayer.asBinder().unlinkToDeath(this, 0); + mIPlayer = null; + Binder.flushPendingCommands(); } } @@ -532,7 +558,7 @@ public final class AudioPlaybackConfiguration implements Parcelable { case PLAYER_TYPE_HW_SOURCE: return "hardware source"; case PLAYER_TYPE_EXTERNAL_PROXY: return "external proxy"; default: - return "unknown player type - FIXME"; + return "unknown player type " + type + " - FIXME"; } } diff --git a/android/media/MediaMuxer.java b/android/media/MediaMuxer.java index 832b2974..91e57ee0 100644 --- a/android/media/MediaMuxer.java +++ b/android/media/MediaMuxer.java @@ -70,7 +70,7 @@ import java.util.Map; <p> Per-frame metadata is useful in carrying extra information that correlated with video or audio to facilitate offline processing, e.g. gyro signals from the sensor could help video stabilization when - doing offline processing. Metaadata track is only supported in MP4 container. When adding a new + doing offline processing. Metadata track is only supported in MP4 container. When adding a new metadata track, track's mime format must start with prefix "application/", e.g. "applicaton/gyro". Metadata's format/layout will be defined by the application. Writing metadata is nearly the same as writing video/audio data except that the data will not be from mediacodec. Application just needs diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java index 0d99473c..7787d4b5 100644 --- a/android/media/MediaPlayer.java +++ b/android/media/MediaPlayer.java @@ -2072,6 +2072,20 @@ public class MediaPlayer extends PlayerBase private native void _reset(); /** + * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be + * notified when the presentation time reaches (becomes greater than or equal to) + * the value specified. + * + * @param mediaTimeUs presentation time to get timed event callback at + * @hide + */ + public void notifyAt(long mediaTimeUs) { + _notifyAt(mediaTimeUs); + } + + private native void _notifyAt(long mediaTimeUs); + + /** * Sets the audio stream type for this MediaPlayer. See {@link AudioManager} * for a list of stream types. Must call this method before prepare() or * prepareAsync() in order for the target stream type to become effective @@ -3155,6 +3169,7 @@ public class MediaPlayer extends PlayerBase private static final int MEDIA_PAUSED = 7; private static final int MEDIA_STOPPED = 8; private static final int MEDIA_SKIPPED = 9; + private static final int MEDIA_NOTIFY_TIME = 98; private static final int MEDIA_TIMED_TEXT = 99; private static final int MEDIA_ERROR = 100; private static final int MEDIA_INFO = 200; @@ -3345,6 +3360,14 @@ public class MediaPlayer extends PlayerBase } // No real default action so far. return; + + case MEDIA_NOTIFY_TIME: + TimeProvider timeProvider = mTimeProvider; + if (timeProvider != null) { + timeProvider.onNotifyTime(); + } + return; + case MEDIA_TIMED_TEXT: OnTimedTextListener onTimedTextListener = mOnTimedTextListener; if (onTimedTextListener == null) @@ -5144,19 +5167,16 @@ public class MediaPlayer extends PlayerBase private boolean mStopped = true; private boolean mBuffering; private long mLastReportedTime; - private long mTimeAdjustment; // since we are expecting only a handful listeners per stream, there is // no need for log(N) search performance private MediaTimeProvider.OnMediaTimeListener mListeners[]; private long mTimes[]; - private long mLastNanoTime; private Handler mEventHandler; private boolean mRefresh = false; private boolean mPausing = false; private boolean mSeeking = false; private static final int NOTIFY = 1; private static final int NOTIFY_TIME = 0; - private static final int REFRESH_AND_NOTIFY_TIME = 1; private static final int NOTIFY_STOP = 2; private static final int NOTIFY_SEEK = 3; private static final int NOTIFY_TRACK_DATA = 4; @@ -5188,13 +5208,11 @@ public class MediaPlayer extends PlayerBase mListeners = new MediaTimeProvider.OnMediaTimeListener[0]; mTimes = new long[0]; mLastTimeUs = 0; - mTimeAdjustment = 0; } private void scheduleNotification(int type, long delayUs) { // ignore time notifications until seek is handled - if (mSeeking && - (type == NOTIFY_TIME || type == REFRESH_AND_NOTIFY_TIME)) { + if (mSeeking && type == NOTIFY_TIME) { return; } @@ -5221,6 +5239,14 @@ public class MediaPlayer extends PlayerBase } /** @hide */ + public void onNotifyTime() { + synchronized (this) { + if (DEBUG) Log.d(TAG, "onNotifyTime: "); + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + + /** @hide */ public void onPaused(boolean paused) { synchronized(this) { if (DEBUG) Log.d(TAG, "onPaused: " + paused); @@ -5231,7 +5257,7 @@ public class MediaPlayer extends PlayerBase } else { mPausing = paused; // special handling if player disappeared mSeeking = false; - scheduleNotification(REFRESH_AND_NOTIFY_TIME, 0 /* delay */); + scheduleNotification(NOTIFY_TIME, 0 /* delay */); } } } @@ -5241,7 +5267,7 @@ public class MediaPlayer extends PlayerBase synchronized (this) { if (DEBUG) Log.d(TAG, "onBuffering: " + buffering); mBuffering = buffering; - scheduleNotification(REFRESH_AND_NOTIFY_TIME, 0 /* delay */); + scheduleNotification(NOTIFY_TIME, 0 /* delay */); } } @@ -5438,7 +5464,7 @@ public class MediaPlayer extends PlayerBase if (nextTimeUs > nowUs && !mPaused) { // schedule callback at nextTimeUs if (DEBUG) Log.d(TAG, "scheduling for " + nextTimeUs + " and " + nowUs); - scheduleNotification(NOTIFY_TIME, nextTimeUs - nowUs); + mPlayer.notifyAt(nextTimeUs); } else { mEventHandler.removeMessages(NOTIFY); // no more callbacks @@ -5449,25 +5475,6 @@ public class MediaPlayer extends PlayerBase } } - private long getEstimatedTime(long nanoTime, boolean monotonic) { - if (mPaused) { - mLastReportedTime = mLastTimeUs + mTimeAdjustment; - } else { - long timeSinceRead = (nanoTime - mLastNanoTime) / 1000; - mLastReportedTime = mLastTimeUs + timeSinceRead; - if (mTimeAdjustment > 0) { - long adjustment = - mTimeAdjustment - timeSinceRead / TIME_ADJUSTMENT_RATE; - if (adjustment <= 0) { - mTimeAdjustment = 0; - } else { - mLastReportedTime += adjustment; - } - } - } - return mLastReportedTime; - } - public long getCurrentTimeUs(boolean refreshTime, boolean monotonic) throws IllegalStateException { synchronized (this) { @@ -5477,42 +5484,38 @@ public class MediaPlayer extends PlayerBase return mLastReportedTime; } - long nanoTime = System.nanoTime(); - if (refreshTime || - nanoTime >= mLastNanoTime + MAX_NS_WITHOUT_POSITION_CHECK) { - try { - mLastTimeUs = mPlayer.getCurrentPosition() * 1000L; - mPaused = !mPlayer.isPlaying() || mBuffering; - if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs); - } catch (IllegalStateException e) { - if (mPausing) { - // if we were pausing, get last estimated timestamp - mPausing = false; - getEstimatedTime(nanoTime, monotonic); - mPaused = true; - if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime); - return mLastReportedTime; + try { + mLastTimeUs = mPlayer.getCurrentPosition() * 1000L; + mPaused = !mPlayer.isPlaying() || mBuffering; + if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs); + } catch (IllegalStateException e) { + if (mPausing) { + // if we were pausing, get last estimated timestamp + mPausing = false; + if (!monotonic || mLastReportedTime < mLastTimeUs) { + mLastReportedTime = mLastTimeUs; } - // TODO get time when prepared - throw e; + mPaused = true; + if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime); + return mLastReportedTime; } - mLastNanoTime = nanoTime; - if (monotonic && mLastTimeUs < mLastReportedTime) { - /* have to adjust time */ - mTimeAdjustment = mLastReportedTime - mLastTimeUs; - if (mTimeAdjustment > 1000000) { - // schedule seeked event if time jumped significantly - // TODO: do this properly by introducing an exception - mStopped = false; - mSeeking = true; - scheduleNotification(NOTIFY_SEEK, 0 /* delay */); - } - } else { - mTimeAdjustment = 0; + // TODO get time when prepared + throw e; + } + if (monotonic && mLastTimeUs < mLastReportedTime) { + /* have to adjust time */ + if (mLastReportedTime - mLastTimeUs > 1000000) { + // schedule seeked event if time jumped significantly + // TODO: do this properly by introducing an exception + mStopped = false; + mSeeking = true; + scheduleNotification(NOTIFY_SEEK, 0 /* delay */); } + } else { + mLastReportedTime = mLastTimeUs; } - return getEstimatedTime(nanoTime, monotonic); + return mLastReportedTime; } } @@ -5526,9 +5529,6 @@ public class MediaPlayer extends PlayerBase if (msg.what == NOTIFY) { switch (msg.arg1) { case NOTIFY_TIME: - notifyTimedEvent(false /* refreshTime */); - break; - case REFRESH_AND_NOTIFY_TIME: notifyTimedEvent(true /* refreshTime */); break; case NOTIFY_STOP: diff --git a/android/media/MediaRouter.java b/android/media/MediaRouter.java index 2894e895..fe427a73 100644 --- a/android/media/MediaRouter.java +++ b/android/media/MediaRouter.java @@ -193,7 +193,9 @@ public class MediaRouter { } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { name = com.android.internal.R.string.default_audio_route_name_dock_speakers; } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HDMI) != 0) { - name = com.android.internal.R.string.default_media_route_name_hdmi; + name = com.android.internal.R.string.default_audio_route_name_hdmi; + } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_USB) != 0) { + name = com.android.internal.R.string.default_audio_route_name_usb; } else { name = com.android.internal.R.string.default_audio_route_name; } diff --git a/android/net/LinkProperties.java b/android/net/LinkProperties.java index f527f77d..2c9fb23e 100644 --- a/android/net/LinkProperties.java +++ b/android/net/LinkProperties.java @@ -70,8 +70,23 @@ public final class LinkProperties implements Parcelable { * @hide */ public static class CompareResult<T> { - public List<T> removed = new ArrayList<T>(); - public List<T> added = new ArrayList<T>(); + public final List<T> removed = new ArrayList<T>(); + public final List<T> added = new ArrayList<T>(); + + public CompareResult() {} + + public CompareResult(Collection<T> oldItems, Collection<T> newItems) { + if (oldItems != null) { + removed.addAll(oldItems); + } + if (newItems != null) { + for (T newItem : newItems) { + if (!removed.remove(newItem)) { + added.add(newItem); + } + } + } + } @Override public String toString() { @@ -1000,17 +1015,8 @@ public final class LinkProperties implements Parcelable { * are in target but not in mLinkAddresses are placed in the * addedAddresses. */ - CompareResult<LinkAddress> result = new CompareResult<LinkAddress>(); - result.removed = new ArrayList<LinkAddress>(mLinkAddresses); - result.added.clear(); - if (target != null) { - for (LinkAddress newAddress : target.getLinkAddresses()) { - if (! result.removed.remove(newAddress)) { - result.added.add(newAddress); - } - } - } - return result; + return new CompareResult<>(mLinkAddresses, + target != null ? target.getLinkAddresses() : null); } /** @@ -1029,18 +1035,7 @@ public final class LinkProperties implements Parcelable { * are in target but not in mDnses are placed in the * addedAddresses. */ - CompareResult<InetAddress> result = new CompareResult<InetAddress>(); - - result.removed = new ArrayList<InetAddress>(mDnses); - result.added.clear(); - if (target != null) { - for (InetAddress newAddress : target.getDnsServers()) { - if (! result.removed.remove(newAddress)) { - result.added.add(newAddress); - } - } - } - return result; + return new CompareResult<>(mDnses, target != null ? target.getDnsServers() : null); } /** @@ -1058,18 +1053,7 @@ public final class LinkProperties implements Parcelable { * leaving the routes that are different. And route address which * are in target but not in mRoutes are placed in added. */ - CompareResult<RouteInfo> result = new CompareResult<RouteInfo>(); - - result.removed = getAllRoutes(); - result.added.clear(); - if (target != null) { - for (RouteInfo r : target.getAllRoutes()) { - if (! result.removed.remove(r)) { - result.added.add(r); - } - } - } - return result; + return new CompareResult<>(getAllRoutes(), target != null ? target.getAllRoutes() : null); } /** @@ -1087,18 +1071,8 @@ public final class LinkProperties implements Parcelable { * leaving the interface names that are different. And interface names which * are in target but not in this are placed in added. */ - CompareResult<String> result = new CompareResult<String>(); - - result.removed = getAllInterfaceNames(); - result.added.clear(); - if (target != null) { - for (String r : target.getAllInterfaceNames()) { - if (! result.removed.remove(r)) { - result.added.add(r); - } - } - } - return result; + return new CompareResult<>(getAllInterfaceNames(), + target != null ? target.getAllInterfaceNames() : null); } diff --git a/android/net/metrics/WakeupEvent.java b/android/net/metrics/WakeupEvent.java new file mode 100644 index 00000000..cbf3fc8c --- /dev/null +++ b/android/net/metrics/WakeupEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 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 android.net.metrics; + +/** + * An event logged when NFLOG notifies userspace of a wakeup packet for + * watched interfaces. + * {@hide} + */ +public class WakeupEvent { + public String iface; + public long timestampMs; + public int uid; + + @Override + public String toString() { + return String.format("WakeupEvent(%tT.%tL, %s, uid: %d)", + timestampMs, timestampMs, iface, uid); + } +} diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java new file mode 100644 index 00000000..d520b974 --- /dev/null +++ b/android/net/metrics/WakeupStats.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 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 android.net.metrics; + +import android.os.Process; +import android.os.SystemClock; + +/** + * An event logged per interface and that aggregates WakeupEvents for that interface. + * {@hide} + */ +public class WakeupStats { + + private static final int NO_UID = -1; + + public final long creationTimeMs = SystemClock.elapsedRealtime(); + public final String iface; + + public long totalWakeups = 0; + public long rootWakeups = 0; + public long systemWakeups = 0; + public long nonApplicationWakeups = 0; + public long applicationWakeups = 0; + public long unroutedWakeups = 0; + public long durationSec = 0; + + public WakeupStats(String iface) { + this.iface = iface; + } + + /** Update durationSec with current time. */ + public void updateDuration() { + durationSec = (SystemClock.elapsedRealtime() - creationTimeMs) / 1000; + } + + /** Update wakeup counters for the given WakeupEvent. */ + public void countEvent(WakeupEvent ev) { + totalWakeups++; + switch (ev.uid) { + case Process.ROOT_UID: + rootWakeups++; + break; + case Process.SYSTEM_UID: + systemWakeups++; + break; + case NO_UID: + unroutedWakeups++; + break; + default: + if (ev.uid >= Process.FIRST_APPLICATION_UID) { + applicationWakeups++; + } else { + nonApplicationWakeups++; + } + break; + } + } + + @Override + public String toString() { + updateDuration(); + return new StringBuilder() + .append("WakeupStats(").append(iface) + .append(", total: ").append(totalWakeups) + .append(", root: ").append(rootWakeups) + .append(", system: ").append(systemWakeups) + .append(", apps: ").append(applicationWakeups) + .append(", non-apps: ").append(nonApplicationWakeups) + .append(", unrouted: ").append(unroutedWakeups) + .append(", ").append(durationSec).append("s)") + .toString(); + } +} diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java index cea5715d..66b6b478 100644 --- a/android/os/BatteryStats.java +++ b/android/os/BatteryStats.java @@ -53,6 +53,8 @@ public abstract class BatteryStats implements Parcelable { private static final String TAG = "BatteryStats"; private static final boolean LOCAL_LOGV = false; + /** Fetching RPM stats is too slow to do each time screen changes, so disable it. */ + protected static final boolean SCREEN_OFF_RPM_STATS_ENABLED = false; /** @hide */ public static final String SERVICE_NAME = "batterystats"; @@ -213,8 +215,10 @@ public abstract class BatteryStats implements Parcelable { * - Fixed bugs in background timers and BLE scan time * New in version 25: * - Package wakeup alarms are now on screen-off timebase + * New in version 26: + * - Resource power manager (rpm) states [but screenOffRpm is disabled from working properly] */ - static final String CHECKIN_VERSION = "25"; + static final String CHECKIN_VERSION = "26"; /** * Old version, we hit 9 and ran out of room, need to remove. @@ -233,6 +237,10 @@ public abstract class BatteryStats implements Parcelable { private static final String CPU_DATA = "cpu"; private static final String GLOBAL_CPU_FREQ_DATA = "gcf"; private static final String CPU_TIMES_AT_FREQ_DATA = "ctf"; + // rpm line is: + // BATTERY_STATS_CHECKIN_VERSION, uid, which, "rpm", state/voter name, total time, total count, + // screen-off time, screen-off count + private static final String RESOURCE_POWER_MANAGER_DATA = "rpm"; private static final String SENSOR_DATA = "sr"; private static final String VIBRATOR_DATA = "vib"; private static final String FOREGROUND_ACTIVITY_DATA = "fg"; @@ -2648,6 +2656,16 @@ public abstract class BatteryStats implements Parcelable { public abstract Map<String, ? extends Timer> getKernelWakelockStats(); + /** + * Returns Timers tracking the total time of each Resource Power Manager state and voter. + */ + public abstract Map<String, ? extends Timer> getRpmStats(); + /** + * Returns Timers tracking the screen-off time of each Resource Power Manager state and voter. + */ + public abstract Map<String, ? extends Timer> getScreenOffRpmStats(); + + public abstract LongSparseArray<? extends Timer> getKernelMemoryStats(); public abstract void writeToParcelWithoutUids(Parcel out, int flags); @@ -3309,6 +3327,30 @@ public abstract class BatteryStats implements Parcelable { } } + final Map<String, ? extends Timer> rpmStats = getRpmStats(); + final Map<String, ? extends Timer> screenOffRpmStats = getScreenOffRpmStats(); + if (rpmStats.size() > 0) { + for (Map.Entry<String, ? extends Timer> ent : rpmStats.entrySet()) { + sb.setLength(0); + Timer totalTimer = ent.getValue(); + long timeMs = (totalTimer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; + int count = totalTimer.getCountLocked(which); + Timer screenOffTimer = screenOffRpmStats.get(ent.getKey()); + long screenOffTimeMs = screenOffTimer != null + ? (screenOffTimer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000 : 0; + int screenOffCount = screenOffTimer != null + ? screenOffTimer.getCountLocked(which) : 0; + if (SCREEN_OFF_RPM_STATS_ENABLED) { + dumpLine(pw, 0 /* uid */, category, RESOURCE_POWER_MANAGER_DATA, + "\"" + ent.getKey() + "\"", timeMs, count, screenOffTimeMs, + screenOffCount); + } else { + dumpLine(pw, 0 /* uid */, category, RESOURCE_POWER_MANAGER_DATA, + "\"" + ent.getKey() + "\"", timeMs, count); + } + } + } + final BatteryStatsHelper helper = new BatteryStatsHelper(context, false, wifiOnly); helper.create(this); helper.refreshStats(which, UserHandle.USER_ALL); @@ -4570,24 +4612,56 @@ public abstract class BatteryStats implements Parcelable { } final LongSparseArray<? extends Timer> mMemoryStats = getKernelMemoryStats(); - pw.println("Memory Stats"); - for (int i = 0; i < mMemoryStats.size(); i++) { - sb.setLength(0); - sb.append("Bandwidth "); - sb.append(mMemoryStats.keyAt(i)); - sb.append(" Time "); - sb.append(mMemoryStats.valueAt(i).getTotalTimeLocked(rawRealtime, which)); - pw.println(sb.toString()); + if (mMemoryStats.size() > 0) { + pw.println(" Memory Stats"); + for (int i = 0; i < mMemoryStats.size(); i++) { + sb.setLength(0); + sb.append(" Bandwidth "); + sb.append(mMemoryStats.keyAt(i)); + sb.append(" Time "); + sb.append(mMemoryStats.valueAt(i).getTotalTimeLocked(rawRealtime, which)); + pw.println(sb.toString()); + } + pw.println(); + } + + final Map<String, ? extends Timer> rpmStats = getRpmStats(); + if (rpmStats.size() > 0) { + pw.print(prefix); pw.println(" Resource Power Manager Stats"); + if (rpmStats.size() > 0) { + for (Map.Entry<String, ? extends Timer> ent : rpmStats.entrySet()) { + final String timerName = ent.getKey(); + final Timer timer = ent.getValue(); + printTimer(pw, sb, timer, rawRealtime, which, prefix, timerName); + } + } + pw.println(); + } + if (SCREEN_OFF_RPM_STATS_ENABLED) { + final Map<String, ? extends Timer> screenOffRpmStats = getScreenOffRpmStats(); + if (screenOffRpmStats.size() > 0) { + pw.print(prefix); + pw.println(" Resource Power Manager Stats for when screen was off"); + if (screenOffRpmStats.size() > 0) { + for (Map.Entry<String, ? extends Timer> ent : screenOffRpmStats.entrySet()) { + final String timerName = ent.getKey(); + final Timer timer = ent.getValue(); + printTimer(pw, sb, timer, rawRealtime, which, prefix, timerName); + } + } + pw.println(); + } } final long[] cpuFreqs = getCpuFreqs(); if (cpuFreqs != null) { sb.setLength(0); - sb.append("CPU freqs:"); + sb.append(" CPU freqs:"); for (int i = 0; i < cpuFreqs.length; ++i) { sb.append(" " + cpuFreqs[i]); } pw.println(sb.toString()); + pw.println(); } for (int iu=0; iu<NU; iu++) { diff --git a/android/os/Process.java b/android/os/Process.java index 93516619..b5d62e55 100644 --- a/android/os/Process.java +++ b/android/os/Process.java @@ -19,8 +19,8 @@ package android.os; import android.annotation.TestApi; import android.system.Os; import android.system.OsConstants; -import android.util.Log; import android.webkit.WebViewZygote; + import dalvik.system.VMRuntime; /** @@ -423,7 +423,7 @@ public class Process { * * When invokeWith is not null, the process will be started as a fresh app * and not a zygote fork. Note that this is only allowed for uid 0 or when - * debugFlags contains DEBUG_ENABLE_DEBUGGER. + * runtimeFlags contains DEBUG_ENABLE_DEBUGGER. * * @param processClass The class to use as the process's main entry * point. @@ -431,7 +431,7 @@ public class Process { * @param uid The user-id under which the process will run. * @param gid The group-id under which the process will run. * @param gids Additional group-ids associated with the process. - * @param debugFlags Additional flags. + * @param runtimeFlags Additional flags for the runtime. * @param targetSdkVersion The target SDK version for the app. * @param seInfo null-ok SELinux information for the new process. * @param abi non-null the ABI this app should be started with. @@ -448,7 +448,7 @@ public class Process { public static final ProcessStartResult start(final String processClass, final String niceName, int uid, int gid, int[] gids, - int debugFlags, int mountExternal, + int runtimeFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, @@ -457,7 +457,7 @@ public class Process { String invokeWith, String[] zygoteArgs) { return zygoteProcess.start(processClass, niceName, uid, gid, gids, - debugFlags, mountExternal, targetSdkVersion, seInfo, + runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, zygoteArgs); } @@ -465,7 +465,7 @@ public class Process { public static final ProcessStartResult startWebView(final String processClass, final String niceName, int uid, int gid, int[] gids, - int debugFlags, int mountExternal, + int runtimeFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, @@ -474,7 +474,7 @@ public class Process { String invokeWith, String[] zygoteArgs) { return WebViewZygote.getProcess().start(processClass, niceName, uid, gid, gids, - debugFlags, mountExternal, targetSdkVersion, seInfo, + runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, zygoteArgs); } diff --git a/android/os/ServiceManager.java b/android/os/ServiceManager.java index e11494d5..34c78455 100644 --- a/android/os/ServiceManager.java +++ b/android/os/ServiceManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2009 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. @@ -16,114 +16,44 @@ package android.os; -import android.util.Log; - -import com.android.internal.os.BinderInternal; - -import java.util.HashMap; import java.util.Map; -/** @hide */ public final class ServiceManager { - private static final String TAG = "ServiceManager"; - - private static IServiceManager sServiceManager; - private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>(); - - private static IServiceManager getIServiceManager() { - if (sServiceManager != null) { - return sServiceManager; - } - - // Find the service manager - sServiceManager = ServiceManagerNative - .asInterface(Binder.allowBlocking(BinderInternal.getContextObject())); - return sServiceManager; - } /** * Returns a reference to a service with the given name. - * + * * @param name the name of the service to get * @return a reference to the service, or <code>null</code> if the service doesn't exist */ public static IBinder getService(String name) { - try { - IBinder service = sCache.get(name); - if (service != null) { - return service; - } else { - return Binder.allowBlocking(getIServiceManager().getService(name)); - } - } catch (RemoteException e) { - Log.e(TAG, "error in getService", e); - } return null; } /** - * Returns a reference to a service with the given name, or throws - * {@link NullPointerException} if none is found. - * - * @hide + * Is not supposed to return null, but that is fine for layoutlib. */ public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException { - final IBinder binder = getService(name); - if (binder != null) { - return binder; - } else { - throw new ServiceNotFoundException(name); - } + throw new ServiceNotFoundException(name); } /** * Place a new @a service called @a name into the service * manager. - * + * * @param name the name of the new service * @param service the service object */ public static void addService(String name, IBinder service) { - try { - getIServiceManager().addService(name, service, false); - } catch (RemoteException e) { - Log.e(TAG, "error in addService", e); - } + // pass } /** - * Place a new @a service called @a name into the service - * manager. - * - * @param name the name of the new service - * @param service the service object - * @param allowIsolated set to true to allow isolated sandboxed processes - * to access this service - */ - public static void addService(String name, IBinder service, boolean allowIsolated) { - try { - getIServiceManager().addService(name, service, allowIsolated); - } catch (RemoteException e) { - Log.e(TAG, "error in addService", e); - } - } - - /** * Retrieve an existing service called @a name from the * service manager. Non-blocking. */ public static IBinder checkService(String name) { - try { - IBinder service = sCache.get(name); - if (service != null) { - return service; - } else { - return Binder.allowBlocking(getIServiceManager().checkService(name)); - } - } catch (RemoteException e) { - Log.e(TAG, "error in checkService", e); - return null; - } + return null; } /** @@ -132,27 +62,21 @@ public final class ServiceManager { * case of an exception */ public static String[] listServices() { - try { - return getIServiceManager().listServices(); - } catch (RemoteException e) { - Log.e(TAG, "error in listServices", e); - return null; - } + // actual implementation returns null sometimes, so it's ok + // to return null instead of an empty list. + return null; } /** * This is only intended to be called when the process is first being brought * up and bound by the activity manager. There is only one thread in the process * at that time, so no locking is done. - * + * * @param cache the cache of service references * @hide */ public static void initServiceCache(Map<String, IBinder> cache) { - if (sCache.size() != 0) { - throw new IllegalStateException("setServiceCache may only be called once"); - } - sCache.putAll(cache); + // pass } /** @@ -163,6 +87,7 @@ public final class ServiceManager { * @hide */ public static class ServiceNotFoundException extends Exception { + // identical to the original implementation public ServiceNotFoundException(String name) { super("No service published for: " + name); } diff --git a/android/os/StrictMode.java b/android/os/StrictMode.java index 615d3c4c..f02631c7 100644 --- a/android/os/StrictMode.java +++ b/android/os/StrictMode.java @@ -722,7 +722,7 @@ public final class StrictMode { } /** - * Detect when an {@link java.io.Closeable} or other object with a explict termination + * Detect when an {@link java.io.Closeable} or other object with an explicit termination * method is finalized without having been closed. * * <p>You always want to explicitly close such objects to avoid unnecessary resources diff --git a/android/os/ZygoteProcess.java b/android/os/ZygoteProcess.java index 7a13ee89..670f7949 100644 --- a/android/os/ZygoteProcess.java +++ b/android/os/ZygoteProcess.java @@ -20,9 +20,11 @@ import android.net.LocalSocket; import android.net.LocalSocketAddress; import android.util.Log; import android.util.Slog; + import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; import com.android.internal.util.Preconditions; + import java.io.BufferedWriter; import java.io.DataInputStream; import java.io.IOException; @@ -173,7 +175,7 @@ public class ZygoteProcess { * * When invokeWith is not null, the process will be started as a fresh app * and not a zygote fork. Note that this is only allowed for uid 0 or when - * debugFlags contains DEBUG_ENABLE_DEBUGGER. + * runtimeFlags contains DEBUG_ENABLE_DEBUGGER. * * @param processClass The class to use as the process's main entry * point. @@ -181,7 +183,7 @@ public class ZygoteProcess { * @param uid The user-id under which the process will run. * @param gid The group-id under which the process will run. * @param gids Additional group-ids associated with the process. - * @param debugFlags Additional flags. + * @param runtimeFlags Additional flags. * @param targetSdkVersion The target SDK version for the app. * @param seInfo null-ok SELinux information for the new process. * @param abi non-null the ABI this app should be started with. @@ -196,7 +198,7 @@ public class ZygoteProcess { public final Process.ProcessStartResult start(final String processClass, final String niceName, int uid, int gid, int[] gids, - int debugFlags, int mountExternal, + int runtimeFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, @@ -206,7 +208,7 @@ public class ZygoteProcess { String[] zygoteArgs) { try { return startViaZygote(processClass, niceName, uid, gid, gids, - debugFlags, mountExternal, targetSdkVersion, seInfo, + runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, zygoteArgs); } catch (ZygoteStartFailedEx ex) { Log.e(LOG_TAG, @@ -317,7 +319,7 @@ public class ZygoteProcess { * @param gid a POSIX gid that the new process shuold setgid() to * @param gids null-ok; a list of supplementary group IDs that the * new process should setgroup() to. - * @param debugFlags Additional flags. + * @param runtimeFlags Additional flags for the runtime. * @param targetSdkVersion The target SDK version for the app. * @param seInfo null-ok SELinux information for the new process. * @param abi the ABI the process should use. @@ -331,7 +333,7 @@ public class ZygoteProcess { final String niceName, final int uid, final int gid, final int[] gids, - int debugFlags, int mountExternal, + int runtimeFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, @@ -347,33 +349,7 @@ public class ZygoteProcess { argsForZygote.add("--runtime-args"); argsForZygote.add("--setuid=" + uid); argsForZygote.add("--setgid=" + gid); - if ((debugFlags & Zygote.DEBUG_ENABLE_JNI_LOGGING) != 0) { - argsForZygote.add("--enable-jni-logging"); - } - if ((debugFlags & Zygote.DEBUG_ENABLE_SAFEMODE) != 0) { - argsForZygote.add("--enable-safemode"); - } - if ((debugFlags & Zygote.DEBUG_ENABLE_JDWP) != 0) { - argsForZygote.add("--enable-jdwp"); - } - if ((debugFlags & Zygote.DEBUG_ENABLE_CHECKJNI) != 0) { - argsForZygote.add("--enable-checkjni"); - } - if ((debugFlags & Zygote.DEBUG_GENERATE_DEBUG_INFO) != 0) { - argsForZygote.add("--generate-debug-info"); - } - if ((debugFlags & Zygote.DEBUG_ALWAYS_JIT) != 0) { - argsForZygote.add("--always-jit"); - } - if ((debugFlags & Zygote.DEBUG_NATIVE_DEBUGGABLE) != 0) { - argsForZygote.add("--native-debuggable"); - } - if ((debugFlags & Zygote.DEBUG_JAVA_DEBUGGABLE) != 0) { - argsForZygote.add("--java-debuggable"); - } - if ((debugFlags & Zygote.DEBUG_ENABLE_ASSERT) != 0) { - argsForZygote.add("--enable-assert"); - } + argsForZygote.add("--runtime-flags=" + runtimeFlags); if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) { argsForZygote.add("--mount-external-default"); } else if (mountExternal == Zygote.MOUNT_EXTERNAL_READ) { diff --git a/android/os/storage/StorageManager.java b/android/os/storage/StorageManager.java index 70363468..6594cd07 100644 --- a/android/os/storage/StorageManager.java +++ b/android/os/storage/StorageManager.java @@ -221,8 +221,6 @@ public class StorageManager { /** {@hide} */ public static final int FSTRIM_FLAG_DEEP = IVold.FSTRIM_FLAG_DEEP_TRIM; - /** {@hide} */ - public static final int FSTRIM_FLAG_BENCHMARK = IVold.FSTRIM_FLAG_BENCHMARK_AFTER; /** @hide The volume is not encrypted. */ public static final int ENCRYPTION_STATE_NONE = diff --git a/android/os/storage/VolumeInfo.java b/android/os/storage/VolumeInfo.java index b8353d7a..76f79f13 100644 --- a/android/os/storage/VolumeInfo.java +++ b/android/os/storage/VolumeInfo.java @@ -76,21 +76,21 @@ public class VolumeInfo implements Parcelable { /** Real volume representing internal emulated storage */ public static final String ID_EMULATED_INTERNAL = "emulated"; - public static final int TYPE_PUBLIC = IVold.TYPE_PUBLIC; - public static final int TYPE_PRIVATE = IVold.TYPE_PRIVATE; - public static final int TYPE_EMULATED = IVold.TYPE_EMULATED; - public static final int TYPE_ASEC = IVold.TYPE_ASEC; - public static final int TYPE_OBB = IVold.TYPE_OBB; - - public static final int STATE_UNMOUNTED = IVold.STATE_UNMOUNTED; - public static final int STATE_CHECKING = IVold.STATE_CHECKING; - public static final int STATE_MOUNTED = IVold.STATE_MOUNTED; - public static final int STATE_MOUNTED_READ_ONLY = IVold.STATE_MOUNTED_READ_ONLY; - public static final int STATE_FORMATTING = IVold.STATE_FORMATTING; - public static final int STATE_EJECTING = IVold.STATE_EJECTING; - public static final int STATE_UNMOUNTABLE = IVold.STATE_UNMOUNTABLE; - public static final int STATE_REMOVED = IVold.STATE_REMOVED; - public static final int STATE_BAD_REMOVAL = IVold.STATE_BAD_REMOVAL; + public static final int TYPE_PUBLIC = IVold.VOLUME_TYPE_PUBLIC; + public static final int TYPE_PRIVATE = IVold.VOLUME_TYPE_PRIVATE; + public static final int TYPE_EMULATED = IVold.VOLUME_TYPE_EMULATED; + public static final int TYPE_ASEC = IVold.VOLUME_TYPE_ASEC; + public static final int TYPE_OBB = IVold.VOLUME_TYPE_OBB; + + public static final int STATE_UNMOUNTED = IVold.VOLUME_STATE_UNMOUNTED; + public static final int STATE_CHECKING = IVold.VOLUME_STATE_CHECKING; + public static final int STATE_MOUNTED = IVold.VOLUME_STATE_MOUNTED; + public static final int STATE_MOUNTED_READ_ONLY = IVold.VOLUME_STATE_MOUNTED_READ_ONLY; + public static final int STATE_FORMATTING = IVold.VOLUME_STATE_FORMATTING; + public static final int STATE_EJECTING = IVold.VOLUME_STATE_EJECTING; + public static final int STATE_UNMOUNTABLE = IVold.VOLUME_STATE_UNMOUNTABLE; + public static final int STATE_REMOVED = IVold.VOLUME_STATE_REMOVED; + public static final int STATE_BAD_REMOVAL = IVold.VOLUME_STATE_BAD_REMOVAL; public static final int MOUNT_FLAG_PRIMARY = IVold.MOUNT_FLAG_PRIMARY; public static final int MOUNT_FLAG_VISIBLE = IVold.MOUNT_FLAG_VISIBLE; diff --git a/android/provider/Settings.java b/android/provider/Settings.java index 2cb3864f..40ced6ce 100644 --- a/android/provider/Settings.java +++ b/android/provider/Settings.java @@ -5194,17 +5194,39 @@ public final class Settings { public static final String ALLOW_MOCK_LOCATION = "mock_location"; /** - * A 64-bit number (as a hex string) that is randomly + * On Android 8.0 (API level 26) and higher versions of the platform, + * a 64-bit number (expressed as a hexadecimal string), unique to + * each combination of app-signing key, user, and device. + * Values of {@code ANDROID_ID} are scoped by signing key and user. + * The value may change if a factory reset is performed on the + * device or if an APK signing key changes. + * + * For more information about how the platform handles {@code ANDROID_ID} + * in Android 8.0 (API level 26) and higher, see <a + * href="{@docRoot}preview/behavior-changes.html#privacy-all"> + * Android 8.0 Behavior Changes</a>. + * + * <p class="note"><strong>Note:</strong> For apps that were installed + * prior to updating the device to a version of Android 8.0 + * (API level 26) or higher, the value of {@code ANDROID_ID} changes + * if the app is uninstalled and then reinstalled after the OTA. + * To preserve values across uninstalls after an OTA to Android 8.0 + * or higher, developers can use + * <a href="{@docRoot}guide/topics/data/keyvaluebackup.html"> + * Key/Value Backup</a>.</p> + * + * <p>In versions of the platform lower than Android 8.0 (API level 26), + * a 64-bit number (expressed as a hexadecimal string) that is randomly * generated when the user first sets up the device and should remain - * constant for the lifetime of the user's device. The value may - * change if a factory reset is performed on the device. - * <p class="note"><strong>Note:</strong> When a device has <a - * href="{@docRoot}about/versions/android-4.2.html#MultipleUsers">multiple users</a> - * (available on certain devices running Android 4.2 or higher), each user appears as a - * completely separate device, so the {@code ANDROID_ID} value is unique to each - * user.</p> - * - * <p class="note"><strong>Note:</strong> If the caller is an Instant App the id is scoped + * constant for the lifetime of the user's device. + * + * On devices that have + * <a href="{@docRoot}about/versions/android-4.2.html#MultipleUsers"> + * multiple users</a>, each user appears as a + * completely separate device, so the {@code ANDROID_ID} value is + * unique to each user.</p> + * + * <p class="note"><strong>Note:</strong> If the caller is an Instant App the ID is scoped * to the Instant App, it is generated when the Instant App is first installed and reset if * the user clears the Instant App. */ @@ -7124,6 +7146,31 @@ public final class Settings { * @hide */ public static final String LOCKDOWN_IN_POWER_MENU = "lockdown_in_power_menu"; + + /** + * Backup manager behavioral parameters. + * This is encoded as a key=value list, separated by commas. Ex: + * + * "key_value_backup_interval_milliseconds=14400000,key_value_backup_require_charging=true" + * + * The following keys are supported: + * + * <pre> + * key_value_backup_interval_milliseconds (long) + * key_value_backup_fuzz_milliseconds (long) + * key_value_backup_require_charging (boolean) + * key_value_backup_required_network_type (int) + * full_backup_interval_milliseconds (long) + * full_backup_require_charging (boolean) + * full_backup_required_network_type (int) + * </pre> + * + * <p> + * Type: string + * @hide + */ + public static final String BACKUP_MANAGER_CONSTANTS = "backup_manager_constants"; + /** * This are the settings to be backed up. * diff --git a/android/service/settings/suggestions/Suggestion.java b/android/service/settings/suggestions/Suggestion.java new file mode 100644 index 00000000..f27cc2eb --- /dev/null +++ b/android/service/settings/suggestions/Suggestion.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2017 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 android.service.settings.suggestions; + +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Data object that has information about a device suggestion. + * + * @hide + */ +@SystemApi +public final class Suggestion implements Parcelable { + + private final String mId; + private final CharSequence mTitle; + private final CharSequence mSummary; + private final PendingIntent mPendingIntent; + + /** + * Gets the id for the suggestion object. + */ + public String getId() { + return mId; + } + + /** + * Title of the suggestion that is shown to the user. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Optional summary describing what this suggestion controls. + */ + public CharSequence getSummary() { + return mSummary; + } + + /** + * The Intent to launch when the suggestion is activated. + */ + public PendingIntent getPendingIntent() { + return mPendingIntent; + } + + private Suggestion(Builder builder) { + mId = builder.mId; + mTitle = builder.mTitle; + mSummary = builder.mSummary; + mPendingIntent = builder.mPendingIntent; + } + + private Suggestion(Parcel in) { + mId = in.readString(); + mTitle = in.readCharSequence(); + mSummary = in.readCharSequence(); + mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader()); + } + + public static final Creator<Suggestion> CREATOR = new Creator<Suggestion>() { + @Override + public Suggestion createFromParcel(Parcel in) { + return new Suggestion(in); + } + + @Override + public Suggestion[] newArray(int size) { + return new Suggestion[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + dest.writeCharSequence(mTitle); + dest.writeCharSequence(mSummary); + dest.writeParcelable(mPendingIntent, flags); + } + + /** + * Builder class for {@link Suggestion}. + */ + public static class Builder { + private final String mId; + private CharSequence mTitle; + private CharSequence mSummary; + private PendingIntent mPendingIntent; + + public Builder(String id) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("Suggestion id cannot be empty"); + } + mId = id; + } + + /** + * Sets suggestion title + */ + public Builder setTitle(CharSequence title) { + mTitle = title; + return this; + } + + /** + * Sets suggestion summary + */ + public Builder setSummary(CharSequence summary) { + mSummary = summary; + return this; + } + + /** + * Sets suggestion intent + */ + public Builder setPendingIntent(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + return this; + } + + /** + * Builds an immutable {@link Suggestion} object. + */ + public Suggestion build() { + return new Suggestion(this /* builder */); + } + } +} diff --git a/android/service/settings/suggestions/SuggestionService.java b/android/service/settings/suggestions/SuggestionService.java new file mode 100644 index 00000000..2a4c84c2 --- /dev/null +++ b/android/service/settings/suggestions/SuggestionService.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 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 android.service.settings.suggestions; + +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +import java.util.List; + +/** + * This is the base class for implementing suggestion service. A suggestion service is responsible + * to provide a collection of {@link Suggestion}s for the current user when queried. + * + * @hide + */ +@SystemApi +public abstract class SuggestionService extends Service { + + private static final String TAG = "SuggestionService"; + private static final boolean DEBUG = false; + + @Override + public IBinder onBind(Intent intent) { + return new ISuggestionService.Stub() { + @Override + public List<Suggestion> getSuggestions() { + if (DEBUG) { + Log.d(TAG, "getSuggestions() " + getPackageName()); + } + return onGetSuggestions(); + } + + @Override + public void dismissSuggestion(Suggestion suggestion) { + if (DEBUG) { + Log.d(TAG, "dismissSuggestion() " + getPackageName()); + } + onSuggestionDismissed(suggestion); + } + }; + } + + /** + * Return all available suggestions. + */ + public abstract List<Suggestion> onGetSuggestions(); + + /** + * Dismiss a suggestion. The suggestion will not be included in future + * {@link #onGetSuggestions()} calls. + * @param suggestion + */ + public abstract void onSuggestionDismissed(Suggestion suggestion); +} diff --git a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java index d0496e44..61ea52ba 100644 --- a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java +++ b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java @@ -254,9 +254,7 @@ public class PlaybackTransportControlGlue<T extends PlayerAdapter> // playing paused paused // paused playing playing // ff/rw playing playing paused - if (canPause - && (canPlay ? mIsPlaying : - !mIsPlaying)) { + if (canPause && mIsPlaying) { mIsPlaying = false; pause(); } else if (canPlay && !mIsPlaying) { diff --git a/android/support/v4/app/FragmentManager.java b/android/support/v4/app/FragmentManager.java index 20e8d9e4..6e6caa04 100644 --- a/android/support/v4/app/FragmentManager.java +++ b/android/support/v4/app/FragmentManager.java @@ -1594,6 +1594,8 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate private void animateRemoveFragment(@NonNull final Fragment fragment, @NonNull AnimationOrAnimator anim, final int newState) { final View viewToAnimate = fragment.mView; + final ViewGroup container = fragment.mContainer; + container.startViewTransition(viewToAnimate); fragment.setStateAfterAnimating(newState); if (anim.animation != null) { Animation animation = anim.animation; @@ -1603,6 +1605,8 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate @Override public void onAnimationEnd(Animation animation) { super.onAnimationEnd(animation); + container.endViewTransition(viewToAnimate); + if (fragment.getAnimatingAway() != null) { fragment.setAnimatingAway(null); moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, false); @@ -1612,25 +1616,18 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate setHWLayerAnimListenerIfAlpha(viewToAnimate, anim); fragment.mView.startAnimation(animation); } else { - final Animator animator = anim.animator; + Animator animator = anim.animator; fragment.setAnimator(anim.animator); - final ViewGroup container = fragment.mContainer; - if (container != null) { - container.startViewTransition(viewToAnimate); - } animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator anim) { - // AnimatorSet in API 26 (only) can end() during start(), so delay by posting - if (container == null || container.indexOfChild(viewToAnimate) < 0) { - finishAnimatedFragmentRemoval(fragment, container, viewToAnimate); - } else { - viewToAnimate.post(new Runnable() { - @Override - public void run() { - finishAnimatedFragmentRemoval(fragment, container, viewToAnimate); - } - }); + container.endViewTransition(viewToAnimate); + // If an animator ends immediately, we can just pretend there is no animation. + // When that happens the the fragment's view won't have been removed yet. + Animator animator = fragment.getAnimator(); + fragment.setAnimator(null); + if (animator != null && container.indexOfChild(viewToAnimate) < 0) { + moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, false); } } }); @@ -1640,17 +1637,6 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate } } - void finishAnimatedFragmentRemoval(Fragment fragment, ViewGroup container, View view) { - if (container != null) { - container.endViewTransition(view); - } - if (fragment.getAnimator() != null) { - fragment.setAnimator(null); - moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, - false); - } - } - void moveToState(Fragment f) { moveToState(f, mCurState, 0, 0, false); } @@ -2646,7 +2632,6 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate // Give up waiting for the animation and just end it. final int stateAfterAnimating = fragment.getStateAfterAnimating(); final View animatingAway = fragment.getAnimatingAway(); - fragment.setAnimatingAway(null); Animation animation = animatingAway.getAnimation(); if (animation != null) { animation.cancel(); @@ -2654,6 +2639,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate // and will instead cause the animation to infinitely loop animatingAway.clearAnimation(); } + fragment.setAnimatingAway(null); moveToState(fragment, stateAfterAnimating, 0, 0, false); } else if (fragment.getAnimator() != null) { fragment.getAnimator().end(); diff --git a/android/support/v4/media/MediaBrowserServiceCompat.java b/android/support/v4/media/MediaBrowserServiceCompat.java index 8175aaee..53b111ab 100644 --- a/android/support/v4/media/MediaBrowserServiceCompat.java +++ b/android/support/v4/media/MediaBrowserServiceCompat.java @@ -550,7 +550,7 @@ public abstract class MediaBrowserServiceCompat extends Service { /** * All the info about a connection. */ - private static class ConnectionRecord { + private class ConnectionRecord implements IBinder.DeathRecipient { String pkg; Bundle rootHints; ServiceCallbacks callbacks; @@ -559,6 +559,16 @@ public abstract class MediaBrowserServiceCompat extends Service { ConnectionRecord() { } + + @Override + public void binderDied() { + mHandler.post(new Runnable() { + @Override + public void run() { + mConnections.remove(callbacks.asBinder()); + } + }); + } } /** @@ -747,6 +757,7 @@ public abstract class MediaBrowserServiceCompat extends Service { } else { try { mConnections.put(b, connection); + b.linkToDeath(connection, 0); if (mSession != null) { callbacks.onConnect(connection.root.getRootId(), mSession, connection.root.getExtras()); @@ -771,6 +782,7 @@ public abstract class MediaBrowserServiceCompat extends Service { final ConnectionRecord old = mConnections.remove(b); if (old != null) { // TODO + old.callbacks.asBinder().unlinkToDeath(old, 0); } } }); @@ -852,6 +864,11 @@ public abstract class MediaBrowserServiceCompat extends Service { connection.callbacks = callbacks; connection.rootHints = rootHints; mConnections.put(b, connection); + try { + b.linkToDeath(connection, 0); + } catch (RemoteException e) { + Log.w(TAG, "IBinder is already dead."); + } } }); } @@ -862,7 +879,10 @@ public abstract class MediaBrowserServiceCompat extends Service { @Override public void run() { final IBinder b = callbacks.asBinder(); - mConnections.remove(b); + ConnectionRecord old = mConnections.remove(b); + if (old != null) { + b.unlinkToDeath(old, 0); + } } }); } diff --git a/android/support/v4/os/BuildCompat.java b/android/support/v4/os/BuildCompat.java index 9a48c5f1..586557d2 100644 --- a/android/support/v4/os/BuildCompat.java +++ b/android/support/v4/os/BuildCompat.java @@ -60,6 +60,7 @@ public class BuildCompat { * be removed in a future release of the Support Library. Instead use * {@code Build.SDK_INT >= Build.VERSION_CODES#O}. */ + @Deprecated public static boolean isAtLeastO() { return VERSION.SDK_INT >= 26; } @@ -68,7 +69,11 @@ public class BuildCompat { * Checks if the device is running on a pre-release version of Android O MR1 or newer. * <p> * @return {@code true} if O MR1 APIs are available for use, {@code false} otherwise + * @deprecated Android O MR1 is a finalized release and this method is no longer necessary. It + * will be removed in a future release of the Support Library. Instead, use + * {@code Build.SDK_INT >= Build.VERSION_CODES#O_MR1}. */ + @Deprecated public static boolean isAtLeastOMR1() { return VERSION.SDK_INT >= 27; } diff --git a/android/support/v7/preference/CollapsiblePreferenceGroupController.java b/android/support/v7/preference/CollapsiblePreferenceGroupController.java new file mode 100644 index 00000000..e15ca18f --- /dev/null +++ b/android/support/v7/preference/CollapsiblePreferenceGroupController.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2017 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 android.support.v7.preference; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * A controller to handle advanced children display logic with collapsible functionality. + */ +final class CollapsiblePreferenceGroupController + implements PreferenceGroup.PreferenceInstanceStateCallback { + + private final PreferenceGroupAdapter mPreferenceGroupAdapter; + private int mMaxPreferenceToShow; + private final Context mContext; + + CollapsiblePreferenceGroupController(PreferenceGroup preferenceGroup, + PreferenceGroupAdapter preferenceGroupAdapter) { + mPreferenceGroupAdapter = preferenceGroupAdapter; + mMaxPreferenceToShow = preferenceGroup.getInitialExpandedChildrenCount(); + mContext = preferenceGroup.getContext(); + preferenceGroup.setPreferenceInstanceStateCallback(this); + } + + /** + * Creates the visible portion of the flattened preferences. + * + * @param flattenedPreferenceList the flattened children of the preference group + * @return the visible portion of the flattened preferences + */ + public List<Preference> createVisiblePreferencesList(List<Preference> flattenedPreferenceList) { + int visiblePreferenceCount = 0; + final List<Preference> visiblePreferenceList = + new ArrayList<>(flattenedPreferenceList.size()); + // Copy only the visible preferences to the active list up to the maximum specified + for (final Preference preference : flattenedPreferenceList) { + if (preference.isVisible()) { + if (visiblePreferenceCount < mMaxPreferenceToShow) { + visiblePreferenceList.add(preference); + } + // Do no count PreferenceGroup as expanded preference because the list of its child + // is already contained in the flattenedPreferenceList + if (!(preference instanceof PreferenceGroup)) { + visiblePreferenceCount++; + } + } + } + // If there are any visible preferences being hidden, add an expand button to show the rest + // of the preferences. Clicking the expand button will show all the visible preferences and + // reset mMaxPreferenceToShow + if (showLimitedChildren() && visiblePreferenceCount > mMaxPreferenceToShow) { + final ExpandButton expandButton = createExpandButton(visiblePreferenceList, + flattenedPreferenceList); + visiblePreferenceList.add(expandButton); + } + return visiblePreferenceList; + } + + /** + * Called when a preference has changed its visibility. + * + * @param preference The preference whose visibility has changed. + * @return {@code true} if view update has been handled by this controller. + */ + public boolean onPreferenceVisibilityChange(Preference preference) { + if (showLimitedChildren()) { + // We only want to show up to the max number of preferences. Preference visibility + // change can result in the expand button being added/removed, as well as expand button + // summary change. Rebulid the data to ensure the correct data is shown. + mPreferenceGroupAdapter.onPreferenceHierarchyChange(preference); + return true; + } + return false; + } + + @Override + public Parcelable saveInstanceState(Parcelable state) { + final SavedState myState = new SavedState(state); + myState.mMaxPreferenceToShow = mMaxPreferenceToShow; + return myState; + } + + @Override + public Parcelable restoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in saveInstanceState + return state; + } + SavedState myState = (SavedState) state; + final int restoredMaxToShow = myState.mMaxPreferenceToShow; + if (mMaxPreferenceToShow != restoredMaxToShow) { + mMaxPreferenceToShow = restoredMaxToShow; + mPreferenceGroupAdapter.onPreferenceHierarchyChange(null); + } + return myState.getSuperState(); + } + + private ExpandButton createExpandButton(List<Preference> visiblePreferenceList, + List<Preference> flattenedPreferenceList) { + final ExpandButton preference = new ExpandButton(mContext, visiblePreferenceList, + flattenedPreferenceList); + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + mMaxPreferenceToShow = Integer.MAX_VALUE; + mPreferenceGroupAdapter.onPreferenceHierarchyChange(preference); + return true; + } + }); + return preference; + } + + private boolean showLimitedChildren() { + return mMaxPreferenceToShow != Integer.MAX_VALUE; + } + + /** + * A {@link Preference} that provides capability to expand the collapsed items in the + * {@link PreferenceGroup}. + */ + static class ExpandButton extends Preference { + ExpandButton(Context context, List<Preference> visiblePreferenceList, + List<Preference> flattenedPreferenceList) { + super(context); + initLayout(); + setSummary(visiblePreferenceList, flattenedPreferenceList); + } + + private void initLayout() { + setLayoutResource(R.layout.expand_button); + setIcon(R.drawable.ic_arrow_down_24dp); + setTitle(R.string.expand_button_title); + // Sets a high order so that the expand button will be placed at the bottom of the group + setOrder(999); + } + + /* + * The summary of this will be the list of title for collapsed preferences. Iterate through + * the preferences not in the visible list and add its title to the summary text. + */ + private void setSummary(List<Preference> visiblePreferenceList, + List<Preference> flattenedPreferenceList) { + final Preference lastVisiblePreference = + visiblePreferenceList.get(visiblePreferenceList.size() - 1); + final int collapsedIndex = flattenedPreferenceList.indexOf(lastVisiblePreference) + 1; + CharSequence summary = null; + for (int i = collapsedIndex; i < flattenedPreferenceList.size(); i++) { + final Preference preference = flattenedPreferenceList.get(i); + if (preference instanceof PreferenceGroup) { + continue; + } + final CharSequence title = preference.getTitle(); + if (!TextUtils.isEmpty(title)) { + if (summary == null) { + summary = title; + } else { + summary = getContext().getString( + R.string.summary_collapsed_preference_list, summary, title); + } + } + } + setSummary(summary); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.setDividerAllowedAbove(false); + } + } + + /** + * A class for managing the instance state of a {@link PreferenceGroup}. + */ + static class SavedState extends Preference.BaseSavedState { + int mMaxPreferenceToShow; + + SavedState(Parcel source) { + super(source); + mMaxPreferenceToShow = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mMaxPreferenceToShow); + } + + SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/android/support/v7/preference/PreferenceGroup.java b/android/support/v7/preference/PreferenceGroup.java index d285ee67..a951e708 100644 --- a/android/support/v7/preference/PreferenceGroup.java +++ b/android/support/v7/preference/PreferenceGroup.java @@ -22,7 +22,9 @@ import android.content.Context; import android.content.res.TypedArray; import android.os.Bundle; import android.os.Handler; +import android.os.Parcelable; import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; import android.support.v4.content.res.TypedArrayUtils; import android.support.v4.util.SimpleArrayMap; import android.text.TextUtils; @@ -45,6 +47,7 @@ import java.util.List; * </div> * * @attr name android:orderingFromXml + * @attr name initialExpandedChildrenCount */ public abstract class PreferenceGroup extends Preference { /** @@ -60,6 +63,9 @@ public abstract class PreferenceGroup extends Preference { private boolean mAttachedToHierarchy = false; + private int mInitialExpandedChildrenCount = Integer.MAX_VALUE; + private PreferenceInstanceStateCallback mPreferenceInstanceStateCallback; + private final SimpleArrayMap<String, Long> mIdRecycleCache = new SimpleArrayMap<>(); private final Handler mHandler = new Handler(); private final Runnable mClearRecycleCacheRunnable = new Runnable() { @@ -83,6 +89,11 @@ public abstract class PreferenceGroup extends Preference { TypedArrayUtils.getBoolean(a, R.styleable.PreferenceGroup_orderingFromXml, R.styleable.PreferenceGroup_orderingFromXml, true); + if (a.hasValue(R.styleable.PreferenceGroup_initialExpandedChildrenCount)) { + mInitialExpandedChildrenCount = TypedArrayUtils.getInt( + a, R.styleable.PreferenceGroup_initialExpandedChildrenCount, + R.styleable.PreferenceGroup_initialExpandedChildrenCount, -1); + } a.recycle(); } @@ -120,6 +131,35 @@ public abstract class PreferenceGroup extends Preference { } /** + * Sets the maximal number of children that are shown when the preference group is launched + * where the rest of the children will be hidden. + * If some children are hidden an expand button will be provided to show all the hidden + * children. Any child in any level of the hierarchy that is also a preference group (e.g. + * preference category) will not be counted towards the limit. But instead the children of such + * group will be counted. + * By default, all children will be shown, so the default value of this attribute is equal to + * Integer.MAX_VALUE. + * + * @param expandedCount the number of children that is initially shown. + * + * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount + */ + public void setInitialExpandedChildrenCount(int expandedCount) { + mInitialExpandedChildrenCount = expandedCount; + } + + /** + * Gets the maximal number of children that is initially shown. + * + * @return the maximal number of children that is initially shown. + * + * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount + */ + public int getInitialExpandedChildrenCount() { + return mInitialExpandedChildrenCount; + } + + /** * Called by the inflater to add an item to this group. */ public void addItemFromInflater(Preference preference) { @@ -400,6 +440,44 @@ public abstract class PreferenceGroup extends Preference { } } + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (mPreferenceInstanceStateCallback != null) { + return mPreferenceInstanceStateCallback.saveInstanceState(superState); + } + return superState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (mPreferenceInstanceStateCallback != null) { + state = mPreferenceInstanceStateCallback.restoreInstanceState(state); + } + super.onRestoreInstanceState(state); + } + + /** + * Sets the instance state callback. + * + * @param callback The callback. + * @see #onSaveInstanceState() + * @see #onRestoreInstanceState() + */ + final void setPreferenceInstanceStateCallback(PreferenceInstanceStateCallback callback) { + mPreferenceInstanceStateCallback = callback; + } + + /** + * Gets the instance state callback. + * + * @return the instance state callback. + */ + @VisibleForTesting + final PreferenceInstanceStateCallback getPreferenceInstanceStateCallback() { + return mPreferenceInstanceStateCallback; + } + /** * Interface for PreferenceGroup Adapters to implement so that * {@link android.support.v14.preference.PreferenceFragment#scrollToPreference(String)} and @@ -426,4 +504,29 @@ public abstract class PreferenceGroup extends Preference { */ int getPreferenceAdapterPosition(Preference preference); } + + /** + * Interface for callback to implement so that they can save and restore the preference group's + * instance state. + */ + interface PreferenceInstanceStateCallback { + + /** + * Save the internal state that can later be used to create a new instance with that + * same state. + * + * @param state The Parcelable to save the current dynamic state. + */ + Parcelable saveInstanceState(Parcelable state); + + /** + * Restore the previously saved state from the given parcelable. + * + * @param state The Parcelable that holds the previously saved state. + * @return the super state if data has been saved in the state in {@link saveInstanceState} + * or state otherwise + */ + Parcelable restoreInstanceState(Parcelable state); + } + } diff --git a/android/support/v7/preference/PreferenceGroupAdapter.java b/android/support/v7/preference/PreferenceGroupAdapter.java index d1c630fe..00a0c5ba 100644 --- a/android/support/v7/preference/PreferenceGroupAdapter.java +++ b/android/support/v7/preference/PreferenceGroupAdapter.java @@ -22,6 +22,7 @@ import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Handler; import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; import android.support.v7.util.DiffUtil; @@ -73,7 +74,9 @@ public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewH private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout(); - private Handler mHandler = new Handler(); + private Handler mHandler; + + private CollapsiblePreferenceGroupController mPreferenceGroupController; private Runnable mSyncRunnable = new Runnable() { @Override @@ -117,7 +120,14 @@ public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewH } public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) { + this(preferenceGroup, new Handler()); + } + + private PreferenceGroupAdapter(PreferenceGroup preferenceGroup, Handler handler) { mPreferenceGroup = preferenceGroup; + mHandler = handler; + mPreferenceGroupController = + new CollapsiblePreferenceGroupController(preferenceGroup, this); // If this group gets or loses any children, let us know mPreferenceGroup.setOnPreferenceChangeInternalListener(this); @@ -134,6 +144,12 @@ public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewH syncMyPreferences(); } + @VisibleForTesting + static PreferenceGroupAdapter createInstanceWithCustomHandler(PreferenceGroup preferenceGroup, + Handler handler) { + return new PreferenceGroupAdapter(preferenceGroup, handler); + } + private void syncMyPreferences() { for (final Preference preference : mPreferenceListInternal) { // Clear out the listeners in anticipation of some items being removed. This listener @@ -143,13 +159,8 @@ public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewH final List<Preference> fullPreferenceList = new ArrayList<>(mPreferenceListInternal.size()); flattenPreferenceGroup(fullPreferenceList, mPreferenceGroup); - final List<Preference> visiblePreferenceList = new ArrayList<>(fullPreferenceList.size()); - // Copy only the visible preferences to the active list - for (final Preference preference : fullPreferenceList) { - if (preference.isVisible()) { - visiblePreferenceList.add(preference); - } - } + final List<Preference> visiblePreferenceList = + mPreferenceGroupController.createVisiblePreferencesList(fullPreferenceList); final List<Preference> oldVisibleList = mPreferenceList; mPreferenceList = visiblePreferenceList; @@ -277,6 +288,9 @@ public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewH if (!mPreferenceListInternal.contains(preference)) { return; } + if (mPreferenceGroupController.onPreferenceVisibilityChange(preference)) { + return; + } if (preference.isVisible()) { // The preference has become visible, we need to add it in the correct location. diff --git a/android/support/v7/recyclerview/extensions/ListAdapter.java b/android/support/v7/recyclerview/extensions/ListAdapter.java index 8035a271..e08cb53c 100644 --- a/android/support/v7/recyclerview/extensions/ListAdapter.java +++ b/android/support/v7/recyclerview/extensions/ListAdapter.java @@ -46,7 +46,7 @@ import java.util.List; * } * } * - * class MyActivity extends Activity implements LifecycleRegistryOwner { + * class MyActivity extends AppCompatActivity { * {@literal @}Override * public void onCreate(Bundle savedState) { * super.onCreate(savedState); diff --git a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java b/android/support/v7/recyclerview/extensions/ListAdapterHelper.java index e4a53fd9..b47b833a 100644 --- a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java +++ b/android/support/v7/recyclerview/extensions/ListAdapterHelper.java @@ -55,7 +55,7 @@ import java.util.List; * } * } * - * class MyActivity extends Activity implements LifecycleRegistryOwner { + * class MyActivity extends AppCompatActivity { * {@literal @}Override * public void onCreate(Bundle savedState) { * super.onCreate(savedState); diff --git a/android/support/v7/view/menu/CascadingMenuPopup.java b/android/support/v7/view/menu/CascadingMenuPopup.java index 73499cff..564bbfca 100644 --- a/android/support/v7/view/menu/CascadingMenuPopup.java +++ b/android/support/v7/view/menu/CascadingMenuPopup.java @@ -404,14 +404,14 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey final boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT; mLastPosition = nextMenuPosition; - final int parentOffsetLeft; - final int parentOffsetTop; + final int parentOffsetX; + final int parentOffsetY; if (Build.VERSION.SDK_INT >= 26) { // Anchor the submenu directly to the parent menu item view. This allows for // accurate submenu positioning when the parent menu is being moved. popupWindow.setAnchorView(parentView); - parentOffsetLeft = 0; - parentOffsetTop = 0; + parentOffsetX = 0; + parentOffsetY = 0; } else { // Framework does not allow anchoring to a view in another popup window. Use the // same top-level anchor as the parent menu is using, with appropriate offsets. @@ -428,10 +428,19 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey final int[] parentViewScreenLocation = new int[2]; parentView.getLocationOnScreen(parentViewScreenLocation); + // For Gravity.LEFT case, the baseline is just the left border of the view. So we + // can use the X of the location directly. But for Gravity.RIGHT case, the baseline + // is the right border. So we need add view's width with the location to make the + // baseline as the right border correctly. + if ((mDropDownGravity & (Gravity.RIGHT | Gravity.LEFT)) == Gravity.RIGHT) { + anchorScreenLocation[0] += mAnchorView.getWidth(); + parentViewScreenLocation[0] += parentView.getWidth(); + } + // If used as horizontal/vertical offsets, these values would position the submenu // at the exact same position as the parent item. - parentOffsetLeft = parentViewScreenLocation[0] - anchorScreenLocation[0]; - parentOffsetTop = parentViewScreenLocation[1] - anchorScreenLocation[1]; + parentOffsetX = parentViewScreenLocation[0] - anchorScreenLocation[0]; + parentOffsetY = parentViewScreenLocation[1] - anchorScreenLocation[1]; } // Adjust the horizontal offset to display the submenu to the right or to the left @@ -441,22 +450,22 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey final int x; if ((mDropDownGravity & Gravity.RIGHT) == Gravity.RIGHT) { if (showOnRight) { - x = parentOffsetLeft + menuWidth; + x = parentOffsetX + menuWidth; } else { - x = parentOffsetLeft - parentView.getWidth(); + x = parentOffsetX - parentView.getWidth(); } } else { if (showOnRight) { - x = parentOffsetLeft + parentView.getWidth(); + x = parentOffsetX + parentView.getWidth(); } else { - x = parentOffsetLeft - menuWidth; + x = parentOffsetX - menuWidth; } } popupWindow.setHorizontalOffset(x); // Vertically align with the parent item. popupWindow.setOverlapAnchor(true); - popupWindow.setVerticalOffset(parentOffsetTop); + popupWindow.setVerticalOffset(parentOffsetY); } else { if (mHasXOffset) { popupWindow.setHorizontalOffset(mXOffset); diff --git a/android/support/v7/widget/AppCompatImageButton.java b/android/support/v7/widget/AppCompatImageButton.java index 90e6aa9f..b2b1f106 100644 --- a/android/support/v7/widget/AppCompatImageButton.java +++ b/android/support/v7/widget/AppCompatImageButton.java @@ -23,7 +23,6 @@ import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; @@ -102,14 +101,6 @@ public class AppCompatImageButton extends ImageButton implements TintableBackgro } @Override - public void setImageIcon(@Nullable Icon icon) { - super.setImageIcon(icon); - if (mImageHelper != null) { - mImageHelper.applySupportImageTint(); - } - } - - @Override public void setImageURI(@Nullable Uri uri) { super.setImageURI(uri); if (mImageHelper != null) { diff --git a/android/support/v7/widget/AppCompatImageView.java b/android/support/v7/widget/AppCompatImageView.java index 0844f9a5..f50799ee 100644 --- a/android/support/v7/widget/AppCompatImageView.java +++ b/android/support/v7/widget/AppCompatImageView.java @@ -23,7 +23,6 @@ import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; @@ -112,14 +111,6 @@ public class AppCompatImageView extends ImageView implements TintableBackgroundV } @Override - public void setImageIcon(@Nullable Icon icon) { - super.setImageIcon(icon); - if (mImageHelper != null) { - mImageHelper.applySupportImageTint(); - } - } - - @Override public void setImageURI(@Nullable Uri uri) { super.setImageURI(uri); if (mImageHelper != null) { diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java index dea8546f..7009733c 100644 --- a/android/support/v7/widget/RecyclerView.java +++ b/android/support/v7/widget/RecyclerView.java @@ -44,6 +44,7 @@ import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import android.support.v4.os.TraceCompat; +import android.support.v4.util.Preconditions; import android.support.v4.view.AbsSavedState; import android.support.v4.view.InputDeviceCompat; import android.support.v4.view.MotionEventCompat; @@ -417,6 +418,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro */ private int mDispatchScrollCounter = 0; + @NonNull + private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory(); private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow; ItemAnimator mItemAnimator = new DefaultItemAnimator(); @@ -2306,7 +2309,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (mLeftGlow != null) { return; } - mLeftGlow = new EdgeEffect(getContext()); + mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT); if (mClipToPadding) { mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); @@ -2319,7 +2322,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (mRightGlow != null) { return; } - mRightGlow = new EdgeEffect(getContext()); + mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT); if (mClipToPadding) { mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); @@ -2332,7 +2335,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (mTopGlow != null) { return; } - mTopGlow = new EdgeEffect(getContext()); + mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP); if (mClipToPadding) { mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); @@ -2346,7 +2349,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (mBottomGlow != null) { return; } - mBottomGlow = new EdgeEffect(getContext()); + mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM); if (mClipToPadding) { mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); @@ -2360,6 +2363,32 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** + * Set a {@link EdgeEffectFactory} for this {@link RecyclerView}. + * <p> + * When a new {@link EdgeEffectFactory} is set, any existing over-scroll effects are cleared + * and new effects are created as needed using + * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int)} + * + * @param edgeEffectFactory The {@link EdgeEffectFactory} instance. + */ + public void setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) { + Preconditions.checkNotNull(edgeEffectFactory); + mEdgeEffectFactory = edgeEffectFactory; + invalidateGlows(); + } + + /** + * Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing + * was set. + * + * @return The previously set {@link EdgeEffectFactory} + * @see #setEdgeEffectFactory(EdgeEffectFactory) + */ + public EdgeEffectFactory getEdgeEffectFactory() { + return mEdgeEffectFactory; + } + + /** * Since RecyclerView is a collection ViewGroup that includes virtual children (items that are * in the Adapter but not visible in the UI), it employs a more involved focus search strategy * that differs from other ViewGroups. @@ -5130,6 +5159,46 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** + * EdgeEffectFactory lets you customize the over-scroll edge effect for RecyclerViews. + * + * @see RecyclerView#setEdgeEffectFactory(EdgeEffectFactory) + */ + public static class EdgeEffectFactory { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM}) + public @interface EdgeDirection {} + + /** + * Direction constant for the left edge + */ + public static final int DIRECTION_LEFT = 0; + + /** + * Direction constant for the top edge + */ + public static final int DIRECTION_TOP = 1; + + /** + * Direction constant for the right edge + */ + public static final int DIRECTION_RIGHT = 2; + + /** + * Direction constant for the bottom edge + */ + public static final int DIRECTION_BOTTOM = 3; + + /** + * Create a new EdgeEffect for the provided direction. + */ + protected @NonNull EdgeEffect createEdgeEffect(RecyclerView view, + @EdgeDirection int direction) { + return new EdgeEffect(view.getContext()); + } + } + + /** * RecycledViewPool lets you share Views between multiple RecyclerViews. * <p> * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool diff --git a/android/telecom/DefaultDialerManager.java b/android/telecom/DefaultDialerManager.java index 2a707c91..1806aee2 100644 --- a/android/telecom/DefaultDialerManager.java +++ b/android/telecom/DefaultDialerManager.java @@ -22,6 +22,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Process; +import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; @@ -163,7 +164,10 @@ public class DefaultDialerManager { for (ResolveInfo resolveInfo : resolveInfoList) { final ActivityInfo activityInfo = resolveInfo.activityInfo; - if (activityInfo != null && !packageNames.contains(activityInfo.packageName)) { + if (activityInfo != null + && !packageNames.contains(activityInfo.packageName) + // ignore cross profile intent handler + && resolveInfo.targetUserId == UserHandle.USER_CURRENT) { packageNames.add(activityInfo.packageName); } } diff --git a/android/telephony/MbmsDownloadManager.java b/android/telephony/MbmsDownloadManager.java deleted file mode 100644 index 1e8cf185..00000000 --- a/android/telephony/MbmsDownloadManager.java +++ /dev/null @@ -1,586 +0,0 @@ -/* - * Copyright (C) 2016 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 android.telephony; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SdkConstant; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.IBinder; -import android.os.RemoteException; -import android.telephony.mbms.DownloadProgressListener; -import android.telephony.mbms.FileInfo; -import android.telephony.mbms.DownloadRequest; -import android.telephony.mbms.MbmsDownloadManagerCallback; -import android.telephony.mbms.MbmsDownloadReceiver; -import android.telephony.mbms.MbmsException; -import android.telephony.mbms.MbmsTempFileProvider; -import android.telephony.mbms.MbmsUtils; -import android.telephony.mbms.vendor.IMbmsDownloadService; -import android.util.Log; - -import java.io.File; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; - -/** @hide */ -public class MbmsDownloadManager { - private static final String LOG_TAG = MbmsDownloadManager.class.getSimpleName(); - - /** @hide */ - // TODO: systemapi - @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) - public static final String MBMS_DOWNLOAD_SERVICE_ACTION = - "android.telephony.action.EmbmsDownload"; - - /** - * Integer extra indicating the result code of the download. One of - * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, or {@link #RESULT_CANCELLED}. - */ - public static final String EXTRA_RESULT = "android.telephony.mbms.extra.RESULT"; - - /** - * Extra containing the {@link android.telephony.mbms.FileInfo} for which the download result - * is for. Must not be null. - */ - public static final String EXTRA_FILE_INFO = "android.telephony.mbms.extra.FILE_INFO"; - - /** - * Extra containing a single {@link Uri} indicating the location of the successfully - * downloaded file. Set on the intent provided via - * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}. - * Will always be set to a non-null value if {@link #EXTRA_RESULT} is set to - * {@link #RESULT_SUCCESSFUL}. - */ - public static final String EXTRA_COMPLETED_FILE_URI = - "android.telephony.mbms.extra.COMPLETED_FILE_URI"; - - public static final int RESULT_SUCCESSFUL = 1; - public static final int RESULT_CANCELLED = 2; - public static final int RESULT_EXPIRED = 3; - public static final int RESULT_IO_ERROR = 4; - // TODO - more results! - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({STATUS_UNKNOWN, STATUS_ACTIVELY_DOWNLOADING, STATUS_PENDING_DOWNLOAD, - STATUS_PENDING_REPAIR, STATUS_PENDING_DOWNLOAD_WINDOW}) - public @interface DownloadStatus {} - - public static final int STATUS_UNKNOWN = 0; - public static final int STATUS_ACTIVELY_DOWNLOADING = 1; - public static final int STATUS_PENDING_DOWNLOAD = 2; - public static final int STATUS_PENDING_REPAIR = 3; - public static final int STATUS_PENDING_DOWNLOAD_WINDOW = 4; - - private static AtomicBoolean sIsInitialized = new AtomicBoolean(false); - - private final Context mContext; - private int mSubscriptionId = INVALID_SUBSCRIPTION_ID; - private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST, "Received death notification"); - } - }; - - private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null); - private final MbmsDownloadManagerCallback mCallback; - - private MbmsDownloadManager(Context context, MbmsDownloadManagerCallback callback, int subId) { - mContext = context; - mCallback = callback; - mSubscriptionId = subId; - } - - /** - * Create a new MbmsDownloadManager using the system default data subscription ID. - * See {@link #create(Context, MbmsDownloadManagerCallback, int)} - * - * @hide - */ - public static MbmsDownloadManager create(Context context, - MbmsDownloadManagerCallback listener) - throws MbmsException { - return create(context, listener, SubscriptionManager.getDefaultSubscriptionId()); - } - - /** - * Create a new MbmsDownloadManager using the given subscription ID. - * - * Note that this call will bind a remote service and that may take a bit. The instance of - * {@link MbmsDownloadManager} that is returned will not be ready for use until - * {@link MbmsDownloadManagerCallback#middlewareReady()} is called on the provided callback. - * If you attempt to use the manager before it is ready, a {@link MbmsException} will be thrown. - * - * This also may throw an {@link IllegalArgumentException} or an {@link IllegalStateException}. - * - * You may only have one instance of {@link MbmsDownloadManager} per UID. If you call this - * method while there is an active instance of {@link MbmsDownloadManager} in your process - * (in other words, one that has not had {@link #dispose()} called on it), this method will - * throw an {@link MbmsException}. If you call this method in a different process - * running under the same UID, an error will be indicated via - * {@link MbmsDownloadManagerCallback#error(int, String)}. - * - * Note that initialization may fail asynchronously. If you wish to try again after you - * receive such an asynchronous error, you must call dispose() on the instance of - * {@link MbmsDownloadManager} that you received before calling this method again. - * - * @param context The instance of {@link Context} to use - * @param listener A callback to get asynchronous error messages and file service updates. - * @param subscriptionId The data subscription ID to use - * @hide - */ - public static MbmsDownloadManager create(Context context, - MbmsDownloadManagerCallback listener, int subscriptionId) - throws MbmsException { - if (!sIsInitialized.compareAndSet(false, true)) { - throw new MbmsException(MbmsException.InitializationErrors.ERROR_DUPLICATE_INITIALIZE); - } - MbmsDownloadManager mdm = new MbmsDownloadManager(context, listener, subscriptionId); - try { - mdm.bindAndInitialize(); - } catch (MbmsException e) { - sIsInitialized.set(false); - throw e; - } - return mdm; - } - - private void bindAndInitialize() throws MbmsException { - MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION, - new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - IMbmsDownloadService downloadService = - IMbmsDownloadService.Stub.asInterface(service); - int result; - try { - result = downloadService.initialize(mSubscriptionId, mCallback); - } catch (RemoteException e) { - Log.e(LOG_TAG, "Service died before initialization"); - sIsInitialized.set(false); - return; - } catch (RuntimeException e) { - Log.e(LOG_TAG, "Runtime exception during initialization"); - sendErrorToApp( - MbmsException.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, - e.toString()); - sIsInitialized.set(false); - return; - } - if (result != MbmsException.SUCCESS) { - sendErrorToApp(result, "Error returned during initialization"); - sIsInitialized.set(false); - return; - } - try { - downloadService.asBinder().linkToDeath(mDeathRecipient, 0); - } catch (RemoteException e) { - sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST, - "Middleware lost during initialization"); - sIsInitialized.set(false); - return; - } - mService.set(downloadService); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - sIsInitialized.set(false); - mService.set(null); - } - }); - } - - /** - * An inspection API to retrieve the list of available - * {@link android.telephony.mbms.FileServiceInfo}s currently being advertised. - * The results are returned asynchronously via a call to - * {@link MbmsDownloadManagerCallback#fileServicesUpdated(List)} - * - * The serviceClasses argument lets the app filter on types of programming and is opaque data - * negotiated beforehand between the app and the carrier. - * - * This may throw an {@link MbmsException} containing one of the following errors: - * {@link MbmsException#ERROR_MIDDLEWARE_NOT_BOUND} - * {@link MbmsException#ERROR_MIDDLEWARE_LOST} - * - * Asynchronous error codes via the {@link MbmsDownloadManagerCallback#error(int, String)} - * callback can include any of the errors except: - * {@link MbmsException.StreamingErrors#ERROR_UNABLE_TO_START_SERVICE} - * - * @param classList A list of service classes which the app wishes to receive - * {@link MbmsDownloadManagerCallback#fileServicesUpdated(List)} callbacks - * about. Subsequent calls to this method will replace this list of service - * classes (i.e. the middleware will no longer send updates for services - * matching classes only in the old list). - */ - public void getFileServices(List<String> classList) throws MbmsException { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); - } - try { - int returnCode = downloadService.getFileServices(mSubscriptionId, classList); - if (returnCode != MbmsException.SUCCESS) { - throw new MbmsException(returnCode); - } - } catch (RemoteException e) { - Log.w(LOG_TAG, "Remote process died"); - mService.set(null); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } - } - - /** - * Sets the temp file root for downloads. - * All temp files created for the middleware to write to will be contained in the specified - * directory. Applications that wish to specify a location only need to call this method once - * as long their data is persisted in storage -- the argument will be stored both in a - * local instance of {@link android.content.SharedPreferences} and by the middleware. - * - * If this method is not called at least once before calling - * {@link #download(DownloadRequest, DownloadProgressListener)}, the framework - * will default to a directory formed by the concatenation of the app's files directory and - * {@link android.telephony.mbms.MbmsTempFileProvider#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}. - * - * Before calling this method, the app must cancel all of its pending - * {@link DownloadRequest}s via {@link #cancelDownload(DownloadRequest)}. If this is not done, - * an {@link MbmsException} will be thrown with code - * {@link MbmsException.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} unless the - * provided directory is the same as what has been previously configured. - * - * The {@link File} supplied as a root temp file directory must already exist. If not, an - * {@link IllegalArgumentException} will be thrown. - * @param tempFileRootDirectory A directory to place temp files in. - */ - public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory) - throws MbmsException { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); - } - if (!tempFileRootDirectory.exists()) { - throw new IllegalArgumentException("Provided directory does not exist"); - } - if (!tempFileRootDirectory.isDirectory()) { - throw new IllegalArgumentException("Provided File is not a directory"); - } - String filePath; - try { - filePath = tempFileRootDirectory.getCanonicalPath(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e); - } - - try { - int result = downloadService.setTempFileRootDirectory(mSubscriptionId, filePath); - if (result != MbmsException.SUCCESS) { - throw new MbmsException(result); - } - } catch (RemoteException e) { - mService.set(null); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } - - SharedPreferences prefs = mContext.getSharedPreferences( - MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); - prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply(); - } - - /** - * Retrieves the currently configured temp file root directory. Returns the file that was - * configured via {@link #setTempFileRootDirectory(File)} or the default directory - * {@link #download(DownloadRequest, DownloadProgressListener)} was called without ever setting - * the temp file root. If neither method has been called since the last time the app's shared - * preferences were reset, returns null. - * - * @return A {@link File} pointing to the configured temp file directory, or null if not yet - * configured. - */ - public @Nullable File getTempFileRootDirectory() { - SharedPreferences prefs = mContext.getSharedPreferences( - MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); - String path = prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null); - if (path != null) { - return new File(path); - } - return null; - } - - /** - * Requests a download of a file that is available via multicast. - * - * downloadListener is an optional callback object which can be used to get progress reports - * of a currently occuring download. Note this can only run while the calling app - * is running, so future downloads will simply result in resultIntents being sent - * for completed or errored-out downloads. A NULL indicates no callbacks are needed. - * - * May throw an {@link IllegalArgumentException} - * - * If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed, - * this method will create a directory at the default location defined at - * {@link MbmsTempFileProvider#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp - * file root directory. - * - * Asynchronous errors through the listener include any of the errors - * - * @param request The request that specifies what should be downloaded - * @param progressListener Optional listener that will be provided progress updates - * if the app is running. - */ - public void download(DownloadRequest request, DownloadProgressListener progressListener) - throws MbmsException { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); - } - - // Check to see whether the app's set a temp root dir yet, and set it if not. - SharedPreferences prefs = mContext.getSharedPreferences( - MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); - if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) { - File tempRootDirectory = new File(mContext.getFilesDir(), - MbmsTempFileProvider.DEFAULT_TOP_LEVEL_TEMP_DIRECTORY); - tempRootDirectory.mkdirs(); - setTempFileRootDirectory(tempRootDirectory); - } - - checkValidDownloadDestination(request); - writeDownloadRequestToken(request); - try { - downloadService.download(request, progressListener); - } catch (RemoteException e) { - mService.set(null); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } - } - - /** - * Returns a list of pending {@link DownloadRequest}s that originated from this application. - * A pending request is one that was issued via - * {@link #download(DownloadRequest, DownloadProgressListener)} but not cancelled through - * {@link #cancelDownload(DownloadRequest)}. - * @return A list, possibly empty, of {@link DownloadRequest}s - */ - public @NonNull List<DownloadRequest> listPendingDownloads() throws MbmsException { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); - } - - try { - return downloadService.listPendingDownloads(mSubscriptionId); - } catch (RemoteException e) { - mService.set(null); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } - } - - /** - * Attempts to cancel the specified {@link DownloadRequest}. - * - * If the middleware is not aware of the specified download request, an MbmsException will be - * thrown with error code {@link MbmsException.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST}. - * - * If this method returns without throwing an exception, you may assume that cancellation - * was successful. - * @param downloadRequest The download request that you wish to cancel. - */ - public void cancelDownload(DownloadRequest downloadRequest) throws MbmsException { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); - } - - try { - int result = downloadService.cancelDownload(downloadRequest); - if (result != MbmsException.SUCCESS) { - throw new MbmsException(result); - } - } catch (RemoteException e) { - mService.set(null); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } - deleteDownloadRequestToken(downloadRequest); - } - - /** - * Gets information about the status of a file pending download. - * - * If the middleware has not yet been properly initialized or if it has no records of the - * file indicated by {@code fileInfo} being associated with {@code downloadRequest}, - * {@link #STATUS_UNKNOWN} will be returned. - * - * @param downloadRequest The download request to query. - * @param fileInfo The particular file within the request to get information on. - * @return The status of the download. - */ - @DownloadStatus - public int getDownloadStatus(DownloadRequest downloadRequest, FileInfo fileInfo) - throws MbmsException { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); - } - - try { - return downloadService.getDownloadStatus(downloadRequest, fileInfo); - } catch (RemoteException e) { - mService.set(null); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } - } - - /** - * Resets the middleware's knowledge of previously-downloaded files in this download request. - * - * Normally, the middleware keeps track of the hashes of downloaded files and won't re-download - * files whose server-reported hash matches one of the already-downloaded files. This means - * that if the file is accidentally deleted by the user or by the app, the middleware will - * not try to download it again. - * This method will reset the middleware's cache of hashes for the provided - * {@link DownloadRequest}, so that previously downloaded content will be downloaded again - * when available. - * This will not interrupt in-progress downloads. - * - * If the middleware is not aware of the specified download request, an MbmsException will be - * thrown with error code {@link MbmsException.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST}. - * - * May throw a {@link MbmsException} with error code - * @param downloadRequest The request to re-download files for. - */ - public void resetDownloadKnowledge(DownloadRequest downloadRequest) throws MbmsException { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); - } - - try { - int result = downloadService.resetDownloadKnowledge(downloadRequest); - if (result != MbmsException.SUCCESS) { - throw new MbmsException(result); - } - } catch (RemoteException e) { - mService.set(null); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } - } - - public void dispose() { - try { - IMbmsDownloadService downloadService = mService.get(); - if (downloadService == null) { - Log.i(LOG_TAG, "Service already dead"); - return; - } - downloadService.dispose(mSubscriptionId); - } catch (RemoteException e) { - // Ignore - Log.i(LOG_TAG, "Remote exception while disposing of service"); - } finally { - mService.set(null); - sIsInitialized.set(false); - } - } - - private void writeDownloadRequestToken(DownloadRequest request) { - File token = getDownloadRequestTokenPath(request); - if (!token.getParentFile().exists()) { - token.getParentFile().mkdirs(); - } - if (token.exists()) { - Log.w(LOG_TAG, "Download token " + token.getName() + " already exists"); - return; - } - try { - if (!token.createNewFile()) { - throw new RuntimeException("Failed to create download token for request " - + request); - } - } catch (IOException e) { - throw new RuntimeException("Failed to create download token for request " + request - + " due to IOException " + e); - } - } - - private void deleteDownloadRequestToken(DownloadRequest request) { - File token = getDownloadRequestTokenPath(request); - if (!token.isFile()) { - Log.w(LOG_TAG, "Attempting to delete non-existent download token at " + token); - return; - } - if (!token.delete()) { - Log.w(LOG_TAG, "Couldn't delete download token at " + token); - } - } - - private File getDownloadRequestTokenPath(DownloadRequest request) { - File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext, - request.getFileServiceId()); - String downloadTokenFileName = request.getHash() - + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX; - return new File(tempFileLocation, downloadTokenFileName); - } - - /** - * Verifies the following: - * If a request is multi-part, - * 1. Destination Uri must exist and be a directory - * 2. Directory specified must contain no files. - * Otherwise - * 1. The file specified by the destination Uri must not exist. - */ - private void checkValidDownloadDestination(DownloadRequest request) { - File toFile = new File(request.getDestinationUri().getSchemeSpecificPart()); - if (request.isMultipartDownload()) { - if (!toFile.isDirectory()) { - throw new IllegalArgumentException("Multipart download must specify valid " + - "destination directory."); - } - if (toFile.listFiles().length > 0) { - throw new IllegalArgumentException("Destination directory must be clear of all " + - "files."); - } - } else { - if (toFile.exists()) { - throw new IllegalArgumentException("Destination file must not exist."); - } - } - } - - private void sendErrorToApp(int errorCode, String message) { - try { - mCallback.error(errorCode, message); - } catch (RemoteException e) { - // Ignore, should not happen locally. - } - } -} diff --git a/android/telephony/MbmsDownloadSession.java b/android/telephony/MbmsDownloadSession.java new file mode 100644 index 00000000..ebac0419 --- /dev/null +++ b/android/telephony/MbmsDownloadSession.java @@ -0,0 +1,773 @@ +/* + * Copyright (C) 2016 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 android.telephony; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SystemApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.telephony.mbms.DownloadStateCallback; +import android.telephony.mbms.FileInfo; +import android.telephony.mbms.DownloadRequest; +import android.telephony.mbms.InternalDownloadSessionCallback; +import android.telephony.mbms.InternalDownloadStateCallback; +import android.telephony.mbms.MbmsDownloadSessionCallback; +import android.telephony.mbms.MbmsDownloadReceiver; +import android.telephony.mbms.MbmsErrors; +import android.telephony.mbms.MbmsTempFileProvider; +import android.telephony.mbms.MbmsUtils; +import android.telephony.mbms.vendor.IMbmsDownloadService; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +/** + * This class provides functionality for file download over MBMS. + */ +public class MbmsDownloadSession implements AutoCloseable { + private static final String LOG_TAG = MbmsDownloadSession.class.getSimpleName(); + + /** + * Service action which must be handled by the middleware implementing the MBMS file download + * interface. + * @hide + */ + @SystemApi + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String MBMS_DOWNLOAD_SERVICE_ACTION = + "android.telephony.action.EmbmsDownload"; + + /** + * Integer extra that Android will attach to the intent supplied via + * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} + * Indicates the result code of the download. One of + * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, {@link #RESULT_CANCELLED}, or + * {@link #RESULT_IO_ERROR}. + * + * This extra may also be used by the middleware when it is sending intents to the app. + */ + public static final String EXTRA_MBMS_DOWNLOAD_RESULT = + "android.telephony.extra.MBMS_DOWNLOAD_RESULT"; + + /** + * {@link FileInfo} extra that Android will attach to the intent supplied via + * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} + * Indicates the file for which the download result is for. Never null. + * + * This extra may also be used by the middleware when it is sending intents to the app. + */ + public static final String EXTRA_MBMS_FILE_INFO = "android.telephony.extra.MBMS_FILE_INFO"; + + /** + * {@link Uri} extra that Android will attach to the intent supplied via + * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} + * Indicates the location of the successfully downloaded file within the temp file root set + * via {@link #setTempFileRootDirectory(File)}. + * While you may use this file in-place, it is highly encouraged that you move + * this file to a different location after receiving the download completion intent, as this + * file resides within the temp file directory. + * + * Will always be set to a non-null value if + * {@link #EXTRA_MBMS_DOWNLOAD_RESULT} is set to {@link #RESULT_SUCCESSFUL}. + */ + public static final String EXTRA_MBMS_COMPLETED_FILE_URI = + "android.telephony.extra.MBMS_COMPLETED_FILE_URI"; + + /** + * Extra containing the {@link DownloadRequest} for which the download result or file + * descriptor request is for. Must not be null. + */ + public static final String EXTRA_MBMS_DOWNLOAD_REQUEST = + "android.telephony.extra.MBMS_DOWNLOAD_REQUEST"; + + /** + * The default directory name for all MBMS temp files. If you call + * {@link #download(DownloadRequest)} without first calling + * {@link #setTempFileRootDirectory(File)}, this directory will be created for you under the + * path returned by {@link Context#getFilesDir()}. + */ + public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot"; + + /** + * Indicates that the download was successful. + */ + public static final int RESULT_SUCCESSFUL = 1; + + /** + * Indicates that the download was cancelled via {@link #cancelDownload(DownloadRequest)}. + */ + public static final int RESULT_CANCELLED = 2; + + /** + * Indicates that the download will not be completed due to the expiration of its download + * window on the carrier's network. + */ + public static final int RESULT_EXPIRED = 3; + + /** + * Indicates that the download will not be completed due to an I/O error incurred while + * writing to temp files. This commonly indicates that the device is out of storage space, + * but may indicate other conditions as well (such as an SD card being removed). + */ + public static final int RESULT_IO_ERROR = 4; + // TODO - more results! + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATUS_UNKNOWN, STATUS_ACTIVELY_DOWNLOADING, STATUS_PENDING_DOWNLOAD, + STATUS_PENDING_REPAIR, STATUS_PENDING_DOWNLOAD_WINDOW}) + public @interface DownloadStatus {} + + /** + * Indicates that the middleware has no information on the file. + */ + public static final int STATUS_UNKNOWN = 0; + + /** + * Indicates that the file is actively downloading. + */ + public static final int STATUS_ACTIVELY_DOWNLOADING = 1; + + /** + * TODO: I don't know... + */ + public static final int STATUS_PENDING_DOWNLOAD = 2; + + /** + * Indicates that the file is being repaired after the download being interrupted. + */ + public static final int STATUS_PENDING_REPAIR = 3; + + /** + * Indicates that the file is waiting to download because its download window has not yet + * started. + */ + public static final int STATUS_PENDING_DOWNLOAD_WINDOW = 4; + + private static AtomicBoolean sIsInitialized = new AtomicBoolean(false); + + private final Context mContext; + private int mSubscriptionId = INVALID_SUBSCRIPTION_ID; + private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { + @Override + public void binderDied() { + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification"); + } + }; + + private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null); + private final InternalDownloadSessionCallback mInternalCallback; + private final Map<DownloadStateCallback, InternalDownloadStateCallback> + mInternalDownloadCallbacks = new HashMap<>(); + + private MbmsDownloadSession(Context context, MbmsDownloadSessionCallback callback, + int subscriptionId, Handler handler) { + mContext = context; + mSubscriptionId = subscriptionId; + if (handler == null) { + handler = new Handler(Looper.getMainLooper()); + } + mInternalCallback = new InternalDownloadSessionCallback(callback, handler); + } + + /** + * Create a new {@link MbmsDownloadSession} using the system default data subscription ID. + * See {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)} + */ + public static MbmsDownloadSession create(@NonNull Context context, + @NonNull MbmsDownloadSessionCallback callback, @NonNull Handler handler) { + return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler); + } + + /** + * Create a new MbmsDownloadManager using the given subscription ID. + * + * Note that this call will bind a remote service and that may take a bit. The instance of + * {@link MbmsDownloadSession} that is returned will not be ready for use until + * {@link MbmsDownloadSessionCallback#onMiddlewareReady()} is called on the provided callback. + * If you attempt to use the instance before it is ready, an {@link IllegalStateException} + * will be thrown or an error will be delivered through + * {@link MbmsDownloadSessionCallback#onError(int, String)}. + * + * This also may throw an {@link IllegalArgumentException}. + * + * You may only have one instance of {@link MbmsDownloadSession} per UID. If you call this + * method while there is an active instance of {@link MbmsDownloadSession} in your process + * (in other words, one that has not had {@link #close()} called on it), this method will + * throw an {@link IllegalStateException}. If you call this method in a different process + * running under the same UID, an error will be indicated via + * {@link MbmsDownloadSessionCallback#onError(int, String)}. + * + * Note that initialization may fail asynchronously. If you wish to try again after you + * receive such an asynchronous error, you must call {@link #close()} on the instance of + * {@link MbmsDownloadSession} that you received before calling this method again. + * + * @param context The instance of {@link Context} to use + * @param callback A callback to get asynchronous error messages and file service updates. + * @param subscriptionId The data subscription ID to use + * @param handler The {@link Handler} on which callbacks should be enqueued. + * @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during + * setup. + */ + public static @Nullable MbmsDownloadSession create(@NonNull Context context, + final @NonNull MbmsDownloadSessionCallback callback, + int subscriptionId, @NonNull Handler handler) { + if (!sIsInitialized.compareAndSet(false, true)) { + throw new IllegalStateException("Cannot have two active instances"); + } + MbmsDownloadSession session = + new MbmsDownloadSession(context, callback, subscriptionId, handler); + final int result = session.bindAndInitialize(); + if (result != MbmsErrors.SUCCESS) { + sIsInitialized.set(false); + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(result, null); + } + }); + return null; + } + return session; + } + + private int bindAndInitialize() { + return MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION, + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + IMbmsDownloadService downloadService = + IMbmsDownloadService.Stub.asInterface(service); + int result; + try { + result = downloadService.initialize(mSubscriptionId, mInternalCallback); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Service died before initialization"); + sIsInitialized.set(false); + return; + } catch (RuntimeException e) { + Log.e(LOG_TAG, "Runtime exception during initialization"); + sendErrorToApp( + MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, + e.toString()); + sIsInitialized.set(false); + return; + } + if (result != MbmsErrors.SUCCESS) { + sendErrorToApp(result, "Error returned during initialization"); + sIsInitialized.set(false); + return; + } + try { + downloadService.asBinder().linkToDeath(mDeathRecipient, 0); + } catch (RemoteException e) { + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, + "Middleware lost during initialization"); + sIsInitialized.set(false); + return; + } + mService.set(downloadService); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + sIsInitialized.set(false); + mService.set(null); + } + }); + } + + /** + * An inspection API to retrieve the list of available + * {@link android.telephony.mbms.FileServiceInfo}s currently being advertised. + * The results are returned asynchronously via a call to + * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)} + * + * Asynchronous error codes via the {@link MbmsDownloadSessionCallback#onError(int, String)} + * callback may include any of the errors that are not specific to the streaming use-case. + * + * May throw an {@link IllegalStateException} or {@link IllegalArgumentException}. + * + * @param classList A list of service classes which the app wishes to receive + * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)} callbacks + * about. Subsequent calls to this method will replace this list of service + * classes (i.e. the middleware will no longer send updates for services + * matching classes only in the old list). + * Values in this list should be negotiated with the wireless carrier prior + * to using this API. + */ + public void requestUpdateFileServices(@NonNull List<String> classList) { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + try { + int returnCode = downloadService.requestUpdateFileServices(mSubscriptionId, classList); + if (returnCode != MbmsErrors.SUCCESS) { + sendErrorToApp(returnCode, null); + } + } catch (RemoteException e) { + Log.w(LOG_TAG, "Remote process died"); + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + } + } + + /** + * Sets the temp file root for downloads. + * All temp files created for the middleware to write to will be contained in the specified + * directory. Applications that wish to specify a location only need to call this method once + * as long their data is persisted in storage -- the argument will be stored both in a + * local instance of {@link android.content.SharedPreferences} and by the middleware. + * + * If this method is not called at least once before calling + * {@link #download(DownloadRequest)}, the framework + * will default to a directory formed by the concatenation of the app's files directory and + * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}. + * + * Before calling this method, the app must cancel all of its pending + * {@link DownloadRequest}s via {@link #cancelDownload(DownloadRequest)}. If this is not done, + * you will receive an asynchronous error with code + * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} unless the + * provided directory is the same as what has been previously configured. + * + * The {@link File} supplied as a root temp file directory must already exist. If not, an + * {@link IllegalArgumentException} will be thrown. In addition, as an additional sanity + * check, an {@link IllegalArgumentException} will be thrown if you attempt to set the temp + * file root directory to one of your data roots (the value of {@link Context#getDataDir()}, + * {@link Context#getFilesDir()}, or {@link Context#getCacheDir()}). + * @param tempFileRootDirectory A directory to place temp files in. + */ + public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory) { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + try { + validateTempFileRootSanity(tempFileRootDirectory); + } catch (IOException e) { + throw new IllegalStateException("Got IOException checking directory sanity"); + } + String filePath; + try { + filePath = tempFileRootDirectory.getCanonicalPath(); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e); + } + + try { + int result = downloadService.setTempFileRootDirectory(mSubscriptionId, filePath); + if (result != MbmsErrors.SUCCESS) { + sendErrorToApp(result, null); + } + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + return; + } + + SharedPreferences prefs = mContext.getSharedPreferences( + MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); + prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply(); + } + + private void validateTempFileRootSanity(File tempFileRootDirectory) throws IOException { + if (!tempFileRootDirectory.exists()) { + throw new IllegalArgumentException("Provided directory does not exist"); + } + if (!tempFileRootDirectory.isDirectory()) { + throw new IllegalArgumentException("Provided File is not a directory"); + } + String canonicalTempFilePath = tempFileRootDirectory.getCanonicalPath(); + if (mContext.getDataDir().getCanonicalPath().equals(canonicalTempFilePath)) { + throw new IllegalArgumentException("Temp file root cannot be your data dir"); + } + if (mContext.getCacheDir().getCanonicalPath().equals(canonicalTempFilePath)) { + throw new IllegalArgumentException("Temp file root cannot be your cache dir"); + } + if (mContext.getFilesDir().getCanonicalPath().equals(canonicalTempFilePath)) { + throw new IllegalArgumentException("Temp file root cannot be your files dir"); + } + } + /** + * Retrieves the currently configured temp file root directory. Returns the file that was + * configured via {@link #setTempFileRootDirectory(File)} or the default directory + * {@link #download(DownloadRequest)} was called without ever + * setting the temp file root. If neither method has been called since the last time the app's + * shared preferences were reset, returns {@code null}. + * + * @return A {@link File} pointing to the configured temp file directory, or null if not yet + * configured. + */ + public @Nullable File getTempFileRootDirectory() { + SharedPreferences prefs = mContext.getSharedPreferences( + MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); + String path = prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null); + if (path != null) { + return new File(path); + } + return null; + } + + /** + * Requests the download of a file or set of files that the carrier has indicated to be + * available. + * + * May throw an {@link IllegalArgumentException} + * + * If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed, + * this method will create a directory at the default location defined at + * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp + * file root directory. + * + * Asynchronous errors through the callback may include any error not specific to the + * streaming use-case. + * @param request The request that specifies what should be downloaded. + */ + public void download(@NonNull DownloadRequest request) { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + + // Check to see whether the app's set a temp root dir yet, and set it if not. + SharedPreferences prefs = mContext.getSharedPreferences( + MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); + if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) { + File tempRootDirectory = new File(mContext.getFilesDir(), + DEFAULT_TOP_LEVEL_TEMP_DIRECTORY); + tempRootDirectory.mkdirs(); + setTempFileRootDirectory(tempRootDirectory); + } + + writeDownloadRequestToken(request); + try { + downloadService.download(request); + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + } + } + + /** + * Returns a list of pending {@link DownloadRequest}s that originated from this application. + * A pending request is one that was issued via + * {@link #download(DownloadRequest)} but not cancelled through + * {@link #cancelDownload(DownloadRequest)}. + * @return A list, possibly empty, of {@link DownloadRequest}s + */ + public @NonNull List<DownloadRequest> listPendingDownloads() { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + + try { + return downloadService.listPendingDownloads(mSubscriptionId); + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + return Collections.emptyList(); + } + } + + /** + * Registers a callback for a {@link DownloadRequest} previously requested via + * {@link #download(DownloadRequest)}. This callback will only be called as long as both this + * app and the middleware are both running -- if either one stops, no further calls on the + * provided {@link DownloadStateCallback} will be enqueued. + * + * If the middleware is not aware of the specified download request, + * this method will throw an {@link IllegalArgumentException}. + * + * @param request The {@link DownloadRequest} that you want updates on. + * @param callback The callback that should be called when the middleware has information to + * share on the download. + * @param handler The {@link Handler} on which calls to {@code callback} should be enqueued on. + */ + public void registerStateCallback(@NonNull DownloadRequest request, + @NonNull DownloadStateCallback callback, + @NonNull Handler handler) { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + + InternalDownloadStateCallback internalCallback = + new InternalDownloadStateCallback(callback, handler); + + try { + int result = downloadService.registerStateCallback(request, internalCallback); + if (result != MbmsErrors.SUCCESS) { + if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { + throw new IllegalArgumentException("Unknown download request."); + } + sendErrorToApp(result, null); + return; + } + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + return; + } + mInternalDownloadCallbacks.put(callback, internalCallback); + } + + /** + * Un-register a callback previously registered via + * {@link #registerStateCallback(DownloadRequest, DownloadStateCallback, Handler)}. After + * this method is called, no further callbacks will be enqueued on the {@link Handler} + * provided upon registration, even if this method throws an exception. + * + * If the middleware is not aware of the specified download request, + * this method will throw an {@link IllegalArgumentException}. + * + * @param request The {@link DownloadRequest} provided during registration + * @param callback The callback provided during registration. + */ + public void unregisterStateCallback(@NonNull DownloadRequest request, + @NonNull DownloadStateCallback callback) { + try { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + + InternalDownloadStateCallback internalCallback = + mInternalDownloadCallbacks.get(callback); + + try { + int result = downloadService.unregisterStateCallback(request, internalCallback); + if (result != MbmsErrors.SUCCESS) { + if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { + throw new IllegalArgumentException("Unknown download request."); + } + sendErrorToApp(result, null); + } + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + } + } finally { + InternalDownloadStateCallback internalCallback = + mInternalDownloadCallbacks.remove(callback); + if (internalCallback != null) { + internalCallback.stop(); + } + } + } + + /** + * Attempts to cancel the specified {@link DownloadRequest}. + * + * If the middleware is not aware of the specified download request, + * this method will throw an {@link IllegalArgumentException}. + * + * @param downloadRequest The download request that you wish to cancel. + */ + public void cancelDownload(@NonNull DownloadRequest downloadRequest) { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + + try { + int result = downloadService.cancelDownload(downloadRequest); + if (result != MbmsErrors.SUCCESS) { + if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { + throw new IllegalArgumentException("Unknown download request."); + } + sendErrorToApp(result, null); + return; + } + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + return; + } + deleteDownloadRequestToken(downloadRequest); + } + + /** + * Gets information about the status of a file pending download. + * + * If there was a problem communicating with the middleware or if it has no records of the + * file indicated by {@code fileInfo} being associated with {@code downloadRequest}, + * {@link #STATUS_UNKNOWN} will be returned. + * + * @param downloadRequest The download request to query. + * @param fileInfo The particular file within the request to get information on. + * @return The status of the download. + */ + @DownloadStatus + public int getDownloadStatus(DownloadRequest downloadRequest, FileInfo fileInfo) { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + + try { + return downloadService.getDownloadStatus(downloadRequest, fileInfo); + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + return STATUS_UNKNOWN; + } + } + + /** + * Resets the middleware's knowledge of previously-downloaded files in this download request. + * + * Normally, the middleware keeps track of the hashes of downloaded files and won't re-download + * files whose server-reported hash matches one of the already-downloaded files. This means + * that if the file is accidentally deleted by the user or by the app, the middleware will + * not try to download it again. + * This method will reset the middleware's cache of hashes for the provided + * {@link DownloadRequest}, so that previously downloaded content will be downloaded again + * when available. + * This will not interrupt in-progress downloads. + * + * This is distinct from cancelling and re-issuing the download request -- if you cancel and + * re-issue, the middleware will not clear its cache of download state information. + * + * If the middleware is not aware of the specified download request, an + * {@link IllegalArgumentException} will be thrown. + * + * @param downloadRequest The request to re-download files for. + */ + public void resetDownloadKnowledge(DownloadRequest downloadRequest) { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + throw new IllegalStateException("Middleware not yet bound"); + } + + try { + int result = downloadService.resetDownloadKnowledge(downloadRequest); + if (result != MbmsErrors.SUCCESS) { + if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { + throw new IllegalArgumentException("Unknown download request."); + } + sendErrorToApp(result, null); + } + } catch (RemoteException e) { + mService.set(null); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + } + } + + /** + * Terminates this instance. + * + * After this method returns, + * no further callbacks originating from the middleware will be enqueued on the provided + * instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been + * enqueued will still be delivered. + * + * It is safe to call {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)} to + * obtain another instance of {@link MbmsDownloadSession} immediately after this method + * returns. + * + * May throw an {@link IllegalStateException} + */ + @Override + public void close() { + try { + IMbmsDownloadService downloadService = mService.get(); + if (downloadService == null) { + Log.i(LOG_TAG, "Service already dead"); + return; + } + downloadService.dispose(mSubscriptionId); + } catch (RemoteException e) { + // Ignore + Log.i(LOG_TAG, "Remote exception while disposing of service"); + } finally { + mService.set(null); + sIsInitialized.set(false); + mInternalCallback.stop(); + } + } + + private void writeDownloadRequestToken(DownloadRequest request) { + File token = getDownloadRequestTokenPath(request); + if (!token.getParentFile().exists()) { + token.getParentFile().mkdirs(); + } + if (token.exists()) { + Log.w(LOG_TAG, "Download token " + token.getName() + " already exists"); + return; + } + try { + if (!token.createNewFile()) { + throw new RuntimeException("Failed to create download token for request " + + request); + } + } catch (IOException e) { + throw new RuntimeException("Failed to create download token for request " + request + + " due to IOException " + e); + } + } + + private void deleteDownloadRequestToken(DownloadRequest request) { + File token = getDownloadRequestTokenPath(request); + if (!token.isFile()) { + Log.w(LOG_TAG, "Attempting to delete non-existent download token at " + token); + return; + } + if (!token.delete()) { + Log.w(LOG_TAG, "Couldn't delete download token at " + token); + } + } + + private File getDownloadRequestTokenPath(DownloadRequest request) { + File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext, + request.getFileServiceId()); + String downloadTokenFileName = request.getHash() + + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX; + return new File(tempFileLocation, downloadTokenFileName); + } + + private void sendErrorToApp(int errorCode, String message) { + try { + mInternalCallback.onError(errorCode, message); + } catch (RemoteException e) { + // Ignore, should not happen locally. + } + } +} diff --git a/android/telephony/MbmsStreamingManager.java b/android/telephony/MbmsStreamingSession.java index b6b253ec..a8c46079 100644 --- a/android/telephony/MbmsStreamingManager.java +++ b/android/telephony/MbmsStreamingSession.java @@ -16,6 +16,8 @@ package android.telephony; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.content.ComponentName; @@ -25,18 +27,20 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.telephony.mbms.InternalStreamingManagerCallback; +import android.telephony.mbms.InternalStreamingSessionCallback; import android.telephony.mbms.InternalStreamingServiceCallback; -import android.telephony.mbms.MbmsException; -import android.telephony.mbms.MbmsStreamingManagerCallback; +import android.telephony.mbms.MbmsErrors; +import android.telephony.mbms.MbmsStreamingSessionCallback; import android.telephony.mbms.MbmsUtils; import android.telephony.mbms.StreamingService; import android.telephony.mbms.StreamingServiceCallback; import android.telephony.mbms.StreamingServiceInfo; import android.telephony.mbms.vendor.IMbmsStreamingService; +import android.util.ArraySet; import android.util.Log; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -45,8 +49,8 @@ import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; /** * This class provides functionality for streaming media over MBMS. */ -public class MbmsStreamingManager { - private static final String LOG_TAG = "MbmsStreamingManager"; +public class MbmsStreamingSession implements AutoCloseable { + private static final String LOG_TAG = "MbmsStreamingSession"; /** * Service action which must be handled by the middleware implementing the MBMS streaming @@ -65,98 +69,98 @@ public class MbmsStreamingManager { @Override public void binderDied() { sIsInitialized.set(false); - sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST, "Received death notification"); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification"); } }; - private InternalStreamingManagerCallback mInternalCallback; + private InternalStreamingSessionCallback mInternalCallback; + private Set<StreamingService> mKnownActiveStreamingServices = new ArraySet<>(); private final Context mContext; private int mSubscriptionId = INVALID_SUBSCRIPTION_ID; /** @hide */ - private MbmsStreamingManager(Context context, MbmsStreamingManagerCallback callback, + private MbmsStreamingSession(Context context, MbmsStreamingSessionCallback callback, int subscriptionId, Handler handler) { mContext = context; mSubscriptionId = subscriptionId; if (handler == null) { handler = new Handler(Looper.getMainLooper()); } - mInternalCallback = new InternalStreamingManagerCallback(callback, handler); + mInternalCallback = new InternalStreamingSessionCallback(callback, handler); } /** - * Create a new MbmsStreamingManager using the given subscription ID. + * Create a new {@link MbmsStreamingSession} using the given subscription ID. * * Note that this call will bind a remote service. You may not call this method on your app's - * main thread. This may throw an {@link MbmsException}, indicating errors that may happen - * during the initialization or binding process. + * main thread. * - * - * You may only have one instance of {@link MbmsStreamingManager} per UID. If you call this - * method while there is an active instance of {@link MbmsStreamingManager} in your process - * (in other words, one that has not had {@link #dispose()} called on it), this method will - * throw an {@link MbmsException}. If you call this method in a different process + * You may only have one instance of {@link MbmsStreamingSession} per UID. If you call this + * method while there is an active instance of {@link MbmsStreamingSession} in your process + * (in other words, one that has not had {@link #close()} called on it), this method will + * throw an {@link IllegalStateException}. If you call this method in a different process * running under the same UID, an error will be indicated via - * {@link MbmsStreamingManagerCallback#onError(int, String)}. + * {@link MbmsStreamingSessionCallback#onError(int, String)}. * * Note that initialization may fail asynchronously. If you wish to try again after you - * receive such an asynchronous error, you must call dispose() on the instance of - * {@link MbmsStreamingManager} that you received before calling this method again. + * receive such an asynchronous error, you must call {@link #close()} on the instance of + * {@link MbmsStreamingSession} that you received before calling this method again. * * @param context The {@link Context} to use. * @param callback A callback object on which you wish to receive results of asynchronous * operations. * @param subscriptionId The subscription ID to use. - * @param handler The handler you wish to receive callbacks on. If null, callbacks will be - * processed on the main looper (in other words, the looper returned from - * {@link Looper#getMainLooper()}). + * @param handler The handler you wish to receive callbacks on. + * @return An instance of {@link MbmsStreamingSession}, or null if an error occurred. */ - public static MbmsStreamingManager create(Context context, - MbmsStreamingManagerCallback callback, int subscriptionId, Handler handler) - throws MbmsException { + public static @Nullable MbmsStreamingSession create(@NonNull Context context, + final @NonNull MbmsStreamingSessionCallback callback, int subscriptionId, + @NonNull Handler handler) { if (!sIsInitialized.compareAndSet(false, true)) { - throw new MbmsException(MbmsException.InitializationErrors.ERROR_DUPLICATE_INITIALIZE); + throw new IllegalStateException("Cannot create two instances of MbmsStreamingSession"); } - MbmsStreamingManager manager = new MbmsStreamingManager(context, callback, + MbmsStreamingSession session = new MbmsStreamingSession(context, callback, subscriptionId, handler); - try { - manager.bindAndInitialize(); - } catch (MbmsException e) { + + final int result = session.bindAndInitialize(); + if (result != MbmsErrors.SUCCESS) { sIsInitialized.set(false); - throw e; + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(result, null); + } + }); + return null; } - return manager; + return session; } /** - * Create a new MbmsStreamingManager using the system default data subscription ID. - * See {@link #create(Context, MbmsStreamingManagerCallback, int, Handler)}. + * Create a new {@link MbmsStreamingSession} using the system default data subscription ID. + * See {@link #create(Context, MbmsStreamingSessionCallback, int, Handler)}. */ - public static MbmsStreamingManager create(Context context, - MbmsStreamingManagerCallback callback, Handler handler) - throws MbmsException { + public static MbmsStreamingSession create(@NonNull Context context, + @NonNull MbmsStreamingSessionCallback callback, @NonNull Handler handler) { return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler); } /** - * Create a new MbmsStreamingManager using the system default data subscription ID and - * default {@link Handler}. - * See {@link #create(Context, MbmsStreamingManagerCallback, int, Handler)}. - */ - public static MbmsStreamingManager create(Context context, - MbmsStreamingManagerCallback callback) - throws MbmsException { - return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), null); - } - - /** - * Terminates this instance, ending calls to the registered listener. Also terminates - * any streaming services spawned from this instance. + * Terminates this instance. Also terminates + * any streaming services spawned from this instance as if + * {@link StreamingService#stopStreaming()} had been called on them. After this method returns, + * no further callbacks originating from the middleware will be enqueued on the provided + * instance of {@link MbmsStreamingSessionCallback}, but callbacks that have already been + * enqueued will still be delivered. + * + * It is safe to call {@link #create(Context, MbmsStreamingSessionCallback, int, Handler)} to + * obtain another instance of {@link MbmsStreamingSession} immediately after this method + * returns. * * May throw an {@link IllegalStateException} */ - public void dispose() { + public void close() { try { IMbmsStreamingService streamingService = mService.get(); if (streamingService == null) { @@ -164,47 +168,49 @@ public class MbmsStreamingManager { return; } streamingService.dispose(mSubscriptionId); + for (StreamingService s : mKnownActiveStreamingServices) { + s.getCallback().stop(); + } + mKnownActiveStreamingServices.clear(); } catch (RemoteException e) { // Ignore for now } finally { mService.set(null); sIsInitialized.set(false); + mInternalCallback.stop(); } } /** * An inspection API to retrieve the list of streaming media currently be advertised. - * The results are returned asynchronously through the previously registered callback. - * serviceClasses lets the app filter on types of programming and is opaque data between - * the app and the carrier. + * The results are returned asynchronously via + * {@link MbmsStreamingSessionCallback#onStreamingServicesUpdated(List)} on the callback + * provided upon creation. * - * Multiple calls replace the list of serviceClasses of interest. + * Multiple calls replace the list of service classes of interest. * - * This may throw an {@link MbmsException} containing any error in - * {@link android.telephony.mbms.MbmsException.GeneralErrors}, - * {@link MbmsException#ERROR_MIDDLEWARE_NOT_BOUND}, or - * {@link MbmsException#ERROR_MIDDLEWARE_LOST}. + * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}. * - * May also throw an unchecked {@link IllegalArgumentException} or an - * {@link IllegalStateException} - * - * @param classList A list of streaming service classes that the app would like updates on. + * @param serviceClassList A list of streaming service classes that the app would like updates + * on. The exact names of these classes should be negotiated with the + * wireless carrier separately. */ - public void getStreamingServices(List<String> classList) throws MbmsException { + public void requestUpdateStreamingServices(List<String> serviceClassList) { IMbmsStreamingService streamingService = mService.get(); if (streamingService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); + throw new IllegalStateException("Middleware not yet bound"); } try { - int returnCode = streamingService.getStreamingServices(mSubscriptionId, classList); - if (returnCode != MbmsException.SUCCESS) { - throw new MbmsException(returnCode); + int returnCode = streamingService.requestUpdateStreamingServices( + mSubscriptionId, serviceClassList); + if (returnCode != MbmsErrors.SUCCESS) { + sendErrorToApp(returnCode, null); } } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService.set(null); sIsInitialized.set(false); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } } @@ -215,56 +221,57 @@ public class MbmsStreamingManager { * reported via * {@link android.telephony.mbms.StreamingServiceCallback#onStreamStateUpdated(int, int)} * - * May throw an - * {@link MbmsException} containing any of the error codes in - * {@link android.telephony.mbms.MbmsException.GeneralErrors}, - * {@link MbmsException#ERROR_MIDDLEWARE_NOT_BOUND}, or - * {@link MbmsException#ERROR_MIDDLEWARE_LOST}. - * - * May also throw an {@link IllegalArgumentException} or an {@link IllegalStateException} + * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} * * Asynchronous errors through the callback include any of the errors in - * {@link android.telephony.mbms.MbmsException.GeneralErrors} or - * {@link android.telephony.mbms.MbmsException.StreamingErrors}. + * {@link MbmsErrors.GeneralErrors} or + * {@link MbmsErrors.StreamingErrors}. * * @param serviceInfo The information about the service to stream. * @param callback A callback that'll be called when something about the stream changes. - * @param handler A handler that calls to {@code callback} should be called on. If null, - * defaults to the handler provided via - * {@link #create(Context, MbmsStreamingManagerCallback, int, Handler)}. + * @param handler A handler that calls to {@code callback} should be called on. * @return An instance of {@link StreamingService} through which the stream can be controlled. + * May be {@code null} if an error occurred. */ - public StreamingService startStreaming(StreamingServiceInfo serviceInfo, - StreamingServiceCallback callback, Handler handler) throws MbmsException { + public @Nullable StreamingService startStreaming(StreamingServiceInfo serviceInfo, + StreamingServiceCallback callback, @NonNull Handler handler) { IMbmsStreamingService streamingService = mService.get(); if (streamingService == null) { - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND); + throw new IllegalStateException("Middleware not yet bound"); } InternalStreamingServiceCallback serviceCallback = new InternalStreamingServiceCallback( - callback, handler == null ? mInternalCallback.getHandler() : handler); + callback, handler); StreamingService serviceForApp = new StreamingService( - mSubscriptionId, streamingService, serviceInfo, serviceCallback); + mSubscriptionId, streamingService, this, serviceInfo, serviceCallback); + mKnownActiveStreamingServices.add(serviceForApp); try { int returnCode = streamingService.startStreaming( mSubscriptionId, serviceInfo.getServiceId(), serviceCallback); - if (returnCode != MbmsException.SUCCESS) { - throw new MbmsException(returnCode); + if (returnCode != MbmsErrors.SUCCESS) { + sendErrorToApp(returnCode, null); + return null; } } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService.set(null); sIsInitialized.set(false); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + return null; } return serviceForApp; } - private void bindAndInitialize() throws MbmsException { - MbmsUtils.startBinding(mContext, MBMS_STREAMING_SERVICE_ACTION, + /** @hide */ + public void onStreamingServiceStopped(StreamingService service) { + mKnownActiveStreamingServices.remove(service); + } + + private int bindAndInitialize() { + return MbmsUtils.startBinding(mContext, MBMS_STREAMING_SERVICE_ACTION, new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { @@ -277,19 +284,19 @@ public class MbmsStreamingManager { } catch (RemoteException e) { Log.e(LOG_TAG, "Service died before initialization"); sendErrorToApp( - MbmsException.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, + MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, e.toString()); sIsInitialized.set(false); return; } catch (RuntimeException e) { Log.e(LOG_TAG, "Runtime exception during initialization"); sendErrorToApp( - MbmsException.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, + MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, e.toString()); sIsInitialized.set(false); return; } - if (result != MbmsException.SUCCESS) { + if (result != MbmsErrors.SUCCESS) { sendErrorToApp(result, "Error returned during initialization"); sIsInitialized.set(false); return; @@ -297,7 +304,7 @@ public class MbmsStreamingManager { try { streamingService.asBinder().linkToDeath(mDeathRecipient, 0); } catch (RemoteException e) { - sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST, + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Middleware lost during initialization"); sIsInitialized.set(false); return; @@ -315,7 +322,7 @@ public class MbmsStreamingManager { private void sendErrorToApp(int errorCode, String message) { try { - mInternalCallback.error(errorCode, message); + mInternalCallback.onError(errorCode, message); } catch (RemoteException e) { // Ignore, should not happen locally. } diff --git a/android/telephony/mbms/DownloadRequest.java b/android/telephony/mbms/DownloadRequest.java index eae9011e..5a57f322 100644 --- a/android/telephony/mbms/DownloadRequest.java +++ b/android/telephony/mbms/DownloadRequest.java @@ -16,6 +16,7 @@ package android.telephony.mbms; +import android.annotation.SystemApi; import android.content.Intent; import android.net.Uri; import android.os.Parcel; @@ -37,34 +38,27 @@ import java.security.NoSuchAlgorithmException; import java.util.Objects; /** - * A Parcelable class describing a pending Cell-Broadcast download request - * @hide + * Describes a request to download files over cell-broadcast. Instances of this class should be + * created by the app when requesting a download, and instances of this class will be passed back + * to the app when the middleware updates the status of the download. */ -public class DownloadRequest implements Parcelable { +public final class DownloadRequest implements Parcelable { // Version code used to keep token calculation consistent. private static final int CURRENT_VERSION = 1; private static final String LOG_TAG = "MbmsDownloadRequest"; - /** - * Maximum permissible length for the app's download-completion intent, when serialized via - * {@link Intent#toUri(int)}. - */ + /** @hide */ public static final int MAX_APP_INTENT_SIZE = 50000; - /** - * Maximum permissible length for the app's destination path, when serialized via - * {@link Uri#toString()}. - */ + /** @hide */ public static final int MAX_DESTINATION_URI_SIZE = 50000; /** @hide */ private static class OpaqueDataContainer implements Serializable { - private final String destinationUri; private final String appIntent; private final int version; - public OpaqueDataContainer(String destinationUri, String appIntent, int version) { - this.destinationUri = destinationUri; + public OpaqueDataContainer(String appIntent, int version) { this.appIntent = appIntent; this.version = version; } @@ -73,7 +67,6 @@ public class DownloadRequest implements Parcelable { public static class Builder { private String fileServiceId; private Uri source; - private Uri dest; private int subscriptionId; private String appIntent; private int version = CURRENT_VERSION; @@ -91,8 +84,8 @@ public class DownloadRequest implements Parcelable { /** * Set the service ID for the download request. For use by the middleware only. * @hide - * TODO: systemapi */ + @SystemApi public Builder setServiceId(String serviceId) { fileServiceId = serviceId; return this; @@ -101,7 +94,6 @@ public class DownloadRequest implements Parcelable { /** * Sets the source URI for the download request to be built. * @param source - * @return */ public Builder setSource(Uri source) { this.source = source; @@ -109,25 +101,8 @@ public class DownloadRequest implements Parcelable { } /** - * Sets the destination URI for the download request to be built. The middleware should - * not set this directly. - * @param dest A URI obtained from {@link Uri#fromFile(File)}, denoting the requested - * final destination of the download. - * @return - */ - public Builder setDest(Uri dest) { - if (dest.toString().length() > MAX_DESTINATION_URI_SIZE) { - throw new IllegalArgumentException("Destination uri must not exceed length " + - MAX_DESTINATION_URI_SIZE); - } - this.dest = dest; - return this; - } - - /** * Set the subscription ID on which the file(s) should be downloaded. * @param subscriptionId - * @return */ public Builder setSubscriptionId(int subscriptionId) { this.subscriptionId = subscriptionId; @@ -141,7 +116,6 @@ public class DownloadRequest implements Parcelable { * * The middleware should not use this method. * @param intent - * @return */ public Builder setAppIntent(Intent intent) { this.appIntent = intent.toUri(0); @@ -158,17 +132,15 @@ public class DownloadRequest implements Parcelable { * manager code, but is irrelevant to the middleware. * @param data A byte array, the contents of which should have been originally obtained * from {@link DownloadRequest#getOpaqueData()}. - * @return - * TODO: systemapi * @hide */ + @SystemApi public Builder setOpaqueData(byte[] data) { try { ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data)); OpaqueDataContainer dataContainer = (OpaqueDataContainer) stream.readObject(); version = dataContainer.version; appIntent = dataContainer.appIntent; - dest = Uri.parse(dataContainer.destinationUri); } catch (IOException e) { // Really should never happen Log.e(LOG_TAG, "Got IOException trying to parse opaque data"); @@ -181,24 +153,21 @@ public class DownloadRequest implements Parcelable { } public DownloadRequest build() { - return new DownloadRequest(fileServiceId, source, dest, - subscriptionId, appIntent, version); + return new DownloadRequest(fileServiceId, source, subscriptionId, appIntent, version); } } private final String fileServiceId; private final Uri sourceUri; - private final Uri destinationUri; private final int subscriptionId; private final String serializedResultIntentForApp; private final int version; private DownloadRequest(String fileServiceId, - Uri source, Uri dest, - int sub, String appIntent, int version) { + Uri source, int sub, + String appIntent, int version) { this.fileServiceId = fileServiceId; sourceUri = source; - destinationUri = dest; subscriptionId = sub; serializedResultIntentForApp = appIntent; this.version = version; @@ -211,7 +180,6 @@ public class DownloadRequest implements Parcelable { private DownloadRequest(DownloadRequest dr) { fileServiceId = dr.fileServiceId; sourceUri = dr.sourceUri; - destinationUri = dr.destinationUri; subscriptionId = dr.subscriptionId; serializedResultIntentForApp = dr.serializedResultIntentForApp; version = dr.version; @@ -220,7 +188,6 @@ public class DownloadRequest implements Parcelable { private DownloadRequest(Parcel in) { fileServiceId = in.readString(); sourceUri = in.readParcelable(getClass().getClassLoader()); - destinationUri = in.readParcelable(getClass().getClassLoader()); subscriptionId = in.readInt(); serializedResultIntentForApp = in.readString(); version = in.readInt(); @@ -233,7 +200,6 @@ public class DownloadRequest implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeString(fileServiceId); out.writeParcelable(sourceUri, flags); - out.writeParcelable(destinationUri, flags); out.writeInt(subscriptionId); out.writeString(serializedResultIntentForApp); out.writeInt(version); @@ -254,14 +220,6 @@ public class DownloadRequest implements Parcelable { } /** - * For use by the client app only. - * @return The URI of the final destination of the download. - */ - public Uri getDestinationUri() { - return destinationUri; - } - - /** * @return The subscription ID on which to perform MBMS operations. */ public int getSubscriptionId() { @@ -287,14 +245,14 @@ public class DownloadRequest implements Parcelable { * {@link Builder#setOpaqueData(byte[])}. * @return A byte array of opaque data to persist. * @hide - * TODO: systemapi */ + @SystemApi public byte[] getOpaqueData() { try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream); OpaqueDataContainer container = new OpaqueDataContainer( - destinationUri.toString(), serializedResultIntentForApp, version); + serializedResultIntentForApp, version); stream.writeObject(container); stream.flush(); return byteArrayOutputStream.toByteArray(); @@ -321,6 +279,22 @@ public class DownloadRequest implements Parcelable { }; /** + * Maximum permissible length for the app's destination path, when serialized via + * {@link Uri#toString()}. + */ + public static int getMaxAppIntentSize() { + return MAX_APP_INTENT_SIZE; + } + + /** + * Maximum permissible length for the app's download-completion intent, when serialized via + * {@link Intent#toUri(int)}. + */ + public static int getMaxDestinationUriSize() { + return MAX_DESTINATION_URI_SIZE; + } + + /** * @hide */ public boolean isMultipartDownload() { @@ -344,7 +318,6 @@ public class DownloadRequest implements Parcelable { if (version >= 1) { // Hash the source URI, destination URI, and the app intent digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8)); - digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8)); digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); } // Add updates for future versions here @@ -365,13 +338,12 @@ public class DownloadRequest implements Parcelable { version == request.version && Objects.equals(fileServiceId, request.fileServiceId) && Objects.equals(sourceUri, request.sourceUri) && - Objects.equals(destinationUri, request.destinationUri) && Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp); } @Override public int hashCode() { - return Objects.hash(fileServiceId, sourceUri, destinationUri, + return Objects.hash(fileServiceId, sourceUri, subscriptionId, serializedResultIntentForApp, version); } } diff --git a/android/telephony/mbms/DownloadProgressListener.java b/android/telephony/mbms/DownloadStateCallback.java index d91e9ad2..86920bd3 100644 --- a/android/telephony/mbms/DownloadProgressListener.java +++ b/android/telephony/mbms/DownloadStateCallback.java @@ -16,18 +16,20 @@ package android.telephony.mbms; -import android.os.RemoteException; +import android.telephony.MbmsDownloadSession; /** - * A optional listener class used by download clients to track progress. - * @hide + * A optional listener class used by download clients to track progress. Apps should extend this + * class and pass an instance into + * {@link MbmsDownloadSession#download(DownloadRequest)} + * + * This is optionally specified when requesting a download and will only be called while the app + * is running. */ -public class DownloadProgressListener extends IDownloadProgressListener.Stub { +public class DownloadStateCallback { + /** - * Gives process callbacks for a given DownloadRequest. - * This is optionally specified when requesting a download and - * only lives while the app is running - it's unlikely to be useful for - * downloads far in the future. + * Called when the middleware wants to report progress for a file in a {@link DownloadRequest}. * * @param request a {@link DownloadRequest}, indicating which download is being referenced. * @param fileInfo a {@link FileInfo} specifying the file to report progress on. Note that @@ -40,9 +42,21 @@ public class DownloadProgressListener extends IDownloadProgressListener.Stub { * @param currentDecodedSize is the number of bytes that have been decoded. * @param fullDecodedSize is the total number of bytes that make up the final decoded content. */ - @Override - public void progress(DownloadRequest request, FileInfo fileInfo, + public void onProgressUpdated(DownloadRequest request, FileInfo fileInfo, int currentDownloadSize, int fullDownloadSize, - int currentDecodedSize, int fullDecodedSize) throws RemoteException { + int currentDecodedSize, int fullDecodedSize) { + } + + /** + * Gives download state callbacks for a file in a {@link DownloadRequest}. + * + * @param request a {@link DownloadRequest}, indicating which download is being referenced. + * @param fileInfo a {@link FileInfo} specifying the file to report progress on. Note that + * the request may result in many files being downloaded and the client + * may not have been able to get a list of them in advance. + * @param state The current state of the download. + */ + public void onStateUpdated(DownloadRequest request, FileInfo fileInfo, + @MbmsDownloadSession.DownloadStatus int state) { } } diff --git a/android/telephony/mbms/FileInfo.java b/android/telephony/mbms/FileInfo.java index f97131dd..0d737b58 100644 --- a/android/telephony/mbms/FileInfo.java +++ b/android/telephony/mbms/FileInfo.java @@ -16,26 +16,18 @@ package android.telephony.mbms; +import android.annotation.SystemApi; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; /** - * A Parcelable class Cell-Broadcast downloadable file information. - * @hide + * Describes a single file that is available over MBMS. */ -public class FileInfo implements Parcelable { +public final class FileInfo implements Parcelable { - /** - * The URI into the carriers infrastructure which points to this file. - * This is used internally but is also one of the few pieces of data about the content that is - * exposed and may be needed for disambiguation by the application. - */ private final Uri uri; - /** - * The mime type of the content. - */ private final String mimeType; public static final Parcelable.Creator<FileInfo> CREATOR = @@ -53,8 +45,8 @@ public class FileInfo implements Parcelable { /** * @hide - * TODO: systemapi */ + @SystemApi public FileInfo(Uri uri, String mimeType) { this.uri = uri; this.mimeType = mimeType; @@ -76,10 +68,17 @@ public class FileInfo implements Parcelable { return 0; } + /** + * @return The URI in the carrier's infrastructure which points to this file. Apps should + * negotiate the contents of this URI separately with the carrier. + */ public Uri getUri() { return uri; } + /** + * @return The MIME type of the file. + */ public String getMimeType() { return mimeType; } diff --git a/android/telephony/mbms/FileServiceInfo.java b/android/telephony/mbms/FileServiceInfo.java index 8afe4d3c..d8d7f48a 100644 --- a/android/telephony/mbms/FileServiceInfo.java +++ b/android/telephony/mbms/FileServiceInfo.java @@ -16,6 +16,7 @@ package android.telephony.mbms; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -26,13 +27,14 @@ import java.util.Locale; import java.util.Map; /** - * A Parcelable class Cell-Broadcast downloadable file information. - * @hide + * Describes a file service available from the carrier from which files can be downloaded via + * cell-broadcast. */ -public class FileServiceInfo extends ServiceInfo implements Parcelable { +public final class FileServiceInfo extends ServiceInfo implements Parcelable { private final List<FileInfo> files; - /** @hide TODO: systemapi */ + /** @hide */ + @SystemApi public FileServiceInfo(Map<Locale, String> newNames, String newClassName, List<Locale> newLocales, String newServiceId, Date start, Date end, List<FileInfo> newFiles) { @@ -56,7 +58,7 @@ public class FileServiceInfo extends ServiceInfo implements Parcelable { FileServiceInfo(Parcel in) { super(in); files = new ArrayList<FileInfo>(); - in.readList(files, null); + in.readList(files, FileInfo.class.getClassLoader()); } @Override @@ -70,8 +72,12 @@ public class FileServiceInfo extends ServiceInfo implements Parcelable { return 0; } + /** + * @return A list of files available from this service. Note that this list may not be + * exhaustive -- the middleware may not have information on all files that are available. + * Consult the carrier for an authoritative and exhaustive list. + */ public List<FileInfo> getFiles() { return files; } - } diff --git a/android/telephony/mbms/InternalDownloadSessionCallback.java b/android/telephony/mbms/InternalDownloadSessionCallback.java new file mode 100644 index 00000000..a7a5958f --- /dev/null +++ b/android/telephony/mbms/InternalDownloadSessionCallback.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 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 android.telephony.mbms; + +import android.os.Handler; +import android.os.RemoteException; + +import java.util.List; + +/** @hide */ +public class InternalDownloadSessionCallback extends IMbmsDownloadSessionCallback.Stub { + + private final Handler mHandler; + private final MbmsDownloadSessionCallback mAppCallback; + private volatile boolean mIsStopped = false; + + public InternalDownloadSessionCallback(MbmsDownloadSessionCallback appCallback, + Handler handler) { + mAppCallback = appCallback; + mHandler = handler; + } + + @Override + public void onError(final int errorCode, final String message) throws RemoteException { + if (mIsStopped) { + return; + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mAppCallback.onError(errorCode, message); + } + }); + } + + @Override + public void onFileServicesUpdated(final List<FileServiceInfo> services) throws RemoteException { + if (mIsStopped) { + return; + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mAppCallback.onFileServicesUpdated(services); + } + }); + } + + @Override + public void onMiddlewareReady() throws RemoteException { + if (mIsStopped) { + return; + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mAppCallback.onMiddlewareReady(); + } + }); + } + + public Handler getHandler() { + return mHandler; + } + + public void stop() { + mIsStopped = true; + } +} diff --git a/android/telephony/mbms/InternalDownloadStateCallback.java b/android/telephony/mbms/InternalDownloadStateCallback.java new file mode 100644 index 00000000..8702952c --- /dev/null +++ b/android/telephony/mbms/InternalDownloadStateCallback.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 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 android.telephony.mbms; + +import android.os.Handler; +import android.os.RemoteException; + +/** + * @hide + */ +public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub { + private final Handler mHandler; + private final DownloadStateCallback mAppCallback; + private volatile boolean mIsStopped = false; + + public InternalDownloadStateCallback(DownloadStateCallback appCallback, Handler handler) { + mAppCallback = appCallback; + mHandler = handler; + } + + @Override + public void onProgressUpdated(final DownloadRequest request, final FileInfo fileInfo, + final int currentDownloadSize, final int fullDownloadSize, final int currentDecodedSize, + final int fullDecodedSize) throws RemoteException { + if (mIsStopped) { + return; + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mAppCallback.onProgressUpdated(request, fileInfo, currentDownloadSize, + fullDownloadSize, currentDecodedSize, fullDecodedSize); + } + }); + } + + @Override + public void onStateUpdated(final DownloadRequest request, final FileInfo fileInfo, + final int state) throws RemoteException { + if (mIsStopped) { + return; + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mAppCallback.onStateUpdated(request, fileInfo, state); + } + }); + } + + public void stop() { + mIsStopped = true; + } +} diff --git a/android/telephony/mbms/InternalStreamingServiceCallback.java b/android/telephony/mbms/InternalStreamingServiceCallback.java index bb337b27..eb6579ce 100644 --- a/android/telephony/mbms/InternalStreamingServiceCallback.java +++ b/android/telephony/mbms/InternalStreamingServiceCallback.java @@ -23,6 +23,7 @@ import android.os.RemoteException; public class InternalStreamingServiceCallback extends IStreamingServiceCallback.Stub { private final StreamingServiceCallback mAppCallback; private final Handler mHandler; + private volatile boolean mIsStopped = false; public InternalStreamingServiceCallback(StreamingServiceCallback appCallback, Handler handler) { mAppCallback = appCallback; @@ -30,7 +31,11 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback. } @Override - public void error(int errorCode, String message) throws RemoteException { + public void onError(final int errorCode, final String message) throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -40,7 +45,11 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback. } @Override - public void streamStateUpdated(int state, int reason) throws RemoteException { + public void onStreamStateUpdated(final int state, final int reason) throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -50,7 +59,11 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback. } @Override - public void mediaDescriptionUpdated() throws RemoteException { + public void onMediaDescriptionUpdated() throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -60,7 +73,11 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback. } @Override - public void broadcastSignalStrengthUpdated(int signalStrength) throws RemoteException { + public void onBroadcastSignalStrengthUpdated(final int signalStrength) throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -70,7 +87,11 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback. } @Override - public void streamMethodUpdated(int methodType) throws RemoteException { + public void onStreamMethodUpdated(final int methodType) throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -78,4 +99,8 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback. } }); } + + public void stop() { + mIsStopped = true; + } } diff --git a/android/telephony/mbms/InternalStreamingManagerCallback.java b/android/telephony/mbms/InternalStreamingSessionCallback.java index b52df8c0..d782d12c 100644 --- a/android/telephony/mbms/InternalStreamingManagerCallback.java +++ b/android/telephony/mbms/InternalStreamingSessionCallback.java @@ -18,25 +18,27 @@ package android.telephony.mbms; import android.os.Handler; import android.os.RemoteException; -import android.telephony.mbms.IMbmsStreamingManagerCallback; -import android.telephony.mbms.MbmsStreamingManagerCallback; -import android.telephony.mbms.StreamingServiceInfo; import java.util.List; /** @hide */ -public class InternalStreamingManagerCallback extends IMbmsStreamingManagerCallback.Stub { +public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallback.Stub { private final Handler mHandler; - private final MbmsStreamingManagerCallback mAppCallback; + private final MbmsStreamingSessionCallback mAppCallback; + private volatile boolean mIsStopped = false; - public InternalStreamingManagerCallback(MbmsStreamingManagerCallback appCallback, + public InternalStreamingSessionCallback(MbmsStreamingSessionCallback appCallback, Handler handler) { mAppCallback = appCallback; mHandler = handler; } @Override - public void error(int errorCode, String message) throws RemoteException { + public void onError(final int errorCode, final String message) throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -46,8 +48,12 @@ public class InternalStreamingManagerCallback extends IMbmsStreamingManagerCallb } @Override - public void streamingServicesUpdated(List<StreamingServiceInfo> services) + public void onStreamingServicesUpdated(final List<StreamingServiceInfo> services) throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -57,7 +63,11 @@ public class InternalStreamingManagerCallback extends IMbmsStreamingManagerCallb } @Override - public void middlewareReady() throws RemoteException { + public void onMiddlewareReady() throws RemoteException { + if (mIsStopped) { + return; + } + mHandler.post(new Runnable() { @Override public void run() { @@ -69,4 +79,8 @@ public class InternalStreamingManagerCallback extends IMbmsStreamingManagerCallb public Handler getHandler() { return mHandler; } + + public void stop() { + mIsStopped = true; + } } diff --git a/android/telephony/mbms/MbmsDownloadManagerCallback.java b/android/telephony/mbms/MbmsDownloadManagerCallback.java deleted file mode 100644 index 17291d09..00000000 --- a/android/telephony/mbms/MbmsDownloadManagerCallback.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2016 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 android.telephony.mbms; - -import android.os.RemoteException; -import android.telephony.MbmsDownloadManager; - -import java.util.List; - -/** - * A Parcelable class with Cell-Broadcast service information. - * @hide - */ -public class MbmsDownloadManagerCallback extends IMbmsDownloadManagerCallback.Stub { - - @Override - public void error(int errorCode, String message) throws RemoteException { - // default implementation empty - } - - /** - * Called to indicate published File Services have changed. - * - * This will only be called after the application has requested - * a list of file services and specified a service class list - * of interest AND the results of a subsequent getFileServices - * call with the same service class list would return different - * results. - * - * @param services a List of FileServiceInfos - * - */ - @Override - public void fileServicesUpdated(List<FileServiceInfo> services) throws RemoteException { - // default implementation empty - } - - /** - * Called to indicate that the middleware has been initialized and is ready. - * - * Before this method is called, calling any method on an instance of - * {@link android.telephony.MbmsDownloadManager} will result in an {@link MbmsException} - * being thrown with error code {@link MbmsException#ERROR_MIDDLEWARE_NOT_BOUND} - * or {@link MbmsException.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY} - */ - @Override - public void middlewareReady() throws RemoteException { - // default implementation empty - } -} diff --git a/android/telephony/mbms/MbmsDownloadReceiver.java b/android/telephony/mbms/MbmsDownloadReceiver.java index ba7d120a..61415b50 100644 --- a/android/telephony/mbms/MbmsDownloadReceiver.java +++ b/android/telephony/mbms/MbmsDownloadReceiver.java @@ -16,6 +16,7 @@ package android.telephony.mbms; +import android.annotation.SystemApi; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -24,8 +25,8 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.telephony.MbmsDownloadManager; -import android.telephony.mbms.vendor.VendorIntents; +import android.telephony.MbmsDownloadSession; +import android.telephony.mbms.vendor.VendorUtils; import android.util.Log; import java.io.File; @@ -35,52 +36,74 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.UUID; /** - * @hide + * The {@link BroadcastReceiver} responsible for handling intents sent from the middleware. Apps + * that wish to download using MBMS APIs should declare this class in their AndroidManifest.xml as + * follows: +<pre>{@code +<receiver + android:name="android.telephony.mbms.MbmsDownloadReceiver" + android:permission="android.permission.SEND_EMBMS_INTENTS" + android:enabled="true" + android:exported="true"> +</receiver>}</pre> */ public class MbmsDownloadReceiver extends BroadcastReceiver { + /** @hide */ public static final String DOWNLOAD_TOKEN_SUFFIX = ".download_token"; + /** @hide */ public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority"; /** - * TODO: @SystemApi all these result codes * Indicates that the requested operation completed without error. + * @hide */ + @SystemApi public static final int RESULT_OK = 0; /** * Indicates that the intent sent had an invalid action. This will be the result if * {@link Intent#getAction()} returns anything other than - * {@link VendorIntents#ACTION_DOWNLOAD_RESULT_INTERNAL}, - * {@link VendorIntents#ACTION_FILE_DESCRIPTOR_REQUEST}, or - * {@link VendorIntents#ACTION_CLEANUP}. + * {@link VendorUtils#ACTION_DOWNLOAD_RESULT_INTERNAL}, + * {@link VendorUtils#ACTION_FILE_DESCRIPTOR_REQUEST}, or + * {@link VendorUtils#ACTION_CLEANUP}. * This is a fatal result code and no result extras should be expected. + * @hide */ + @SystemApi public static final int RESULT_INVALID_ACTION = 1; /** * Indicates that the intent was missing some required extras. * This is a fatal result code and no result extras should be expected. + * @hide */ + @SystemApi public static final int RESULT_MALFORMED_INTENT = 2; /** - * Indicates that the supplied value for {@link VendorIntents#EXTRA_TEMP_FILE_ROOT} + * Indicates that the supplied value for {@link VendorUtils#EXTRA_TEMP_FILE_ROOT} * does not match what the app has stored. * This is a fatal result code and no result extras should be expected. + * @hide */ + @SystemApi public static final int RESULT_BAD_TEMP_FILE_ROOT = 3; /** * Indicates that the manager was unable to move the completed download to its final location. * This is a fatal result code and no result extras should be expected. + * @hide */ + @SystemApi public static final int RESULT_DOWNLOAD_FINALIZATION_ERROR = 4; /** @@ -88,35 +111,48 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { * descriptors. * This is a non-fatal result code -- some file descriptors may still be generated, but there * is no guarantee that they will be the same number as requested. + * @hide */ + @SystemApi public static final int RESULT_TEMP_FILE_GENERATION_ERROR = 5; + /** + * Indicates that the manager was unable to notify the app of the completed download. + * This is a fatal result code and no result extras should be expected. + * @hide + */ + @SystemApi + public static final int RESULT_APP_NOTIFICATION_ERROR = 6; + + private static final String LOG_TAG = "MbmsDownloadReceiver"; private static final String TEMP_FILE_SUFFIX = ".embms.temp"; - private static final int MAX_TEMP_FILE_RETRIES = 5; + private static final String TEMP_FILE_STAGING_LOCATION = "staged_completed_files"; + private static final int MAX_TEMP_FILE_RETRIES = 5; private String mFileProviderAuthorityCache = null; private String mMiddlewarePackageNameCache = null; + /** @hide */ @Override public void onReceive(Context context, Intent intent) { if (!verifyIntentContents(context, intent)) { setResultCode(RESULT_MALFORMED_INTENT); return; } - if (!Objects.equals(intent.getStringExtra(VendorIntents.EXTRA_TEMP_FILE_ROOT), + if (!Objects.equals(intent.getStringExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT), MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath())) { setResultCode(RESULT_BAD_TEMP_FILE_ROOT); return; } - if (VendorIntents.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { + if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { moveDownloadedFile(context, intent); cleanupPostMove(context, intent); - } else if (VendorIntents.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { + } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { generateTempFiles(context, intent); - } else if (VendorIntents.ACTION_CLEANUP.equals(intent.getAction())) { + } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) { cleanupTempFiles(context, intent); } else { setResultCode(RESULT_INVALID_ACTION); @@ -124,30 +160,31 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } private boolean verifyIntentContents(Context context, Intent intent) { - if (VendorIntents.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { - if (!intent.hasExtra(MbmsDownloadManager.EXTRA_RESULT)) { + if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { + if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT)) { Log.w(LOG_TAG, "Download result did not include a result code. Ignoring."); return false; } - if (!intent.hasExtra(VendorIntents.EXTRA_REQUEST)) { + if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) { Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring."); return false; } - if (!intent.hasExtra(VendorIntents.EXTRA_TEMP_FILE_ROOT)) { + if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); return false; } - if (!intent.hasExtra(MbmsDownloadManager.EXTRA_FILE_INFO)) { + if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO)) { Log.w(LOG_TAG, "Download result did not include the associated file info. " + "Ignoring."); return false; } - if (!intent.hasExtra(VendorIntents.EXTRA_FINAL_URI)) { + if (!intent.hasExtra(VendorUtils.EXTRA_FINAL_URI)) { Log.w(LOG_TAG, "Download result did not include the path to the final " + "temp file. Ignoring."); return false; } - DownloadRequest request = intent.getParcelableExtra(VendorIntents.EXTRA_REQUEST); + DownloadRequest request = intent.getParcelableExtra( + MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); String expectedTokenFileName = request.getHash() + DOWNLOAD_TOKEN_SUFFIX; File expectedTokenFile = new File( MbmsUtils.getEmbmsTempFileDirForService(context, request.getFileServiceId()), @@ -157,27 +194,27 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { "Expected " + expectedTokenFile); return false; } - } else if (VendorIntents.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { - if (!intent.hasExtra(VendorIntents.EXTRA_SERVICE_INFO)) { - Log.w(LOG_TAG, "Temp file request did not include the associated service info." + + } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { + if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) { + Log.w(LOG_TAG, "Temp file request did not include the associated service id." + " Ignoring."); return false; } - if (!intent.hasExtra(VendorIntents.EXTRA_TEMP_FILE_ROOT)) { + if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); return false; } - } else if (VendorIntents.ACTION_CLEANUP.equals(intent.getAction())) { - if (!intent.hasExtra(VendorIntents.EXTRA_SERVICE_INFO)) { - Log.w(LOG_TAG, "Cleanup request did not include the associated service info." + + } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) { + if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) { + Log.w(LOG_TAG, "Cleanup request did not include the associated service id." + " Ignoring."); return false; } - if (!intent.hasExtra(VendorIntents.EXTRA_TEMP_FILE_ROOT)) { + if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Cleanup request did not include the temp file root. Ignoring."); return false; } - if (!intent.hasExtra(VendorIntents.EXTRA_TEMP_FILES_IN_USE)) { + if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE)) { Log.w(LOG_TAG, "Cleanup request did not include the list of temp files in use. " + "Ignoring."); return false; @@ -187,21 +224,26 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } private void moveDownloadedFile(Context context, Intent intent) { - DownloadRequest request = intent.getParcelableExtra(VendorIntents.EXTRA_REQUEST); + DownloadRequest request = intent.getParcelableExtra( + MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); Intent intentForApp = request.getIntentForApp(); + if (intentForApp == null) { + Log.i(LOG_TAG, "Malformed app notification intent"); + setResultCode(RESULT_APP_NOTIFICATION_ERROR); + return; + } - int result = intent.getIntExtra(MbmsDownloadManager.EXTRA_RESULT, - MbmsDownloadManager.RESULT_CANCELLED); - intentForApp.putExtra(MbmsDownloadManager.EXTRA_RESULT, result); + int result = intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, + MbmsDownloadSession.RESULT_CANCELLED); + intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result); - if (result != MbmsDownloadManager.RESULT_SUCCESSFUL) { + if (result != MbmsDownloadSession.RESULT_SUCCESSFUL) { Log.i(LOG_TAG, "Download request indicated a failed download. Aborting."); context.sendBroadcast(intentForApp); return; } - Uri destinationUri = request.getDestinationUri(); - Uri finalTempFile = intent.getParcelableExtra(VendorIntents.EXTRA_FINAL_URI); + Uri finalTempFile = intent.getParcelableExtra(VendorUtils.EXTRA_FINAL_URI); if (!verifyTempFilePath(context, request.getFileServiceId(), finalTempFile)) { Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile); setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); @@ -209,30 +251,37 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } FileInfo completedFileInfo = - (FileInfo) intent.getParcelableExtra(MbmsDownloadManager.EXTRA_FILE_INFO); - String relativePath = calculateDestinationFileRelativePath(request, completedFileInfo); + (FileInfo) intent.getParcelableExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO); + Path stagingDirectory = FileSystems.getDefault().getPath( + MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath(), + TEMP_FILE_STAGING_LOCATION); - Uri finalFileLocation = moveTempFile(finalTempFile, destinationUri, relativePath); - if (finalFileLocation == null) { + Uri stagedFileLocation; + try { + stagedFileLocation = stageTempFile(finalTempFile, stagingDirectory); + } catch (IOException e) { Log.w(LOG_TAG, "Failed to move temp file to final destination"); setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); return; } - intentForApp.putExtra(MbmsDownloadManager.EXTRA_COMPLETED_FILE_URI, finalFileLocation); - intentForApp.putExtra(MbmsDownloadManager.EXTRA_FILE_INFO, completedFileInfo); + intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, + stagedFileLocation); + intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo); + intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request); context.sendBroadcast(intentForApp); setResultCode(RESULT_OK); } private void cleanupPostMove(Context context, Intent intent) { - DownloadRequest request = intent.getParcelableExtra(VendorIntents.EXTRA_REQUEST); + DownloadRequest request = intent.getParcelableExtra( + MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); if (request == null) { Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring."); return; } - List<Uri> tempFiles = intent.getParcelableExtra(VendorIntents.EXTRA_TEMP_LIST); + List<Uri> tempFiles = intent.getParcelableExtra(VendorUtils.EXTRA_TEMP_LIST); if (tempFiles == null) { return; } @@ -246,16 +295,15 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } private void generateTempFiles(Context context, Intent intent) { - FileServiceInfo serviceInfo = - intent.getParcelableExtra(VendorIntents.EXTRA_SERVICE_INFO); - if (serviceInfo == null) { - Log.w(LOG_TAG, "Temp file request did not include the associated service info. " + + String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID); + if (serviceId == null) { + Log.w(LOG_TAG, "Temp file request did not include the associated service id. " + "Ignoring."); setResultCode(RESULT_MALFORMED_INTENT); return; } - int fdCount = intent.getIntExtra(VendorIntents.EXTRA_FD_COUNT, 0); - List<Uri> pausedList = intent.getParcelableExtra(VendorIntents.EXTRA_PAUSED_LIST); + int fdCount = intent.getIntExtra(VendorUtils.EXTRA_FD_COUNT, 0); + List<Uri> pausedList = intent.getParcelableExtra(VendorUtils.EXTRA_PAUSED_LIST); if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) { Log.i(LOG_TAG, "No temp files actually requested. Ending."); @@ -265,22 +313,20 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } ArrayList<UriPathPair> freshTempFiles = - generateFreshTempFiles(context, serviceInfo, fdCount); + generateFreshTempFiles(context, serviceId, fdCount); ArrayList<UriPathPair> pausedFiles = - generateUrisForPausedFiles(context, serviceInfo, pausedList); + generateUrisForPausedFiles(context, serviceId, pausedList); Bundle result = new Bundle(); - result.putParcelableArrayList(VendorIntents.EXTRA_FREE_URI_LIST, freshTempFiles); - result.putParcelableArrayList(VendorIntents.EXTRA_PAUSED_URI_LIST, pausedFiles); + result.putParcelableArrayList(VendorUtils.EXTRA_FREE_URI_LIST, freshTempFiles); + result.putParcelableArrayList(VendorUtils.EXTRA_PAUSED_URI_LIST, pausedFiles); setResultCode(RESULT_OK); setResultExtras(result); } - private ArrayList<UriPathPair> generateFreshTempFiles(Context context, - FileServiceInfo serviceInfo, + private ArrayList<UriPathPair> generateFreshTempFiles(Context context, String serviceId, int freshFdCount) { - File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, - serviceInfo.getServiceId()); + File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId); if (!tempFileDir.exists()) { tempFileDir.mkdirs(); } @@ -324,14 +370,14 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context, - FileServiceInfo serviceInfo, List<Uri> pausedFiles) { + String serviceId, List<Uri> pausedFiles) { if (pausedFiles == null) { return new ArrayList<>(0); } ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size()); for (Uri fileUri : pausedFiles) { - if (!verifyTempFilePath(context, serviceInfo.getServiceId(), fileUri)) { + if (!verifyTempFilePath(context, serviceId, fileUri)) { Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume"); setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); continue; @@ -353,12 +399,10 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } private void cleanupTempFiles(Context context, Intent intent) { - FileServiceInfo serviceInfo = - intent.getParcelableExtra(VendorIntents.EXTRA_SERVICE_INFO); - File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, - serviceInfo.getServiceId()); + String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID); + File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId); final List<Uri> filesInUse = - intent.getParcelableArrayListExtra(VendorIntents.EXTRA_TEMP_FILES_IN_USE); + intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE); File[] filesToDelete = tempFileDir.listFiles(new FileFilter() { @Override public boolean accept(File file) { @@ -385,63 +429,22 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } } - private static String calculateDestinationFileRelativePath(DownloadRequest request, - FileInfo info) { - List<String> filePathComponents = info.getUri().getPathSegments(); - List<String> requestPathComponents = request.getSourceUri().getPathSegments(); - Iterator<String> filePathIter = filePathComponents.iterator(); - Iterator<String> requestPathIter = requestPathComponents.iterator(); - - StringBuilder pathBuilder = new StringBuilder(); - // Iterate through the segments of the carrier's URI to the file, along with the segments - // of the source URI specified in the download request. The relative path is calculated - // as the tail of the file's URI that does not match the path segments in the source URI. - while (filePathIter.hasNext()) { - String currFilePathComponent = filePathIter.next(); - if (requestPathIter.hasNext()) { - String requestFilePathComponent = requestPathIter.next(); - if (requestFilePathComponent.equals(currFilePathComponent)) { - continue; - } - } - pathBuilder.append(currFilePathComponent); - pathBuilder.append('/'); - } - // remove the trailing slash - if (pathBuilder.length() > 0) { - pathBuilder.deleteCharAt(pathBuilder.length() - 1); - } - return pathBuilder.toString(); - } - /* - * Moves a tempfile located at fromPath to a new location at toPath. If - * toPath is a directory, the destination file will be located at relativePath - * underneath toPath. + * Moves a tempfile located at fromPath to a new location in the staging directory. */ - private static Uri moveTempFile(Uri fromPath, Uri toPath, String relativePath) { + private static Uri stageTempFile(Uri fromPath, Path stagingDirectory) throws IOException { if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) { Log.w(LOG_TAG, "Moving source uri " + fromPath+ " does not have a file scheme"); return null; } - if (!ContentResolver.SCHEME_FILE.equals(toPath.getScheme())) { - Log.w(LOG_TAG, "Moving destination uri " + toPath + " does not have a file scheme"); - return null; - } - File fromFile = new File(fromPath.getSchemeSpecificPart()); - File toFile = new File(toPath.getSchemeSpecificPart()); - if (toFile.isDirectory()) { - toFile = new File(toFile, relativePath); + Path fromFile = FileSystems.getDefault().getPath(fromPath.getPath()); + if (!Files.isDirectory(stagingDirectory)) { + Files.createDirectory(stagingDirectory); } - toFile.getParentFile().mkdirs(); + Path result = Files.move(fromFile, stagingDirectory.resolve(fromFile.getFileName())); - if (fromFile.renameTo(toFile)) { - return Uri.fromFile(toFile); - } else if (manualMove(fromFile, toFile)) { - return Uri.fromFile(toFile); - } - return null; + return Uri.fromFile(result.toFile()); } private static boolean verifyTempFilePath(Context context, String serviceId, @@ -493,7 +496,7 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { private String getMiddlewarePackageCached(Context context) { if (mMiddlewarePackageNameCache == null) { mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context, - MbmsDownloadManager.MBMS_DOWNLOAD_SERVICE_ACTION).packageName; + MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION).packageName; } return mMiddlewarePackageNameCache; } diff --git a/android/telephony/mbms/MbmsDownloadSessionCallback.java b/android/telephony/mbms/MbmsDownloadSessionCallback.java new file mode 100644 index 00000000..77dea6f3 --- /dev/null +++ b/android/telephony/mbms/MbmsDownloadSessionCallback.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 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 android.telephony.mbms; + +import android.telephony.MbmsDownloadSession; + +import java.util.List; + +/** + * A callback class that apps should use to receive information on file downloads over + * cell-broadcast. + */ +public class MbmsDownloadSessionCallback { + + /** + * Indicates that the middleware has encountered an asynchronous error. + * @param errorCode Any error code listed in {@link MbmsErrors} + * @param message A message, intended for debugging purposes, describing the error in further + * detail. + */ + public void onError(int errorCode, String message) { + // default implementation empty + } + + /** + * Called to indicate published File Services have changed. + * + * This will only be called after the application has requested a list of file services and + * specified a service class list of interest via + * {@link MbmsDownloadSession#requestUpdateFileServices(List)}. If there are subsequent calls to + * {@link MbmsDownloadSession#requestUpdateFileServices(List)}, + * this method may not be called again if + * the list of service classes would remain the same. + * + * @param services The most recently updated list of available file services. + */ + public void onFileServicesUpdated(List<FileServiceInfo> services) { + // default implementation empty + } + + /** + * Called to indicate that the middleware has been initialized and is ready. + * + * Before this method is called, calling any method on an instance of + * {@link MbmsDownloadSession} will result in an {@link IllegalStateException} + * being thrown or {@link #onError(int, String)} being called with error code + * {@link MbmsErrors.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY} + */ + public void onMiddlewareReady() { + // default implementation empty + } +} diff --git a/android/telephony/mbms/MbmsException.java b/android/telephony/mbms/MbmsErrors.java index 6de5a18e..af0af24c 100644 --- a/android/telephony/mbms/MbmsException.java +++ b/android/telephony/mbms/MbmsErrors.java @@ -16,7 +16,9 @@ package android.telephony.mbms; -public class MbmsException extends Exception { +import android.telephony.MbmsStreamingSession; + +public class MbmsErrors { /** Indicates that the operation was successful. */ public static final int SUCCESS = 0; @@ -30,8 +32,8 @@ public class MbmsException extends Exception { /** * Indicates that the app attempted to perform an operation on an instance of - * TODO: link android.telephony.MbmsDownloadManager or - * {@link android.telephony.MbmsStreamingManager} without being bound to the middleware. + * {@link android.telephony.MbmsDownloadSession} or + * {@link MbmsStreamingSession} without being bound to the middleware. */ public static final int ERROR_MIDDLEWARE_NOT_BOUND = 2; @@ -46,8 +48,7 @@ public class MbmsException extends Exception { private InitializationErrors() {} /** * Indicates that the app tried to create more than one instance each of - * {@link android.telephony.MbmsStreamingManager} or - * TODO: link android.telephony.MbmsDownloadManager + * {@link MbmsStreamingSession} or {@link android.telephony.MbmsDownloadSession}. */ public static final int ERROR_DUPLICATE_INITIALIZE = 101; /** Indicates that the app is not authorized to access media via MBMS.*/ @@ -64,8 +65,8 @@ public class MbmsException extends Exception { private GeneralErrors() {} /** * Indicates that the app attempted to perform an operation before receiving notification - * that the middleware is ready via {@link MbmsStreamingManagerCallback#onMiddlewareReady()} - * or TODO: link MbmsDownloadManagerCallback#middlewareReady + * that the middleware is ready via {@link MbmsStreamingSessionCallback#onMiddlewareReady()} + * or {@link MbmsDownloadSessionCallback#onMiddlewareReady()}. */ public static final int ERROR_MIDDLEWARE_NOT_YET_READY = 201; /** @@ -107,7 +108,7 @@ public class MbmsException extends Exception { /** * Indicates that the app called - * {@link android.telephony.MbmsStreamingManager#startStreaming( + * {@link MbmsStreamingSession#startStreaming( * StreamingServiceInfo, StreamingServiceCallback, android.os.Handler)} * more than once for the same {@link StreamingServiceInfo}. */ @@ -116,10 +117,9 @@ public class MbmsException extends Exception { /** * Indicates the errors that are applicable only to the file-download use-case - * TODO: unhide - * @hide */ public static class DownloadErrors { + private DownloadErrors() { } /** * Indicates that the app is not allowed to change the temp file root at this time due to * outstanding download requests. @@ -130,15 +130,5 @@ public class MbmsException extends Exception { public static final int ERROR_UNKNOWN_DOWNLOAD_REQUEST = 402; } - private final int mErrorCode; - - /** @hide */ - public MbmsException(int errorCode) { - super(); - mErrorCode = errorCode; - } - - public int getErrorCode() { - return mErrorCode; - } + private MbmsErrors() {} } diff --git a/android/telephony/mbms/MbmsStreamingManagerCallback.java b/android/telephony/mbms/MbmsStreamingSessionCallback.java index b31ffa72..5c130a09 100644 --- a/android/telephony/mbms/MbmsStreamingManagerCallback.java +++ b/android/telephony/mbms/MbmsStreamingSessionCallback.java @@ -16,25 +16,26 @@ package android.telephony.mbms; +import android.annotation.Nullable; import android.content.Context; -import android.os.RemoteException; -import android.telephony.MbmsStreamingManager; +import android.os.Handler; +import android.telephony.MbmsStreamingSession; import java.util.List; /** * A callback class that is used to receive information from the middleware on MBMS streaming * services. An instance of this object should be passed into - * {@link android.telephony.MbmsStreamingManager#create(Context, MbmsStreamingManagerCallback)}. + * {@link MbmsStreamingSession#create(Context, MbmsStreamingSessionCallback, int, Handler)}. */ -public class MbmsStreamingManagerCallback { +public class MbmsStreamingSessionCallback { /** * Called by the middleware when it has detected an error condition. The possible error codes - * are listed in {@link MbmsException}. + * are listed in {@link MbmsErrors}. * @param errorCode The error code. * @param message A human-readable message generated by the middleware for debugging purposes. */ - public void onError(int errorCode, String message) { + public void onError(int errorCode, @Nullable String message) { // default implementation empty } @@ -47,8 +48,7 @@ public class MbmsStreamingManagerCallback { * call with the same service class list would return different * results. * - * @param services a List of StreamingServiceInfos - * + * @param services The list of available services. */ public void onStreamingServicesUpdated(List<StreamingServiceInfo> services) { // default implementation empty @@ -58,9 +58,9 @@ public class MbmsStreamingManagerCallback { * Called to indicate that the middleware has been initialized and is ready. * * Before this method is called, calling any method on an instance of - * {@link android.telephony.MbmsStreamingManager} will result in an {@link MbmsException} - * being thrown with error code {@link MbmsException#ERROR_MIDDLEWARE_NOT_BOUND} - * or {@link MbmsException.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY} + * {@link MbmsStreamingSession} will result in an {@link IllegalStateException} or an error + * delivered via {@link #onError(int, String)} with error code + * {@link MbmsErrors.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY}. */ public void onMiddlewareReady() { // default implementation empty diff --git a/android/telephony/mbms/MbmsTempFileProvider.java b/android/telephony/mbms/MbmsTempFileProvider.java index c4d033bf..689becd7 100644 --- a/android/telephony/mbms/MbmsTempFileProvider.java +++ b/android/telephony/mbms/MbmsTempFileProvider.java @@ -23,12 +23,11 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.net.Uri; -import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.telephony.MbmsDownloadSession; import java.io.File; import java.io.FileNotFoundException; @@ -39,7 +38,6 @@ import java.util.Objects; * @hide */ public class MbmsTempFileProvider extends ContentProvider { - public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot"; public static final String TEMP_FILE_ROOT_PREF_FILE_NAME = "MbmsTempFileRootPrefs"; public static final String TEMP_FILE_ROOT_PREF_NAME = "mbms_temp_file_root"; @@ -182,8 +180,8 @@ public class MbmsTempFileProvider extends ContentProvider { if (storedTempFileRoot != null) { return new File(storedTempFileRoot).getCanonicalFile(); } else { - return new File(context.getFilesDir(), DEFAULT_TOP_LEVEL_TEMP_DIRECTORY) - .getCanonicalFile(); + return new File(context.getFilesDir(), + MbmsDownloadSession.DEFAULT_TOP_LEVEL_TEMP_DIRECTORY).getCanonicalFile(); } } catch (IOException e) { throw new RuntimeException("Unable to canonicalize temp file root path " + e); diff --git a/android/telephony/mbms/MbmsUtils.java b/android/telephony/mbms/MbmsUtils.java index 4b913f82..d38d8a71 100644 --- a/android/telephony/mbms/MbmsUtils.java +++ b/android/telephony/mbms/MbmsUtils.java @@ -68,19 +68,20 @@ public class MbmsUtils { return downloadServices.get(0).serviceInfo; } - public static void startBinding(Context context, String serviceAction, - ServiceConnection serviceConnection) throws MbmsException { + public static int startBinding(Context context, String serviceAction, + ServiceConnection serviceConnection) { Intent bindIntent = new Intent(); ServiceInfo mbmsServiceInfo = MbmsUtils.getMiddlewareServiceInfo(context, serviceAction); if (mbmsServiceInfo == null) { - throw new MbmsException(MbmsException.ERROR_NO_UNIQUE_MIDDLEWARE); + return MbmsErrors.ERROR_NO_UNIQUE_MIDDLEWARE; } bindIntent.setComponent(MbmsUtils.toComponentName(mbmsServiceInfo)); context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE); + return MbmsErrors.SUCCESS; } /** diff --git a/android/telephony/mbms/ServiceInfo.java b/android/telephony/mbms/ServiceInfo.java index c01604b0..9a01ed00 100644 --- a/android/telephony/mbms/ServiceInfo.java +++ b/android/telephony/mbms/ServiceInfo.java @@ -16,6 +16,8 @@ package android.telephony.mbms; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -26,12 +28,13 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; /** * Describes a cell-broadcast service. This class should not be instantiated directly -- use - * {@link StreamingServiceInfo} or FileServiceInfo TODO: add link once that's unhidden + * {@link StreamingServiceInfo} or {@link FileServiceInfo} */ public class ServiceInfo { // arbitrary limit on the number of locale -> name pairs we support @@ -58,6 +61,13 @@ public class ServiceInfo { if (newLocales.size() > MAP_LIMIT) { throw new RuntimeException("bad locales length " + newLocales.size()); } + + for (Locale l : newLocales) { + if (!newNames.containsKey(l)) { + throw new IllegalArgumentException("A name must be provided for each locale"); + } + } + names = new HashMap(newNames.size()); names.putAll(newNames); className = newClassName; @@ -114,16 +124,25 @@ public class ServiceInfo { } /** - * User displayable names listed by language. Do not modify the map returned from this method. + * Get the user-displayable name for this cell-broadcast service corresponding to the + * provided {@link Locale}. + * @param locale The {@link Locale} in which you want the name of the service. This must be a + * value from the list returned by {@link #getLocales()} -- an + * {@link java.util.NoSuchElementException} may be thrown otherwise. + * @return The {@link CharSequence} providing the name of the service in the given + * {@link Locale} */ - public Map<Locale, String> getNames() { - return names; + public @NonNull CharSequence getNameForLocale(@NonNull Locale locale) { + if (!names.containsKey(locale)) { + throw new NoSuchElementException("Locale not supported"); + } + return names.get(locale); } /** * The class name for this service - used to categorize and filter */ - public String getClassName() { + public String getServiceClassName() { return className; } diff --git a/android/telephony/mbms/StreamingService.java b/android/telephony/mbms/StreamingService.java index 1d66bac0..ec9134a4 100644 --- a/android/telephony/mbms/StreamingService.java +++ b/android/telephony/mbms/StreamingService.java @@ -17,8 +17,10 @@ package android.telephony.mbms; import android.annotation.IntDef; +import android.annotation.Nullable; import android.net.Uri; import android.os.RemoteException; +import android.telephony.MbmsStreamingSession; import android.telephony.mbms.vendor.IMbmsStreamingService; import android.util.Log; @@ -27,7 +29,7 @@ import java.lang.annotation.RetentionPolicy; /** * Class used to represent a single MBMS stream. After a stream has been started with - * {@link android.telephony.MbmsStreamingManager#startStreaming(StreamingServiceInfo, + * {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo, * StreamingServiceCallback, android.os.Handler)}, * this class is used to hold information about the stream and control it. */ @@ -63,7 +65,7 @@ public class StreamingService { /** * State changed due to a call to {@link #stopStreaming()} or - * {@link android.telephony.MbmsStreamingManager#startStreaming(StreamingServiceInfo, + * {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo, * StreamingServiceCallback, android.os.Handler)} */ public static final int REASON_BY_USER_REQUEST = 1; @@ -101,6 +103,7 @@ public class StreamingService { public final static int UNICAST_METHOD = 2; private final int mSubscriptionId; + private final MbmsStreamingSession mParentSession; private final StreamingServiceInfo mServiceInfo; private final InternalStreamingServiceCallback mCallback; @@ -111,25 +114,25 @@ public class StreamingService { */ public StreamingService(int subscriptionId, IMbmsStreamingService service, + MbmsStreamingSession session, StreamingServiceInfo streamingServiceInfo, InternalStreamingServiceCallback callback) { mSubscriptionId = subscriptionId; + mParentSession = session; mService = service; mServiceInfo = streamingServiceInfo; mCallback = callback; } /** - * Retreive the Uri used to play this stream. + * Retrieve the Uri used to play this stream. * - * This may throw a {@link MbmsException} with the error code - * {@link MbmsException#ERROR_MIDDLEWARE_LOST} + * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}. * - * May also throw an {@link IllegalArgumentException} or an {@link IllegalStateException} - * - * @return The {@link Uri} to pass to the streaming client. + * @return The {@link Uri} to pass to the streaming client, or {@code null} if an error + * occurred. */ - public Uri getPlaybackUri() throws MbmsException { + public @Nullable Uri getPlaybackUri() { if (mService == null) { throw new IllegalStateException("No streaming service attached"); } @@ -139,25 +142,26 @@ public class StreamingService { } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService = null; - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); + mParentSession.onStreamingServiceStopped(this); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + return null; } } /** - * Retreive the info for this StreamingService. + * Retrieve the {@link StreamingServiceInfo} corresponding to this stream. */ public StreamingServiceInfo getInfo() { return mServiceInfo; } /** - * Stop streaming this service. - * This may throw a {@link MbmsException} with the error code - * {@link MbmsException#ERROR_MIDDLEWARE_LOST} + * Stop streaming this service. Further operations on this object will fail with an + * {@link IllegalStateException}. * - * May also throw an {@link IllegalArgumentException} or an {@link IllegalStateException} + * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} */ - public void stopStreaming() throws MbmsException { + public void stopStreaming() { if (mService == null) { throw new IllegalStateException("No streaming service attached"); } @@ -167,32 +171,22 @@ public class StreamingService { } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService = null; - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); + sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + } finally { + mParentSession.onStreamingServiceStopped(this); } } - /** - * Disposes of this stream. Further operations on this object will fail with an - * {@link IllegalStateException}. - * - * This may throw a {@link MbmsException} with the error code - * {@link MbmsException#ERROR_MIDDLEWARE_LOST} - * May also throw an {@link IllegalStateException} - */ - public void dispose() throws MbmsException { - if (mService == null) { - throw new IllegalStateException("No streaming service attached"); - } + /** @hide */ + public InternalStreamingServiceCallback getCallback() { + return mCallback; + } + private void sendErrorToApp(int errorCode, String message) { try { - mService.disposeStream(mSubscriptionId, mServiceInfo.getServiceId()); + mCallback.onError(errorCode, message); } catch (RemoteException e) { - Log.w(LOG_TAG, "Remote process died"); - throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST); - } catch (IllegalArgumentException e) { - throw new IllegalStateException("StreamingService state inconsistent with middleware"); - } finally { - mService = null; + // Ignore, should not happen locally. } } } diff --git a/android/telephony/mbms/StreamingServiceCallback.java b/android/telephony/mbms/StreamingServiceCallback.java index b72c7157..09038244 100644 --- a/android/telephony/mbms/StreamingServiceCallback.java +++ b/android/telephony/mbms/StreamingServiceCallback.java @@ -16,6 +16,8 @@ package android.telephony.mbms; +import android.annotation.Nullable; + /** * A callback class for use when the application is actively streaming content. The middleware * will provide updates on the status of the stream via this callback. @@ -33,11 +35,11 @@ public class StreamingServiceCallback { /** * Called by the middleware when it has detected an error condition in this stream. The - * possible error codes are listed in {@link MbmsException}. + * possible error codes are listed in {@link MbmsErrors}. * @param errorCode The error code. * @param message A human-readable message generated by the middleware for debugging purposes. */ - public void onError(int errorCode, String message) { + public void onError(int errorCode, @Nullable String message) { // default implementation empty } diff --git a/android/telephony/mbms/UriPathPair.java b/android/telephony/mbms/UriPathPair.java index 7acc270e..187e9eed 100644 --- a/android/telephony/mbms/UriPathPair.java +++ b/android/telephony/mbms/UriPathPair.java @@ -16,13 +16,20 @@ package android.telephony.mbms; +import android.annotation.SystemApi; import android.content.ContentResolver; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.telephony.mbms.vendor.VendorUtils; -/** @hide */ -public class UriPathPair implements Parcelable { +/** + * Wrapper for a pair of {@link Uri}s that describe a temp file used by the middleware to + * download files via cell-broadcast. + * @hide + */ +@SystemApi +public final class UriPathPair implements Parcelable { private final Uri mFilePathUri; private final Uri mContentUri; @@ -40,7 +47,7 @@ public class UriPathPair implements Parcelable { } /** @hide */ - protected UriPathPair(Parcel in) { + private UriPathPair(Parcel in) { mFilePathUri = in.readParcelable(Uri.class.getClassLoader()); mContentUri = in.readParcelable(Uri.class.getClassLoader()); } @@ -57,12 +64,23 @@ public class UriPathPair implements Parcelable { } }; - /** future systemapi */ + /** + * Returns the file-path {@link Uri}. This has scheme {@code file} and points to the actual + * location on disk where the temp file resides. Use this when sending {@link Uri}s back to the + * app in the intents in {@link VendorUtils}. + * @return A {@code file} {@link Uri}. + */ public Uri getFilePathUri() { return mFilePathUri; } - /** future systemapi */ + /** + * Returns the content {@link Uri} that may be used with + * {@link ContentResolver#openFileDescriptor(Uri, String)} to obtain a + * {@link android.os.ParcelFileDescriptor} to a temp file to write to. This {@link Uri} will + * expire if the middleware process dies. + * @return A {@code content} {@link Uri} + */ public Uri getContentUri() { return mContentUri; } diff --git a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java index 71713d01..d845a57b 100644 --- a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java +++ b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java @@ -17,40 +17,50 @@ package android.telephony.mbms.vendor; import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; import android.os.RemoteException; -import android.telephony.mbms.DownloadProgressListener; +import android.telephony.MbmsDownloadSession; import android.telephony.mbms.DownloadRequest; +import android.telephony.mbms.DownloadStateCallback; import android.telephony.mbms.FileInfo; import android.telephony.mbms.FileServiceInfo; -import android.telephony.mbms.IDownloadProgressListener; -import android.telephony.mbms.IMbmsDownloadManagerCallback; -import android.telephony.mbms.MbmsDownloadManagerCallback; -import android.telephony.mbms.MbmsException; +import android.telephony.mbms.IDownloadStateCallback; +import android.telephony.mbms.IMbmsDownloadSessionCallback; +import android.telephony.mbms.MbmsDownloadSessionCallback; +import android.telephony.mbms.MbmsErrors; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** - * Base class for MbmsDownloadService. The middleware should extend this base class rather than - * the aidl stub for compatibility + * Base class for MbmsDownloadService. The middleware should return an instance of this object from + * its {@link android.app.Service#onBind(Intent)} method. * @hide - * TODO: future systemapi */ +@SystemApi public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { + private final Map<IBinder, DownloadStateCallback> mDownloadCallbackBinderMap = new HashMap<>(); + private final Map<IBinder, DeathRecipient> mDownloadCallbackDeathRecipients = new HashMap<>(); + /** * Initialize the download service for this app and subId, registering the listener. * * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}, which * will be intercepted and passed to the app as - * {@link android.telephony.mbms.MbmsException.InitializationErrors#ERROR_UNABLE_TO_INITIALIZE} + * {@link MbmsErrors.InitializationErrors#ERROR_UNABLE_TO_INITIALIZE} * - * May return any value from {@link android.telephony.mbms.MbmsException.InitializationErrors} - * or {@link MbmsException#SUCCESS}. Non-successful error codes will be passed to the app via - * {@link IMbmsDownloadManagerCallback#error(int, String)}. + * May return any value from {@link MbmsErrors.InitializationErrors} + * or {@link MbmsErrors#SUCCESS}. Non-successful error codes will be passed to the app via + * {@link IMbmsDownloadSessionCallback#onError(int, String)}. * * @param callback The callback to use to communicate with the app. * @param subscriptionId The subscription ID to use. */ - public int initialize(int subscriptionId, MbmsDownloadManagerCallback callback) + public int initialize(int subscriptionId, MbmsDownloadSessionCallback callback) throws RemoteException { return 0; } @@ -60,22 +70,42 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * @hide */ @Override - public final int initialize(int subscriptionId, - final IMbmsDownloadManagerCallback callback) throws RemoteException { - return initialize(subscriptionId, new MbmsDownloadManagerCallback() { + public final int initialize(final int subscriptionId, + final IMbmsDownloadSessionCallback callback) throws RemoteException { + final int uid = Binder.getCallingUid(); + callback.asBinder().linkToDeath(new DeathRecipient() { + @Override + public void binderDied() { + onAppCallbackDied(uid, subscriptionId); + } + }, 0); + + return initialize(subscriptionId, new MbmsDownloadSessionCallback() { @Override - public void error(int errorCode, String message) throws RemoteException { - callback.error(errorCode, message); + public void onError(int errorCode, String message) { + try { + callback.onError(errorCode, message); + } catch (RemoteException e) { + onAppCallbackDied(uid, subscriptionId); + } } @Override - public void fileServicesUpdated(List<FileServiceInfo> services) throws RemoteException { - callback.fileServicesUpdated(services); + public void onFileServicesUpdated(List<FileServiceInfo> services) { + try { + callback.onFileServicesUpdated(services); + } catch (RemoteException e) { + onAppCallbackDied(uid, subscriptionId); + } } @Override - public void middlewareReady() throws RemoteException { - callback.middlewareReady(); + public void onMiddlewareReady() { + try { + callback.onMiddlewareReady(); + } catch (RemoteException e) { + onAppCallbackDied(uid, subscriptionId); + } } }); } @@ -83,7 +113,7 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { /** * Registers serviceClasses of interest with the appName/subId key. * Starts async fetching data on streaming services of matching classes to be reported - * later via {@link IMbmsDownloadManagerCallback#fileServicesUpdated(List)} + * later via {@link IMbmsDownloadSessionCallback#onFileServicesUpdated(List)} * * Note that subsequent calls with the same uid and subId will replace * the service class list. @@ -94,11 +124,11 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * @param serviceClasses The service classes that the app wishes to get info on. The strings * may contain arbitrary data as negotiated between the app and the * carrier. - * @return One of {@link MbmsException#SUCCESS} or - * {@link MbmsException.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY}, + * @return One of {@link MbmsErrors#SUCCESS} or + * {@link MbmsErrors.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY}, */ @Override - public int getFileServices(int subscriptionId, List<String> serviceClasses) + public int requestUpdateFileServices(int subscriptionId, List<String> serviceClasses) throws RemoteException { return 0; } @@ -110,13 +140,13 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * * If the calling app (as identified by the calling UID) currently has any pending download * requests that have not been canceled, the middleware must return - * {@link MbmsException.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} here. + * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} here. * * @param subscriptionId The subscription id the download is operating under. * @param rootDirectoryPath The path to the app's temp file root directory. - * @return {@link MbmsException#SUCCESS}, - * {@link MbmsException.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY} or - * {@link MbmsException.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} + * @return {@link MbmsErrors#SUCCESS}, + * {@link MbmsErrors.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY} or + * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} */ @Override public int setTempFileRootDirectory(int subscriptionId, @@ -132,12 +162,32 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * this is not the case, an {@link IllegalStateException} may be thrown. * * @param downloadRequest An object describing the set of files to be downloaded. - * @param listener A listener through which the middleware can provide progress updates to - * the app while both are still running. - * @return Any error from {@link android.telephony.mbms.MbmsException.GeneralErrors} - * or {@link MbmsException#SUCCESS} + * @return Any error from {@link MbmsErrors.GeneralErrors} + * or {@link MbmsErrors#SUCCESS} */ - public int download(DownloadRequest downloadRequest, DownloadProgressListener listener) { + @Override + public int download(DownloadRequest downloadRequest) throws RemoteException { + return 0; + } + + /** + * Registers a download state callbacks for the provided {@link DownloadRequest}. + * + * This method is called by the app when it wants to request updates on the progress or + * status of the download. + * + * If the middleware is not aware of a download having been requested with the provided + * + * {@link DownloadRequest} in the past, + * {@link MbmsErrors.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST} + * must be returned. + * + * @param downloadRequest The {@link DownloadRequest} that was used to initiate the download + * for which progress updates are being requested. + * @param callback The callback object to use. + */ + public int registerStateCallback(DownloadRequest downloadRequest, + DownloadStateCallback callback) throws RemoteException { return 0; } @@ -146,24 +196,101 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * @hide */ @Override - public final int download(DownloadRequest downloadRequest, IDownloadProgressListener listener) + public final int registerStateCallback( + final DownloadRequest downloadRequest, final IDownloadStateCallback callback) throws RemoteException { - return download(downloadRequest, new DownloadProgressListener() { + final int uid = Binder.getCallingUid(); + DeathRecipient deathRecipient = new DeathRecipient() { + @Override + public void binderDied() { + onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); + mDownloadCallbackBinderMap.remove(callback.asBinder()); + mDownloadCallbackDeathRecipients.remove(callback.asBinder()); + } + }; + mDownloadCallbackDeathRecipients.put(callback.asBinder(), deathRecipient); + callback.asBinder().linkToDeath(deathRecipient, 0); + + DownloadStateCallback exposedCallback = new DownloadStateCallback() { @Override - public void progress(DownloadRequest request, FileInfo fileInfo, int + public void onProgressUpdated(DownloadRequest request, FileInfo fileInfo, int currentDownloadSize, int fullDownloadSize, int currentDecodedSize, int - fullDecodedSize) throws RemoteException { - listener.progress(request, fileInfo, currentDownloadSize, fullDownloadSize, - currentDecodedSize, fullDecodedSize); + fullDecodedSize) { + try { + callback.onProgressUpdated(request, fileInfo, currentDownloadSize, + fullDownloadSize, + currentDecodedSize, fullDecodedSize); + } catch (RemoteException e) { + onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); + } } - }); + + @Override + public void onStateUpdated(DownloadRequest request, FileInfo fileInfo, + @MbmsDownloadSession.DownloadStatus int state) { + try { + callback.onStateUpdated(request, fileInfo, state); + } catch (RemoteException e) { + onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); + } + } + }; + + mDownloadCallbackBinderMap.put(callback.asBinder(), exposedCallback); + + return registerStateCallback(downloadRequest, exposedCallback); } + /** + * Un-registers a download state callbacks for the provided {@link DownloadRequest}. + * + * This method is called by the app when it no longer wants to request updates on the + * download. + * + * If the middleware is not aware of a download having been requested with the provided + * {@link DownloadRequest} in the past, + * {@link MbmsErrors.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST} + * must be returned. + * + * @param downloadRequest The {@link DownloadRequest} that was used to register the callback + * @param callback The callback object that + * {@link #registerStateCallback(DownloadRequest, DownloadStateCallback)} + * was called with. + */ + public int unregisterStateCallback(DownloadRequest downloadRequest, + DownloadStateCallback callback) throws RemoteException { + return 0; + } + + /** + * Actual AIDL implementation -- hides the callback AIDL from the API. + * @hide + */ + @Override + public final int unregisterStateCallback( + final DownloadRequest downloadRequest, final IDownloadStateCallback callback) + throws RemoteException { + DeathRecipient deathRecipient = + mDownloadCallbackDeathRecipients.remove(callback.asBinder()); + if (deathRecipient == null) { + throw new IllegalArgumentException("Unknown callback"); + } + + callback.asBinder().unlinkToDeath(deathRecipient, 0); + + DownloadStateCallback exposedCallback = + mDownloadCallbackBinderMap.remove(callback.asBinder()); + if (exposedCallback == null) { + throw new IllegalArgumentException("Unknown callback"); + } + + return unregisterStateCallback(downloadRequest, exposedCallback); + } /** * Returns a list of pending {@link DownloadRequest}s that originated from the calling * application, identified by its uid. A pending request is one that was issued via - * {@link #download(DownloadRequest, IDownloadProgressListener)} but not cancelled through + * {@link #download(DownloadRequest)} but not cancelled through * {@link #cancelDownload(DownloadRequest)}. * The middleware must return a non-null result synchronously or throw an exception * inheriting from {@link RuntimeException}. @@ -179,13 +306,13 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * Issues a request to cancel the specified download request. * * If the middleware is unable to cancel the request for whatever reason, it should return - * synchronously with an error. If this method returns {@link MbmsException#SUCCESS}, the app + * synchronously with an error. If this method returns {@link MbmsErrors#SUCCESS}, the app * will no longer be expecting any more file-completed intents from the middleware for this * {@link DownloadRequest}. * @param downloadRequest The request to cancel - * @return {@link MbmsException#SUCCESS}, - * {@link MbmsException.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST}, - * {@link MbmsException.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY} + * @return {@link MbmsErrors#SUCCESS}, + * {@link MbmsErrors.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST}, + * {@link MbmsErrors.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY} */ @Override public int cancelDownload(DownloadRequest downloadRequest) throws RemoteException { @@ -197,7 +324,7 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * * If the middleware has not yet been properly initialized or if it has no records of the * file indicated by {@code fileInfo} being associated with {@code downloadRequest}, - * {@link android.telephony.MbmsDownloadManager#STATUS_UNKNOWN} must be returned. + * {@link MbmsDownloadSession#STATUS_UNKNOWN} must be returned. * * @param downloadRequest The download request to query. * @param fileInfo The particular file within the request to get information on. @@ -217,7 +344,7 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * In addition, current in-progress downloads must not be interrupted. * * If the middleware is not aware of the specified download request, return - * {@link MbmsException.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST}. + * {@link MbmsErrors.DownloadErrors#ERROR_UNKNOWN_DOWNLOAD_REQUEST}. * * @param downloadRequest The request to re-download files for. */ @@ -231,7 +358,7 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * Signals that the app wishes to dispose of the session identified by the * {@code subscriptionId} argument and the caller's uid. No notification back to the * app is required for this operation, and the corresponding callback provided via - * {@link #initialize(int, IMbmsDownloadManagerCallback)} should no longer be used + * {@link #initialize(int, IMbmsDownloadSessionCallback)} should no longer be used * after this method has been called by the app. * * Any download requests issued by the app should remain in effect until the app calls @@ -244,4 +371,12 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { @Override public void dispose(int subscriptionId) throws RemoteException { } + + /** + * Indicates that the app identified by the given UID and subscription ID has died. + * @param uid the UID of the app, as returned by {@link Binder#getCallingUid()}. + * @param subscriptionId The subscription ID the app is using. + */ + public void onAppCallbackDied(int uid, int subscriptionId) { + } } diff --git a/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java b/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java index 843e0482..f8f370a5 100644 --- a/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java +++ b/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java @@ -18,13 +18,14 @@ package android.telephony.mbms.vendor; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.content.Intent; import android.net.Uri; import android.os.Binder; import android.os.RemoteException; -import android.telephony.mbms.IMbmsStreamingManagerCallback; +import android.telephony.mbms.IMbmsStreamingSessionCallback; import android.telephony.mbms.IStreamingServiceCallback; -import android.telephony.mbms.MbmsException; -import android.telephony.mbms.MbmsStreamingManagerCallback; +import android.telephony.mbms.MbmsErrors; +import android.telephony.mbms.MbmsStreamingSessionCallback; import android.telephony.mbms.StreamingService; import android.telephony.mbms.StreamingServiceCallback; import android.telephony.mbms.StreamingServiceInfo; @@ -32,6 +33,8 @@ import android.telephony.mbms.StreamingServiceInfo; import java.util.List; /** + * Base class for MBMS streaming services. The middleware should return an instance of this + * object from its {@link android.app.Service#onBind(Intent)} method. * @hide */ @SystemApi @@ -41,16 +44,16 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { * * May throw an {@link IllegalArgumentException} or a {@link SecurityException}, which * will be intercepted and passed to the app as - * {@link android.telephony.mbms.MbmsException.InitializationErrors#ERROR_UNABLE_TO_INITIALIZE} + * {@link MbmsErrors.InitializationErrors#ERROR_UNABLE_TO_INITIALIZE} * - * May return any value from {@link android.telephony.mbms.MbmsException.InitializationErrors} - * or {@link MbmsException#SUCCESS}. Non-successful error codes will be passed to the app via - * {@link IMbmsStreamingManagerCallback#error(int, String)}. + * May return any value from {@link MbmsErrors.InitializationErrors} + * or {@link MbmsErrors#SUCCESS}. Non-successful error codes will be passed to the app via + * {@link IMbmsStreamingSessionCallback#onError(int, String)}. * * @param callback The callback to use to communicate with the app. * @param subscriptionId The subscription ID to use. */ - public int initialize(MbmsStreamingManagerCallback callback, int subscriptionId) + public int initialize(MbmsStreamingSessionCallback callback, int subscriptionId) throws RemoteException { return 0; } @@ -60,7 +63,7 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { * @hide */ @Override - public final int initialize(final IMbmsStreamingManagerCallback callback, + public final int initialize(final IMbmsStreamingSessionCallback callback, final int subscriptionId) throws RemoteException { final int uid = Binder.getCallingUid(); callback.asBinder().linkToDeath(new DeathRecipient() { @@ -70,20 +73,20 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { } }, 0); - return initialize(new MbmsStreamingManagerCallback() { + return initialize(new MbmsStreamingSessionCallback() { @Override - public void onError(int errorCode, String message) { + public void onError(final int errorCode, final String message) { try { - callback.error(errorCode, message); + callback.onError(errorCode, message); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } } @Override - public void onStreamingServicesUpdated(List<StreamingServiceInfo> services) { + public void onStreamingServicesUpdated(final List<StreamingServiceInfo> services) { try { - callback.streamingServicesUpdated(services); + callback.onStreamingServicesUpdated(services); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } @@ -92,7 +95,7 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { @Override public void onMiddlewareReady() { try { - callback.middlewareReady(); + callback.onMiddlewareReady(); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } @@ -104,7 +107,7 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { /** * Registers serviceClasses of interest with the appName/subId key. * Starts async fetching data on streaming services of matching classes to be reported - * later via {@link IMbmsStreamingManagerCallback#streamingServicesUpdated(List)} + * later via {@link IMbmsStreamingSessionCallback#onStreamingServicesUpdated(List)} * * Note that subsequent calls with the same uid and subId will replace * the service class list. @@ -115,11 +118,11 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { * @param serviceClasses The service classes that the app wishes to get info on. The strings * may contain arbitrary data as negotiated between the app and the * carrier. - * @return {@link MbmsException#SUCCESS} or any of the errors in - * {@link android.telephony.mbms.MbmsException.GeneralErrors} + * @return {@link MbmsErrors#SUCCESS} or any of the errors in + * {@link MbmsErrors.GeneralErrors} */ @Override - public int getStreamingServices(int subscriptionId, + public int requestUpdateStreamingServices(int subscriptionId, List<String> serviceClasses) throws RemoteException { return 0; } @@ -127,14 +130,14 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { /** * Starts streaming on a particular service. This method may perform asynchronous work. When * the middleware is ready to send bits to the frontend, it should inform the app via - * {@link IStreamingServiceCallback#streamStateUpdated(int, int)}. + * {@link IStreamingServiceCallback#onStreamStateUpdated(int, int)}. * * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} * * @param subscriptionId The subscription id to use. * @param serviceId The ID of the streaming service that the app has requested. * @param callback The callback object on which the app wishes to receive updates. - * @return Any error in {@link android.telephony.mbms.MbmsException.GeneralErrors} + * @return Any error in {@link MbmsErrors.GeneralErrors} */ public int startStreaming(int subscriptionId, String serviceId, StreamingServiceCallback callback) throws RemoteException { @@ -147,8 +150,8 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { * @hide */ @Override - public int startStreaming(int subscriptionId, String serviceId, - IStreamingServiceCallback callback) throws RemoteException { + public int startStreaming(final int subscriptionId, String serviceId, + final IStreamingServiceCallback callback) throws RemoteException { final int uid = Binder.getCallingUid(); callback.asBinder().linkToDeath(new DeathRecipient() { @Override @@ -159,19 +162,19 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { return startStreaming(subscriptionId, serviceId, new StreamingServiceCallback() { @Override - public void onError(int errorCode, String message) { + public void onError(final int errorCode, final String message) { try { - callback.error(errorCode, message); + callback.onError(errorCode, message); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } } @Override - public void onStreamStateUpdated(@StreamingService.StreamingState int state, - @StreamingService.StreamingStateChangeReason int reason) { + public void onStreamStateUpdated(@StreamingService.StreamingState final int state, + @StreamingService.StreamingStateChangeReason final int reason) { try { - callback.streamStateUpdated(state, reason); + callback.onStreamStateUpdated(state, reason); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } @@ -180,25 +183,25 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { @Override public void onMediaDescriptionUpdated() { try { - callback.mediaDescriptionUpdated(); + callback.onMediaDescriptionUpdated(); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } } @Override - public void onBroadcastSignalStrengthUpdated(int signalStrength) { + public void onBroadcastSignalStrengthUpdated(final int signalStrength) { try { - callback.broadcastSignalStrengthUpdated(signalStrength); + callback.onBroadcastSignalStrengthUpdated(signalStrength); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } } @Override - public void onStreamMethodUpdated(int methodType) { + public void onStreamMethodUpdated(final int methodType) { try { - callback.streamMethodUpdated(methodType); + callback.onStreamMethodUpdated(methodType); } catch (RemoteException e) { onAppCallbackDied(uid, subscriptionId); } @@ -225,32 +228,19 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { /** * Stop streaming the stream identified by {@code serviceId}. Notification of the resulting * stream state change should be reported to the app via - * {@link IStreamingServiceCallback#streamStateUpdated(int, int)}. - * - * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} + * {@link IStreamingServiceCallback#onStreamStateUpdated(int, int)}. * - * @param subscriptionId The subscription id to use. - * @param serviceId The ID of the streaming service that the app wishes to stop. - */ - @Override - public void stopStreaming(int subscriptionId, String serviceId) - throws RemoteException { - } - - /** - * Dispose of the stream identified by {@code serviceId} for the app identified by the - * {@code appName} and {@code subscriptionId} arguments along with the caller's uid. - * No notification back to the app is required for this operation, and the callback provided via + * In addition, the callback provided via * {@link #startStreaming(int, String, IStreamingServiceCallback)} should no longer be * used after this method has called by the app. * * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} * * @param subscriptionId The subscription id to use. - * @param serviceId The ID of the streaming service that the app wishes to dispose of. + * @param serviceId The ID of the streaming service that the app wishes to stop. */ @Override - public void disposeStream(int subscriptionId, String serviceId) + public void stopStreaming(int subscriptionId, String serviceId) throws RemoteException { } @@ -258,7 +248,7 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { * Signals that the app wishes to dispose of the session identified by the * {@code subscriptionId} argument and the caller's uid. No notification back to the * app is required for this operation, and the corresponding callback provided via - * {@link #initialize(IMbmsStreamingManagerCallback, int)} should no longer be used + * {@link #initialize(IMbmsStreamingSessionCallback, int)} should no longer be used * after this method has been called by the app. * * May throw an {@link IllegalStateException} diff --git a/android/telephony/mbms/vendor/VendorIntents.java b/android/telephony/mbms/vendor/VendorUtils.java index 367c995c..8fb27b29 100644 --- a/android/telephony/mbms/vendor/VendorIntents.java +++ b/android/telephony/mbms/vendor/VendorUtils.java @@ -16,29 +16,32 @@ package android.telephony.mbms.vendor; +import android.annotation.SystemApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; -import android.telephony.mbms.DownloadRequest; +import android.telephony.MbmsDownloadSession; import android.telephony.mbms.MbmsDownloadReceiver; import java.io.File; import java.util.List; /** + * Contains constants and utility methods for MBMS Download middleware apps to communicate with + * frontend apps. * @hide - * TODO: future systemapi */ -public class VendorIntents { +@SystemApi +public class VendorUtils { /** * The MBMS middleware should send this when a download of single file has completed or * failed. Mandatory extras are - * {@link android.telephony.MbmsDownloadManager#EXTRA_RESULT} - * {@link android.telephony.MbmsDownloadManager#EXTRA_FILE_INFO} - * {@link #EXTRA_REQUEST} + * {@link MbmsDownloadSession#EXTRA_MBMS_DOWNLOAD_RESULT} + * {@link MbmsDownloadSession#EXTRA_MBMS_FILE_INFO} + * {@link MbmsDownloadSession#EXTRA_MBMS_DOWNLOAD_REQUEST} * {@link #EXTRA_TEMP_LIST} * {@link #EXTRA_FINAL_URI} */ @@ -49,7 +52,7 @@ public class VendorIntents { * The MBMS middleware should send this when it wishes to request {@code content://} URIs to * serve as temp files for downloads or when it wishes to resume paused downloads. Mandatory * extras are - * {@link #EXTRA_REQUEST} + * {@link #EXTRA_SERVICE_ID} * * Optional extras are * {@link #EXTRA_FD_COUNT} (0 if not present) @@ -118,48 +121,35 @@ public class VendorIntents { "android.telephony.mbms.extra.TEMP_FILES_IN_USE"; /** - * Extra containing the {@link DownloadRequest} for which the download result or file - * descriptor request is for. Must not be null. - */ - public static final String EXTRA_REQUEST = "android.telephony.mbms.extra.REQUEST"; - - /** * Extra containing a single {@link Uri} indicating the path to the temp file in which the * decoded downloaded file resides. Must not be null. */ public static final String EXTRA_FINAL_URI = "android.telephony.mbms.extra.FINAL_URI"; /** - * Extra containing an instance of {@link android.telephony.mbms.ServiceInfo}, used by + * Extra containing a String representing a service ID, used by * file-descriptor requests and cleanup requests to specify which service they want to * request temp files or clean up temp files for, respectively. */ - public static final String EXTRA_SERVICE_INFO = - "android.telephony.mbms.extra.SERVICE_INFO"; + public static final String EXTRA_SERVICE_ID = + "android.telephony.mbms.extra.SERVICE_ID"; /** * Retrieves the {@link ComponentName} for the {@link android.content.BroadcastReceiver} that * the various intents from the middleware should be targeted towards. - * @param uid The uid of the frontend app. + * @param packageName The package name of the app. * @return The component name of the receiver that the middleware should send its intents to, * or null if the app didn't declare it in the manifest. */ - public static ComponentName getAppReceiverFromUid(Context context, int uid) { - String[] packageNames = context.getPackageManager().getPackagesForUid(uid); - if (packageNames == null) { - return null; - } - - for (String packageName : packageNames) { - ComponentName candidate = new ComponentName(packageName, - MbmsDownloadReceiver.class.getCanonicalName()); - Intent queryIntent = new Intent(); - queryIntent.setComponent(candidate); - List<ResolveInfo> receivers = - context.getPackageManager().queryBroadcastReceivers(queryIntent, 0); - if (receivers != null && receivers.size() > 0) { - return candidate; - } + public static ComponentName getAppReceiverFromPackageName(Context context, String packageName) { + ComponentName candidate = new ComponentName(packageName, + MbmsDownloadReceiver.class.getCanonicalName()); + Intent queryIntent = new Intent(); + queryIntent.setComponent(candidate); + List<ResolveInfo> receivers = + context.getPackageManager().queryBroadcastReceivers(queryIntent, 0); + if (receivers != null && receivers.size() > 0) { + return candidate; } return null; } diff --git a/android/text/InputType.java b/android/text/InputType.java index 8967b709..f38482e6 100644 --- a/android/text/InputType.java +++ b/android/text/InputType.java @@ -182,9 +182,9 @@ public interface InputType { * want the IME to correct typos. * Note the contrast with {@link #TYPE_TEXT_FLAG_AUTO_CORRECT} and * {@link #TYPE_TEXT_FLAG_AUTO_COMPLETE}: - * {@code TYPE_TEXT_FLAG_NO_SUGGESTIONS} means the IME should never + * {@code TYPE_TEXT_FLAG_NO_SUGGESTIONS} means the IME does not need to * show an interface to display suggestions. Most IMEs will also take this to - * mean they should not try to auto-correct what the user is typing. + * mean they do not need to try to auto-correct what the user is typing. */ public static final int TYPE_TEXT_FLAG_NO_SUGGESTIONS = 0x00080000; diff --git a/android/util/LruCache.java b/android/util/LruCache.java index 40154880..52086065 100644 --- a/android/util/LruCache.java +++ b/android/util/LruCache.java @@ -20,6 +20,10 @@ import java.util.LinkedHashMap; import java.util.Map; /** + * BEGIN LAYOUTLIB CHANGE + * This is a custom version that doesn't use the non standard LinkedHashMap#eldest. + * END LAYOUTLIB CHANGE + * * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may @@ -87,8 +91,9 @@ public class LruCache<K, V> { /** * Sets the size of the cache. - * * @param maxSize The new maximum size. + * + * @hide */ public void resize(int maxSize) { if (maxSize <= 0) { @@ -185,13 +190,10 @@ public class LruCache<K, V> { } /** - * Remove the eldest entries until the total of remaining entries is at or - * below the requested size. - * * @param maxSize the maximum size of the cache before returning. May be -1 - * to evict even 0-sized elements. + * to evict even 0-sized elements. */ - public void trimToSize(int maxSize) { + private void trimToSize(int maxSize) { while (true) { K key; V value; @@ -205,7 +207,16 @@ public class LruCache<K, V> { break; } - Map.Entry<K, V> toEvict = map.eldest(); + // BEGIN LAYOUTLIB CHANGE + // get the last item in the linked list. + // This is not efficient, the goal here is to minimize the changes + // compared to the platform version. + Map.Entry<K, V> toEvict = null; + for (Map.Entry<K, V> entry : map.entrySet()) { + toEvict = entry; + } + // END LAYOUTLIB CHANGE + if (toEvict == null) { break; } diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java index cac27afa..ebb2af45 100644 --- a/android/view/SurfaceView.java +++ b/android/view/SurfaceView.java @@ -16,1209 +16,115 @@ package android.view; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; -import static android.view.WindowManagerPolicy.APPLICATION_MEDIA_OVERLAY_SUBLAYER; -import static android.view.WindowManagerPolicy.APPLICATION_MEDIA_SUBLAYER; -import static android.view.WindowManagerPolicy.APPLICATION_PANEL_SUBLAYER; +import com.android.layoutlib.bridge.MockView; import android.content.Context; -import android.content.res.CompatibilityInfo.Translator; -import android.content.res.Configuration; import android.graphics.Canvas; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; -import android.os.Build; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.SystemClock; import android.util.AttributeSet; -import android.util.Log; - -import com.android.internal.view.SurfaceCallbackHelper; - -import java.util.ArrayList; -import java.util.concurrent.locks.ReentrantLock; /** - * Provides a dedicated drawing surface embedded inside of a view hierarchy. - * You can control the format of this surface and, if you like, its size; the - * SurfaceView takes care of placing the surface at the correct location on the - * screen - * - * <p>The surface is Z ordered so that it is behind the window holding its - * SurfaceView; the SurfaceView punches a hole in its window to allow its - * surface to be displayed. The view hierarchy will take care of correctly - * compositing with the Surface any siblings of the SurfaceView that would - * normally appear on top of it. This can be used to place overlays such as - * buttons on top of the Surface, though note however that it can have an - * impact on performance since a full alpha-blended composite will be performed - * each time the Surface changes. - * - * <p> The transparent region that makes the surface visible is based on the - * layout positions in the view hierarchy. If the post-layout transform - * properties are used to draw a sibling view on top of the SurfaceView, the - * view may not be properly composited with the surface. + * Mock version of the SurfaceView. + * Only non override public methods from the real SurfaceView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. * - * <p>Access to the underlying surface is provided via the SurfaceHolder interface, - * which can be retrieved by calling {@link #getHolder}. + * TODO: generate automatically. * - * <p>The Surface will be created for you while the SurfaceView's window is - * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated} - * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the - * Surface is created and destroyed as the window is shown and hidden. - * - * <p>One of the purposes of this class is to provide a surface in which a - * secondary thread can render into the screen. If you are going to use it - * this way, you need to be aware of some threading semantics: - * - * <ul> - * <li> All SurfaceView and - * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called - * from the thread running the SurfaceView's window (typically the main thread - * of the application). They thus need to correctly synchronize with any - * state that is also touched by the drawing thread. - * <li> You must ensure that the drawing thread only touches the underlying - * Surface while it is valid -- between - * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()} - * and - * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}. - * </ul> - * - * <p class="note"><strong>Note:</strong> Starting in platform version - * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is - * updated synchronously with other View rendering. This means that translating - * and scaling a SurfaceView on screen will not cause rendering artifacts. Such - * artifacts may occur on previous versions of the platform when its window is - * positioned asynchronously.</p> */ -public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback { - private static final String TAG = "SurfaceView"; - private static final boolean DEBUG = false; - - final ArrayList<SurfaceHolder.Callback> mCallbacks - = new ArrayList<SurfaceHolder.Callback>(); - - final int[] mLocation = new int[2]; - - final ReentrantLock mSurfaceLock = new ReentrantLock(); - final Surface mSurface = new Surface(); // Current surface in use - boolean mDrawingStopped = true; - // We use this to track if the application has produced a frame - // in to the Surface. Up until that point, we should be careful not to punch - // holes. - boolean mDrawFinished = false; - - final Rect mScreenRect = new Rect(); - SurfaceSession mSurfaceSession; - - SurfaceControl mSurfaceControl; - // In the case of format changes we switch out the surface in-place - // we need to preserve the old one until the new one has drawn. - SurfaceControl mDeferredDestroySurfaceControl; - final Rect mTmpRect = new Rect(); - final Configuration mConfiguration = new Configuration(); - - int mSubLayer = APPLICATION_MEDIA_SUBLAYER; - - boolean mIsCreating = false; - private volatile boolean mRtHandlingPositionUpdates = false; - - private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener - = new ViewTreeObserver.OnScrollChangedListener() { - @Override - public void onScrollChanged() { - updateSurface(); - } - }; - - private final ViewTreeObserver.OnPreDrawListener mDrawListener = - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // reposition ourselves where the surface is - mHaveFrame = getWidth() > 0 && getHeight() > 0; - updateSurface(); - return true; - } - }; - - boolean mRequestedVisible = false; - boolean mWindowVisibility = false; - boolean mLastWindowVisibility = false; - boolean mViewVisibility = false; - boolean mWindowStopped = false; - - int mRequestedWidth = -1; - int mRequestedHeight = -1; - /* Set SurfaceView's format to 565 by default to maintain backward - * compatibility with applications assuming this format. - */ - int mRequestedFormat = PixelFormat.RGB_565; - - boolean mHaveFrame = false; - boolean mSurfaceCreated = false; - long mLastLockTime = 0; - - boolean mVisible = false; - int mWindowSpaceLeft = -1; - int mWindowSpaceTop = -1; - int mSurfaceWidth = -1; - int mSurfaceHeight = -1; - int mFormat = -1; - final Rect mSurfaceFrame = new Rect(); - int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1; - private Translator mTranslator; - - private boolean mGlobalListenersAdded; - private boolean mAttachedToWindow; - - private int mSurfaceFlags = SurfaceControl.HIDDEN; - - private int mPendingReportDraws; +public class SurfaceView extends MockView { public SurfaceView(Context context) { this(context, null); } public SurfaceView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs , 0); } - public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); + public SurfaceView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); } public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mRenderNode.requestPositionUpdates(this); - - setWillNotDraw(true); - } - - /** - * Return the SurfaceHolder providing access and control over this - * SurfaceView's underlying surface. - * - * @return SurfaceHolder The holder of the surface. - */ - public SurfaceHolder getHolder() { - return mSurfaceHolder; - } - - private void updateRequestedVisibility() { - mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped; - } - - /** @hide */ - @Override - public void windowStopped(boolean stopped) { - mWindowStopped = stopped; - updateRequestedVisibility(); - updateSurface(); } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - getViewRootImpl().addWindowStoppedCallback(this); - mWindowStopped = false; - - mViewVisibility = getVisibility() == VISIBLE; - updateRequestedVisibility(); - - mAttachedToWindow = true; - if (!mGlobalListenersAdded) { - ViewTreeObserver observer = getViewTreeObserver(); - observer.addOnScrollChangedListener(mScrollChangedListener); - observer.addOnPreDrawListener(mDrawListener); - mGlobalListenersAdded = true; - } - } - - @Override - protected void onWindowVisibilityChanged(int visibility) { - super.onWindowVisibilityChanged(visibility); - mWindowVisibility = visibility == VISIBLE; - updateRequestedVisibility(); - updateSurface(); - } - - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - mViewVisibility = visibility == VISIBLE; - boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped; - if (newRequestedVisible != mRequestedVisible) { - // our base class (View) invalidates the layout only when - // we go from/to the GONE state. However, SurfaceView needs - // to request a re-layout when the visibility changes at all. - // This is needed because the transparent region is computed - // as part of the layout phase, and it changes (obviously) when - // the visibility changes. - requestLayout(); - } - mRequestedVisible = newRequestedVisible; - updateSurface(); - } - - private void performDrawFinished() { - if (mPendingReportDraws > 0) { - mDrawFinished = true; - if (mAttachedToWindow) { - mParent.requestTransparentRegion(SurfaceView.this); - - notifyDrawFinished(); - invalidate(); - } - } else { - Log.e(TAG, System.identityHashCode(this) + "finished drawing" - + " but no pending report draw (extra call" - + " to draw completion runnable?)"); - } - } - - void notifyDrawFinished() { - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot != null) { - viewRoot.pendingDrawFinished(); - } - mPendingReportDraws--; - } - - @Override - protected void onDetachedFromWindow() { - ViewRootImpl viewRoot = getViewRootImpl(); - // It's possible to create a SurfaceView using the default constructor and never - // attach it to a view hierarchy, this is a common use case when dealing with - // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage - // the lifecycle. Instead of attaching it to a view, he/she can just pass - // the SurfaceHolder forward, most live wallpapers do it. - if (viewRoot != null) { - viewRoot.removeWindowStoppedCallback(this); - } - - mAttachedToWindow = false; - if (mGlobalListenersAdded) { - ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnScrollChangedListener(mScrollChangedListener); - observer.removeOnPreDrawListener(mDrawListener); - mGlobalListenersAdded = false; - } - - while (mPendingReportDraws > 0) { - notifyDrawFinished(); - } - - mRequestedVisible = false; - - updateSurface(); - if (mSurfaceControl != null) { - mSurfaceControl.destroy(); - } - mSurfaceControl = null; - - mHaveFrame = false; - - super.onDetachedFromWindow(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = mRequestedWidth >= 0 - ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0) - : getDefaultSize(0, widthMeasureSpec); - int height = mRequestedHeight >= 0 - ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0) - : getDefaultSize(0, heightMeasureSpec); - setMeasuredDimension(width, height); - } - - /** @hide */ - @Override - protected boolean setFrame(int left, int top, int right, int bottom) { - boolean result = super.setFrame(left, top, right, bottom); - updateSurface(); - return result; - } - - @Override public boolean gatherTransparentRegion(Region region) { - if (isAboveParent() || !mDrawFinished) { - return super.gatherTransparentRegion(region); - } - - boolean opaque = true; - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { - // this view draws, remove it from the transparent region - opaque = super.gatherTransparentRegion(region); - } else if (region != null) { - int w = getWidth(); - int h = getHeight(); - if (w>0 && h>0) { - getLocationInWindow(mLocation); - // otherwise, punch a hole in the whole hierarchy - int l = mLocation[0]; - int t = mLocation[1]; - region.op(l, t, l+w, t+h, Region.Op.UNION); - } - } - if (PixelFormat.formatHasAlpha(mRequestedFormat)) { - opaque = false; - } - return opaque; + return false; } - @Override - public void draw(Canvas canvas) { - if (mDrawFinished && !isAboveParent()) { - // draw() is not called when SKIP_DRAW is set - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { - // punch a whole in the view-hierarchy below us - canvas.drawColor(0, PorterDuff.Mode.CLEAR); - } - } - super.draw(canvas); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - if (mDrawFinished && !isAboveParent()) { - // draw() is not called when SKIP_DRAW is set - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { - // punch a whole in the view-hierarchy below us - canvas.drawColor(0, PorterDuff.Mode.CLEAR); - } - } - super.dispatchDraw(canvas); - } - - /** - * Control whether the surface view's surface is placed on top of another - * regular surface view in the window (but still behind the window itself). - * This is typically used to place overlays on top of an underlying media - * surface view. - * - * <p>Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. - */ public void setZOrderMediaOverlay(boolean isMediaOverlay) { - mSubLayer = isMediaOverlay - ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER; } - /** - * Control whether the surface view's surface is placed on top of its - * window. Normally it is placed behind the window, to allow it to - * (for the most part) appear to composite with the views in the - * hierarchy. By setting this, you cause it to be placed above the - * window. This means that none of the contents of the window this - * SurfaceView is in will be visible on top of its surface. - * - * <p>Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. - */ public void setZOrderOnTop(boolean onTop) { - if (onTop) { - mSubLayer = APPLICATION_PANEL_SUBLAYER; - } else { - mSubLayer = APPLICATION_MEDIA_SUBLAYER; - } } - /** - * Control whether the surface view's content should be treated as secure, - * preventing it from appearing in screenshots or from being viewed on - * non-secure displays. - * - * <p>Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - * <p>See {@link android.view.Display#FLAG_SECURE} for details. - * - * @param isSecure True if the surface view is secure. - */ public void setSecure(boolean isSecure) { - if (isSecure) { - mSurfaceFlags |= SurfaceControl.SECURE; - } else { - mSurfaceFlags &= ~SurfaceControl.SECURE; - } - } - - private void updateOpaqueFlag() { - if (!PixelFormat.formatHasAlpha(mRequestedFormat)) { - mSurfaceFlags |= SurfaceControl.OPAQUE; - } else { - mSurfaceFlags &= ~SurfaceControl.OPAQUE; - } } - private Rect getParentSurfaceInsets() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } else { - return root.mWindowAttributes.surfaceInsets; - } - } - - /** @hide */ - protected void updateSurface() { - if (!mHaveFrame) { - return; - } - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) { - return; - } - - mTranslator = viewRoot.mTranslator; - if (mTranslator != null) { - mSurface.setCompatibilityTranslator(mTranslator); - } - - int myWidth = mRequestedWidth; - if (myWidth <= 0) myWidth = getWidth(); - int myHeight = mRequestedHeight; - if (myHeight <= 0) myHeight = getHeight(); - - final boolean formatChanged = mFormat != mRequestedFormat; - final boolean visibleChanged = mVisible != mRequestedVisible; - final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged) - && mRequestedVisible; - final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight; - final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility; - boolean redrawNeeded = false; - - if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) { - getLocationInWindow(mLocation); - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "Changes: creating=" + creating - + " format=" + formatChanged + " size=" + sizeChanged - + " visible=" + visibleChanged - + " left=" + (mWindowSpaceLeft != mLocation[0]) - + " top=" + (mWindowSpaceTop != mLocation[1])); - - try { - final boolean visible = mVisible = mRequestedVisible; - mWindowSpaceLeft = mLocation[0]; - mWindowSpaceTop = mLocation[1]; - mSurfaceWidth = myWidth; - mSurfaceHeight = myHeight; - mFormat = mRequestedFormat; - mLastWindowVisibility = mWindowVisibility; - - mScreenRect.left = mWindowSpaceLeft; - mScreenRect.top = mWindowSpaceTop; - mScreenRect.right = mWindowSpaceLeft + getWidth(); - mScreenRect.bottom = mWindowSpaceTop + getHeight(); - if (mTranslator != null) { - mTranslator.translateRectInAppWindowToScreen(mScreenRect); - } - - final Rect surfaceInsets = getParentSurfaceInsets(); - mScreenRect.offset(surfaceInsets.left, surfaceInsets.top); - - if (creating) { - mSurfaceSession = new SurfaceSession(viewRoot.mSurface); - mDeferredDestroySurfaceControl = mSurfaceControl; - - updateOpaqueFlag(); - mSurfaceControl = new SurfaceControlWithBackground(mSurfaceSession, - "SurfaceView - " + viewRoot.getTitle().toString(), - mSurfaceWidth, mSurfaceHeight, mFormat, - mSurfaceFlags); - } else if (mSurfaceControl == null) { - return; - } - - boolean realSizeChanged = false; - - mSurfaceLock.lock(); - try { - mDrawingStopped = !visible; - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "Cur surface: " + mSurface); - - SurfaceControl.openTransaction(); - try { - mSurfaceControl.setLayer(mSubLayer); - if (mViewVisibility) { - mSurfaceControl.show(); - } else { - mSurfaceControl.hide(); - } - - // While creating the surface, we will set it's initial - // geometry. Outside of that though, we should generally - // leave it to the RenderThread. - // - // There is one more case when the buffer size changes we aren't yet - // prepared to sync (as even following the transaction applying - // we still need to latch a buffer). - // b/28866173 - if (sizeChanged || creating || !mRtHandlingPositionUpdates) { - mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top); - mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth, - 0.0f, 0.0f, - mScreenRect.height() / (float) mSurfaceHeight); - } - if (sizeChanged) { - mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight); - } - } finally { - SurfaceControl.closeTransaction(); - } - - if (sizeChanged || creating) { - redrawNeeded = true; - } - - mSurfaceFrame.left = 0; - mSurfaceFrame.top = 0; - if (mTranslator == null) { - mSurfaceFrame.right = mSurfaceWidth; - mSurfaceFrame.bottom = mSurfaceHeight; - } else { - float appInvertedScale = mTranslator.applicationInvertedScale; - mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f); - mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f); - } - - final int surfaceWidth = mSurfaceFrame.right; - final int surfaceHeight = mSurfaceFrame.bottom; - realSizeChanged = mLastSurfaceWidth != surfaceWidth - || mLastSurfaceHeight != surfaceHeight; - mLastSurfaceWidth = surfaceWidth; - mLastSurfaceHeight = surfaceHeight; - } finally { - mSurfaceLock.unlock(); - } - - try { - redrawNeeded |= visible && !mDrawFinished; - - SurfaceHolder.Callback callbacks[] = null; - - final boolean surfaceChanged = creating; - if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) { - mSurfaceCreated = false; - if (mSurface.isValid()) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "visibleChanged -- surfaceDestroyed"); - callbacks = getSurfaceCallbacks(); - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceDestroyed(mSurfaceHolder); - } - // Since Android N the same surface may be reused and given to us - // again by the system server at a later point. However - // as we didn't do this in previous releases, clients weren't - // necessarily required to clean up properly in - // surfaceDestroyed. This leads to problems for example when - // clients don't destroy their EGL context, and try - // and create a new one on the same surface following reuse. - // Since there is no valid use of the surface in-between - // surfaceDestroyed and surfaceCreated, we force a disconnect, - // so the next connect will always work if we end up reusing - // the surface. - if (mSurface.isValid()) { - mSurface.forceScopedDisconnect(); - } - } - } - - if (creating) { - mSurface.copyFrom(mSurfaceControl); - } - - if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion - < Build.VERSION_CODES.O) { - // Some legacy applications use the underlying native {@link Surface} object - // as a key to whether anything has changed. In these cases, updates to the - // existing {@link Surface} will be ignored when the size changes. - // Therefore, we must explicitly recreate the {@link Surface} in these - // cases. - mSurface.createFrom(mSurfaceControl); - } - - if (visible && mSurface.isValid()) { - if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) { - mSurfaceCreated = true; - mIsCreating = true; - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "visibleChanged -- surfaceCreated"); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceCreated(mSurfaceHolder); - } - } - if (creating || formatChanged || sizeChanged - || visibleChanged || realSizeChanged) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "surfaceChanged -- format=" + mFormat - + " w=" + myWidth + " h=" + myHeight); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight); - } - } - if (redrawNeeded) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "surfaceRedrawNeeded"); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - - mPendingReportDraws++; - viewRoot.drawPending(); - SurfaceCallbackHelper sch = - new SurfaceCallbackHelper(this::onDrawFinished); - sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks); - } - } - } finally { - mIsCreating = false; - if (mSurfaceControl != null && !mSurfaceCreated) { - mSurface.release(); - // If we are not in the stopped state, then the destruction of the Surface - // represents a visual change we need to display, and we should go ahead - // and destroy the SurfaceControl. However if we are in the stopped state, - // we can just leave the Surface around so it can be a part of animations, - // and we let the life-time be tied to the parent surface. - if (!mWindowStopped) { - mSurfaceControl.destroy(); - mSurfaceControl = null; - } - } - } - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - if (DEBUG) Log.v( - TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top - + " w=" + mScreenRect.width() + " h=" + mScreenRect.height() - + ", frame=" + mSurfaceFrame); - } else { - // Calculate the window position in case RT loses the window - // and we need to fallback to a UI-thread driven position update - getLocationInSurface(mLocation); - final boolean positionChanged = mWindowSpaceLeft != mLocation[0] - || mWindowSpaceTop != mLocation[1]; - final boolean layoutSizeChanged = getWidth() != mScreenRect.width() - || getHeight() != mScreenRect.height(); - if (positionChanged || layoutSizeChanged) { // Only the position has changed - mWindowSpaceLeft = mLocation[0]; - mWindowSpaceTop = mLocation[1]; - // For our size changed check, we keep mScreenRect.width() and mScreenRect.height() - // in view local space. - mLocation[0] = getWidth(); - mLocation[1] = getHeight(); - - mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop, - mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]); - - if (mTranslator != null) { - mTranslator.translateRectInAppWindowToScreen(mScreenRect); - } - - if (mSurfaceControl == null) { - return; - } - - if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) { - try { - if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - mScreenRect.left, mScreenRect.top, - mScreenRect.right, mScreenRect.bottom)); - setParentSpaceRectangle(mScreenRect, -1); - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - } - } - } - } - - private void onDrawFinished() { - if (DEBUG) { - Log.i(TAG, System.identityHashCode(this) + " " - + "finishedDrawing"); - } - - if (mDeferredDestroySurfaceControl != null) { - mDeferredDestroySurfaceControl.destroy(); - mDeferredDestroySurfaceControl = null; - } - - runOnUiThread(() -> { - performDrawFinished(); - }); - } - - private void setParentSpaceRectangle(Rect position, long frameNumber) { - ViewRootImpl viewRoot = getViewRootImpl(); - - SurfaceControl.openTransaction(); - try { - if (frameNumber > 0) { - mSurfaceControl.deferTransactionUntil(viewRoot.mSurface, frameNumber); - } - mSurfaceControl.setPosition(position.left, position.top); - mSurfaceControl.setMatrix(position.width() / (float) mSurfaceWidth, - 0.0f, 0.0f, - position.height() / (float) mSurfaceHeight); - } finally { - SurfaceControl.closeTransaction(); - } - } - - private Rect mRTLastReportedPosition = new Rect(); - - /** - * Called by native by a Rendering Worker thread to update the window position - * @hide - */ - public final void updateSurfacePosition_renderWorker(long frameNumber, - int left, int top, int right, int bottom) { - if (mSurfaceControl == null) { - return; - } - - // TODO: This is teensy bit racey in that a brand new SurfaceView moving on - // its 2nd frame if RenderThread is running slowly could potentially see - // this as false, enter the branch, get pre-empted, then this comes along - // and reports a new position, then the UI thread resumes and reports - // its position. This could therefore be de-sync'd in that interval, but - // the synchronization would violate the rule that RT must never block - // on the UI thread which would open up potential deadlocks. The risk of - // a single-frame desync is therefore preferable for now. - mRtHandlingPositionUpdates = true; - if (mRTLastReportedPosition.left == left - && mRTLastReportedPosition.top == top - && mRTLastReportedPosition.right == right - && mRTLastReportedPosition.bottom == bottom) { - return; - } - try { - if (DEBUG) { - Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - frameNumber, left, top, right, bottom)); - } - mRTLastReportedPosition.set(left, top, right, bottom); - setParentSpaceRectangle(mRTLastReportedPosition, frameNumber); - // Now overwrite mRTLastReportedPosition with our values - } catch (Exception ex) { - Log.e(TAG, "Exception from repositionChild", ex); - } - } - - /** - * Called by native on RenderThread to notify that the view is no longer in the - * draw tree. UI thread is blocked at this point. - * @hide - */ - public final void surfacePositionLost_uiRtSync(long frameNumber) { - if (DEBUG) { - Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d", - System.identityHashCode(this), frameNumber)); - } - mRTLastReportedPosition.setEmpty(); - - if (mSurfaceControl == null) { - return; - } - if (mRtHandlingPositionUpdates) { - mRtHandlingPositionUpdates = false; - // This callback will happen while the UI thread is blocked, so we can - // safely access other member variables at this time. - // So do what the UI thread would have done if RT wasn't handling position - // updates. - if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) { - try { - if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - mScreenRect.left, mScreenRect.top, - mScreenRect.right, mScreenRect.bottom)); - setParentSpaceRectangle(mScreenRect, frameNumber); - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - } - } - } - - private SurfaceHolder.Callback[] getSurfaceCallbacks() { - SurfaceHolder.Callback callbacks[]; - synchronized (mCallbacks) { - callbacks = new SurfaceHolder.Callback[mCallbacks.size()]; - mCallbacks.toArray(callbacks); - } - return callbacks; - } - - /** - * This method still exists only for compatibility reasons because some applications have relied - * on this method via reflection. See Issue 36345857 for details. - * - * @deprecated No platform code is using this method anymore. - * @hide - */ - @Deprecated - public void setWindowType(int type) { - if (getContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) { - throw new UnsupportedOperationException( - "SurfaceView#setWindowType() has never been a public API."); - } - - if (type == TYPE_APPLICATION_PANEL) { - Log.e(TAG, "If you are calling SurfaceView#setWindowType(TYPE_APPLICATION_PANEL) " - + "just to make the SurfaceView to be placed on top of its window, you must " - + "call setZOrderOnTop(true) instead.", new Throwable()); - setZOrderOnTop(true); - return; - } - Log.e(TAG, "SurfaceView#setWindowType(int) is deprecated and now does nothing. " - + "type=" + type, new Throwable()); - } - - private void runOnUiThread(Runnable runnable) { - Handler handler = getHandler(); - if (handler != null && handler.getLooper() != Looper.myLooper()) { - handler.post(runnable); - } else { - runnable.run(); - } - } - - /** - * Check to see if the surface has fixed size dimensions or if the surface's - * dimensions are dimensions are dependent on its current layout. - * - * @return true if the surface has dimensions that are fixed in size - * @hide - */ - public boolean isFixedSize() { - return (mRequestedWidth != -1 || mRequestedHeight != -1); - } - - private boolean isAboveParent() { - return mSubLayer >= 0; + public SurfaceHolder getHolder() { + return mSurfaceHolder; } - private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() { - private static final String LOG_TAG = "SurfaceHolder"; + private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { @Override public boolean isCreating() { - return mIsCreating; + return false; } @Override public void addCallback(Callback callback) { - synchronized (mCallbacks) { - // This is a linear search, but in practice we'll - // have only a couple callbacks, so it doesn't matter. - if (mCallbacks.contains(callback) == false) { - mCallbacks.add(callback); - } - } } @Override public void removeCallback(Callback callback) { - synchronized (mCallbacks) { - mCallbacks.remove(callback); - } } @Override public void setFixedSize(int width, int height) { - if (mRequestedWidth != width || mRequestedHeight != height) { - mRequestedWidth = width; - mRequestedHeight = height; - requestLayout(); - } } @Override public void setSizeFromLayout() { - if (mRequestedWidth != -1 || mRequestedHeight != -1) { - mRequestedWidth = mRequestedHeight = -1; - requestLayout(); - } } @Override public void setFormat(int format) { - // for backward compatibility reason, OPAQUE always - // means 565 for SurfaceView - if (format == PixelFormat.OPAQUE) - format = PixelFormat.RGB_565; - - mRequestedFormat = format; - if (mSurfaceControl != null) { - updateSurface(); - } } - /** - * @deprecated setType is now ignored. - */ @Override - @Deprecated - public void setType(int type) { } + public void setType(int type) { + } @Override public void setKeepScreenOn(boolean screenOn) { - runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn)); } - /** - * Gets a {@link Canvas} for drawing into the SurfaceView's Surface - * - * After drawing into the provided {@link Canvas}, the caller must - * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. - * - * The caller must redraw the entire surface. - * @return A canvas for drawing into the surface. - */ @Override public Canvas lockCanvas() { - return internalLockCanvas(null, false); - } - - /** - * Gets a {@link Canvas} for drawing into the SurfaceView's Surface - * - * After drawing into the provided {@link Canvas}, the caller must - * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. - * - * @param inOutDirty A rectangle that represents the dirty region that the caller wants - * to redraw. This function may choose to expand the dirty rectangle if for example - * the surface has been resized or if the previous contents of the surface were - * not available. The caller must redraw the entire dirty region as represented - * by the contents of the inOutDirty rectangle upon return from this function. - * The caller may also pass <code>null</code> instead, in the case where the - * entire surface should be redrawn. - * @return A canvas for drawing into the surface. - */ - @Override - public Canvas lockCanvas(Rect inOutDirty) { - return internalLockCanvas(inOutDirty, false); + return null; } @Override - public Canvas lockHardwareCanvas() { - return internalLockCanvas(null, true); - } - - private Canvas internalLockCanvas(Rect dirty, boolean hardware) { - mSurfaceLock.lock(); - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped=" - + mDrawingStopped + ", surfaceControl=" + mSurfaceControl); - - Canvas c = null; - if (!mDrawingStopped && mSurfaceControl != null) { - try { - if (hardware) { - c = mSurface.lockHardwareCanvas(); - } else { - c = mSurface.lockCanvas(dirty); - } - } catch (Exception e) { - Log.e(LOG_TAG, "Exception locking surface", e); - } - } - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c); - if (c != null) { - mLastLockTime = SystemClock.uptimeMillis(); - return c; - } - - // If the Surface is not ready to be drawn, then return null, - // but throttle calls to this function so it isn't called more - // than every 100ms. - long now = SystemClock.uptimeMillis(); - long nextTime = mLastLockTime + 100; - if (nextTime > now) { - try { - Thread.sleep(nextTime-now); - } catch (InterruptedException e) { - } - now = SystemClock.uptimeMillis(); - } - mLastLockTime = now; - mSurfaceLock.unlock(); - + public Canvas lockCanvas(Rect dirty) { return null; } - /** - * Posts the new contents of the {@link Canvas} to the surface and - * releases the {@link Canvas}. - * - * @param canvas The canvas previously obtained from {@link #lockCanvas}. - */ @Override public void unlockCanvasAndPost(Canvas canvas) { - mSurface.unlockCanvasAndPost(canvas); - mSurfaceLock.unlock(); } @Override public Surface getSurface() { - return mSurface; + return null; } @Override public Rect getSurfaceFrame() { - return mSurfaceFrame; + return null; } }; - - class SurfaceControlWithBackground extends SurfaceControl { - private SurfaceControl mBackgroundControl; - private boolean mOpaque = true; - public boolean mVisible = false; - - public SurfaceControlWithBackground(SurfaceSession s, - String name, int w, int h, int format, int flags) - throws Exception { - super(s, name, w, h, format, flags); - mBackgroundControl = new SurfaceControl(s, "Background for - " + name, w, h, - PixelFormat.OPAQUE, flags | SurfaceControl.FX_SURFACE_DIM); - mOpaque = (flags & SurfaceControl.OPAQUE) != 0; - } - - @Override - public void setAlpha(float alpha) { - super.setAlpha(alpha); - mBackgroundControl.setAlpha(alpha); - } - - @Override - public void setLayer(int zorder) { - super.setLayer(zorder); - // -3 is below all other child layers as SurfaceView never goes below -2 - mBackgroundControl.setLayer(-3); - } - - @Override - public void setPosition(float x, float y) { - super.setPosition(x, y); - mBackgroundControl.setPosition(x, y); - } - - @Override - public void setSize(int w, int h) { - super.setSize(w, h); - mBackgroundControl.setSize(w, h); - } - - @Override - public void setWindowCrop(Rect crop) { - super.setWindowCrop(crop); - mBackgroundControl.setWindowCrop(crop); - } - - @Override - public void setFinalCrop(Rect crop) { - super.setFinalCrop(crop); - mBackgroundControl.setFinalCrop(crop); - } - - @Override - public void setLayerStack(int layerStack) { - super.setLayerStack(layerStack); - mBackgroundControl.setLayerStack(layerStack); - } - - @Override - public void setOpaque(boolean isOpaque) { - super.setOpaque(isOpaque); - mOpaque = isOpaque; - updateBackgroundVisibility(); - } - - @Override - public void setSecure(boolean isSecure) { - super.setSecure(isSecure); - } - - @Override - public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { - super.setMatrix(dsdx, dtdx, dsdy, dtdy); - mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy); - } - - @Override - public void hide() { - super.hide(); - mVisible = false; - updateBackgroundVisibility(); - } - - @Override - public void show() { - super.show(); - mVisible = true; - updateBackgroundVisibility(); - } - - @Override - public void destroy() { - super.destroy(); - mBackgroundControl.destroy(); - } - - @Override - public void release() { - super.release(); - mBackgroundControl.release(); - } - - @Override - public void setTransparentRegionHint(Region region) { - super.setTransparentRegionHint(region); - mBackgroundControl.setTransparentRegionHint(region); - } - - @Override - public void deferTransactionUntil(IBinder handle, long frame) { - super.deferTransactionUntil(handle, frame); - mBackgroundControl.deferTransactionUntil(handle, frame); - } - - @Override - public void deferTransactionUntil(Surface barrier, long frame) { - super.deferTransactionUntil(barrier, frame); - mBackgroundControl.deferTransactionUntil(barrier, frame); - } - - void updateBackgroundVisibility() { - if (mOpaque && mVisible) { - mBackgroundControl.show(); - } else { - mBackgroundControl.hide(); - } - } - } } + diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 0b9bc576..11cb046a 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,152 +16,46 @@ package android.view.accessibility; -import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME; - -import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SdkConstant; -import android.annotation.SystemService; -import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.content.res.Resources; -import android.os.Binder; import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.util.ArrayMap; -import android.util.Log; -import android.util.SparseArray; import android.view.IWindow; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.IntPair; - -import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * System level service that serves as an event dispatch for {@link AccessibilityEvent}s, - * and provides facilities for querying the accessibility state of the system. - * Accessibility events are generated when something notable happens in the user interface, + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. + * Such events are generated when something notable happens in the user interface, * for example an {@link android.app.Activity} starts, the focus or selection of a * {@link android.view.View} changes etc. Parties interested in handling accessibility * events implement and register an accessibility service which extends - * {@link android.accessibilityservice.AccessibilityService}. + * {@code android.accessibilityservice.AccessibilityService}. * * @see AccessibilityEvent - * @see AccessibilityNodeInfo - * @see android.accessibilityservice.AccessibilityService - * @see Context#getSystemService - * @see Context#ACCESSIBILITY_SERVICE + * @see android.content.Context#getSystemService */ -@SystemService(Context.ACCESSIBILITY_SERVICE) +@SuppressWarnings("UnusedDeclaration") public final class AccessibilityManager { - private static final boolean DEBUG = false; - - private static final String LOG_TAG = "AccessibilityManager"; - - /** @hide */ - public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001; - - /** @hide */ - public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; - - /** @hide */ - public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004; - - /** @hide */ - public static final int DALTONIZER_DISABLED = -1; - - /** @hide */ - public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; - - /** @hide */ - public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; - - /** @hide */ - public static final int AUTOCLICK_DELAY_DEFAULT = 600; - - /** - * Activity action: Launch UI to manage which accessibility service or feature is assigned - * to the navigation bar Accessibility button. - * <p> - * Input: Nothing. - * </p> - * <p> - * Output: Nothing. - * </p> - * - * @hide - */ - @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON = - "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON"; - - static final Object sInstanceSync = new Object(); - - private static AccessibilityManager sInstance; - - private final Object mLock = new Object(); - - private IAccessibilityManager mService; - - final int mUserId; - - final Handler mHandler; - - final Handler.Callback mCallback; - - boolean mIsEnabled; - int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; + private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); - boolean mIsTouchExplorationEnabled; - - boolean mIsHighTextContrastEnabled; - - private final ArrayMap<AccessibilityStateChangeListener, Handler> - mAccessibilityStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap<TouchExplorationStateChangeListener, Handler> - mTouchExplorationStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap<HighTextContrastChangeListener, Handler> - mHighTextContrastStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap<AccessibilityServicesStateChangeListener, Handler> - mServicesStateChangeListeners = new ArrayMap<>(); - - /** - * Map from a view's accessibility id to the list of request preparers set for that view - */ - private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists; /** - * Listener for the system accessibility state. To listen for changes to the - * accessibility state on the device, implement this interface and register - * it with the system by calling {@link #addAccessibilityStateChangeListener}. + * Listener for the accessibility state. */ public interface AccessibilityStateChangeListener { /** - * Called when the accessibility enabled state changes. + * Called back on change in the accessibility state. * * @param enabled Whether accessibility is enabled. */ - void onAccessibilityStateChanged(boolean enabled); + public void onAccessibilityStateChanged(boolean enabled); } /** @@ -177,24 +71,7 @@ public final class AccessibilityManager { * * @param enabled Whether touch exploration is enabled. */ - void onTouchExplorationStateChanged(boolean enabled); - } - - /** - * Listener for changes to the state of accessibility services. Changes include services being - * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service. - * {@see #addAccessibilityServicesStateChangeListener}. - * - * @hide - */ - public interface AccessibilityServicesStateChangeListener { - - /** - * Called when the state of accessibility services changes. - * - * @param manager The manager that is calling back - */ - void onAccessibilityServicesStateChanged(AccessibilityManager manager); + public void onTouchExplorationStateChanged(boolean enabled); } /** @@ -202,8 +79,6 @@ public final class AccessibilityManager { * the high text contrast state on the device, implement this interface and * register it with the system by calling * {@link #addHighTextContrastStateChangeListener}. - * - * @hide */ public interface HighTextContrastChangeListener { @@ -212,72 +87,26 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - void onHighTextContrastStateChanged(boolean enabled); + public void onHighTextContrastStateChanged(boolean enabled); } private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { - @Override - public void setState(int state) { - // We do not want to change this immediately as the application may - // have already checked that accessibility is on and fired an event, - // that is now propagating up the view tree, Hence, if accessibility - // is now off an exception will be thrown. We want to have the exception - // enforcement to guard against apps that fire unnecessary accessibility - // events when accessibility is off. - mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget(); - } - - @Override - public void notifyServicesStateChanged() { - final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners; - synchronized (mLock) { - if (mServicesStateChangeListeners.isEmpty()) { - return; + public void setState(int state) { } - listeners = new ArrayMap<>(mServicesStateChangeListeners); - } - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final AccessibilityServicesStateChangeListener listener = - mServicesStateChangeListeners.keyAt(i); - mServicesStateChangeListeners.valueAt(i).post(() -> listener - .onAccessibilityServicesStateChanged(AccessibilityManager.this)); - } - } + public void notifyServicesStateChanged() { + } - @Override - public void setRelevantEventTypes(int eventTypes) { - mRelevantEventTypes = eventTypes; - } - }; + public void setRelevantEventTypes(int eventTypes) { + } + }; /** * Get an AccessibilityManager instance (create one if necessary). * - * @param context Context in which this manager operates. - * - * @hide */ public static AccessibilityManager getInstance(Context context) { - synchronized (sInstanceSync) { - if (sInstance == null) { - final int userId; - if (Binder.getCallingUid() == Process.SYSTEM_UID - || context.checkCallingOrSelfPermission( - Manifest.permission.INTERACT_ACROSS_USERS) - == PackageManager.PERMISSION_GRANTED - || context.checkCallingOrSelfPermission( - Manifest.permission.INTERACT_ACROSS_USERS_FULL) - == PackageManager.PERMISSION_GRANTED) { - userId = UserHandle.USER_CURRENT; - } else { - userId = UserHandle.myUserId(); - } - sInstance = new AccessibilityManager(context, null, userId); - } - } return sInstance; } @@ -285,68 +114,21 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. - * @param service An interface to the backing service. - * @param userId User id under which to run. - * - * @hide */ public AccessibilityManager(Context context, IAccessibilityManager service, int userId) { - // Constructor can't be chained because we can't create an instance of an inner class - // before calling another constructor. - mCallback = new MyCallback(); - mHandler = new Handler(context.getMainLooper(), mCallback); - mUserId = userId; - synchronized (mLock) { - tryConnectToServiceLocked(service); - } - } - - /** - * Create an instance. - * - * @param handler The handler to use - * @param service An interface to the backing service. - * @param userId User id under which to run. - * - * @hide - */ - public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) { - mCallback = new MyCallback(); - mHandler = handler; - mUserId = userId; - synchronized (mLock) { - tryConnectToServiceLocked(service); - } } - /** - * @hide - */ public IAccessibilityManagerClient getClient() { return mClient; } /** - * @hide - */ - @VisibleForTesting - public Handler.Callback getCallback() { - return mCallback; - } - - /** - * Returns if the accessibility in the system is enabled. + * Returns if the {@link AccessibilityManager} is enabled. * - * @return True if accessibility is enabled, false otherwise. + * @return True if this {@link AccessibilityManager} is enabled, false otherwise. */ public boolean isEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsEnabled; - } + return false; } /** @@ -355,13 +137,7 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsTouchExplorationEnabled; - } + return true; } /** @@ -371,169 +147,35 @@ public final class AccessibilityManager { * doing its own rendering and does not rely on the platform rendering pipeline. * </p> * - * @return True if high text contrast is enabled, false otherwise. - * - * @hide */ public boolean isHighTextContrastEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsHighTextContrastEnabled; - } + return false; } /** * Sends an {@link AccessibilityEvent}. - * - * @param event The event to send. - * - * @throws IllegalStateException if accessibility is not enabled. - * - * <strong>Note:</strong> The preferred mechanism for sending custom accessibility - * events is through calling - * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} - * instead of this method to allow predecessors to augment/filter events sent by - * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - if (!mIsEnabled) { - Looper myLooper = Looper.myLooper(); - if (myLooper == Looper.getMainLooper()) { - throw new IllegalStateException( - "Accessibility off. Did you forget to check that?"); - } else { - // If we're not running on the thread with the main looper, it's possible for - // the state of accessibility to change between checking isEnabled and - // calling this method. So just log the error rather than throwing the - // exception. - Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); - return; - } - } - if ((event.getEventType() & mRelevantEventTypes) == 0) { - if (DEBUG) { - Log.i(LOG_TAG, "Not dispatching irrelevant event: " + event - + " that is not among " - + AccessibilityEvent.eventTypeToString(mRelevantEventTypes)); - } - return; - } - userId = mUserId; - } - try { - event.setEventTime(SystemClock.uptimeMillis()); - // it is possible that this manager is in the same process as the service but - // client using it is called through Binder from another process. Example: MMS - // app adds a SMS notification and the NotificationManagerService calls this method - long identityToken = Binder.clearCallingIdentity(); - service.sendAccessibilityEvent(event, userId); - Binder.restoreCallingIdentity(identityToken); - if (DEBUG) { - Log.i(LOG_TAG, event + " sent"); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error during sending " + event + " ", re); - } finally { - event.recycle(); - } } /** - * Requests feedback interruption from all accessibility services. + * Requests interruption of the accessibility feedback from all accessibility services. */ public void interrupt() { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - if (!mIsEnabled) { - Looper myLooper = Looper.myLooper(); - if (myLooper == Looper.getMainLooper()) { - throw new IllegalStateException( - "Accessibility off. Did you forget to check that?"); - } else { - // If we're not running on the thread with the main looper, it's possible for - // the state of accessibility to change between checking isEnabled and - // calling this method. So just log the error rather than throwing the - // exception. - Log.e(LOG_TAG, "Interrupt called with accessibility disabled"); - return; - } - } - userId = mUserId; - } - try { - service.interrupt(userId); - if (DEBUG) { - Log.i(LOG_TAG, "Requested interrupt from all services"); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); - } } /** * Returns the {@link ServiceInfo}s of the installed accessibility services. * * @return An unmodifiable list with {@link ServiceInfo}s. - * - * @deprecated Use {@link #getInstalledAccessibilityServiceList()} */ @Deprecated public List<ServiceInfo> getAccessibilityServiceList() { - List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList(); - List<ServiceInfo> services = new ArrayList<>(); - final int infoCount = infos.size(); - for (int i = 0; i < infoCount; i++) { - AccessibilityServiceInfo info = infos.get(i); - services.add(info.getResolveInfo().serviceInfo); - } - return Collections.unmodifiableList(services); + return Collections.emptyList(); } - /** - * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. - * - * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. - */ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return Collections.emptyList(); - } - userId = mUserId; - } - - List<AccessibilityServiceInfo> services = null; - try { - services = service.getInstalledAccessibilityServiceList(userId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); - } - if (services != null) { - return Collections.unmodifiableList(services); - } else { - return Collections.emptyList(); - } + return Collections.emptyList(); } /** @@ -548,48 +190,21 @@ public final class AccessibilityManager { * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN * @see AccessibilityServiceInfo#FEEDBACK_VISUAL - * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE */ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( int feedbackTypeFlags) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return Collections.emptyList(); - } - userId = mUserId; - } - - List<AccessibilityServiceInfo> services = null; - try { - services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); - } - if (services != null) { - return Collections.unmodifiableList(services); - } else { - return Collections.emptyList(); - } + return Collections.emptyList(); } /** * Registers an {@link AccessibilityStateChangeListener} for changes in - * the global accessibility state of the system. Equivalent to calling - * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} - * with a null handler. + * the global accessibility state of the system. * * @param listener The listener. - * @return Always returns {@code true}. + * @return True if successfully registered. */ public boolean addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener) { - addAccessibilityStateChangeListener(listener, null); + AccessibilityStateChangeListener listener) { return true; } @@ -603,40 +218,22 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mAccessibilityStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {} - /** - * Unregisters an {@link AccessibilityStateChangeListener}. - * - * @param listener The listener. - * @return True if the listener was previously registered. - */ public boolean removeAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener) { - synchronized (mLock) { - int index = mAccessibilityStateChangeListeners.indexOfKey(listener); - mAccessibilityStateChangeListeners.remove(listener); - return (index >= 0); - } + AccessibilityStateChangeListener listener) { + return true; } /** * Registers a {@link TouchExplorationStateChangeListener} for changes in - * the global touch exploration state of the system. Equivalent to calling - * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)} - * with a null handler. + * the global touch exploration state of the system. * * @param listener The listener. - * @return Always returns {@code true}. + * @return True if successfully registered. */ public boolean addTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - addTouchExplorationStateChangeListener(listener, null); return true; } @@ -650,103 +247,17 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addTouchExplorationStateChangeListener( - @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mTouchExplorationStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {} /** * Unregisters a {@link TouchExplorationStateChangeListener}. * * @param listener The listener. - * @return True if listener was previously registered. + * @return True if successfully unregistered. */ public boolean removeTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - synchronized (mLock) { - int index = mTouchExplorationStateChangeListeners.indexOfKey(listener); - mTouchExplorationStateChangeListeners.remove(listener); - return (index >= 0); - } - } - - /** - * Registers a {@link AccessibilityServicesStateChangeListener}. - * - * @param listener The listener. - * @param handler The handler on which the listener should be called back, or {@code null} - * for a callback on the process's main handler. - * @hide - */ - public void addAccessibilityServicesStateChangeListener( - @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mServicesStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } - - /** - * Unregisters a {@link AccessibilityServicesStateChangeListener}. - * - * @param listener The listener. - * - * @hide - */ - public void removeAccessibilityServicesStateChangeListener( - @NonNull AccessibilityServicesStateChangeListener listener) { - // Final CopyOnWriteArrayList - no lock needed. - mServicesStateChangeListeners.remove(listener); - } - - /** - * Registers a {@link AccessibilityRequestPreparer}. - */ - public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { - if (mRequestPreparerLists == null) { - mRequestPreparerLists = new SparseArray<>(1); - } - int id = preparer.getView().getAccessibilityViewId(); - List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id); - if (requestPreparerList == null) { - requestPreparerList = new ArrayList<>(1); - mRequestPreparerLists.put(id, requestPreparerList); - } - requestPreparerList.add(preparer); - } - - /** - * Unregisters a {@link AccessibilityRequestPreparer}. - */ - public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { - if (mRequestPreparerLists == null) { - return; - } - int viewId = preparer.getView().getAccessibilityViewId(); - List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId); - if (requestPreparerList != null) { - requestPreparerList.remove(preparer); - if (requestPreparerList.isEmpty()) { - mRequestPreparerLists.remove(viewId); - } - } - } - - /** - * Get the preparers that are registered for an accessibility ID - * - * @param id The ID of interest - * @return The list of preparers, or {@code null} if there are none. - * - * @hide - */ - public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) { - if (mRequestPreparerLists == null) { - return null; - } - return mRequestPreparerLists.get(id); + return true; } /** @@ -758,12 +269,7 @@ public final class AccessibilityManager { * @hide */ public void addHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mHighTextContrastStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {} /** * Unregisters a {@link HighTextContrastChangeListener}. @@ -773,51 +279,7 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) { - synchronized (mLock) { - mHighTextContrastStateChangeListeners.remove(listener); - } - } - - /** - * Check if the accessibility volume stream is active. - * - * @return True if accessibility volume is active (i.e. some service has requested it). False - * otherwise. - * @hide - */ - public boolean isAccessibilityVolumeStreamActive() { - List<AccessibilityServiceInfo> serviceInfos = - getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); - for (int i = 0; i < serviceInfos.size(); i++) { - if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) { - return true; - } - } - return false; - } - - /** - * Report a fingerprint gesture to accessibility. Only available for the system process. - * - * @param keyCode The key code of the gesture - * @return {@code true} if accessibility consumes the event. {@code false} if not. - * @hide - */ - public boolean sendFingerprintGesture(int keyCode) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return false; - } - } - try { - return service.sendFingerprintGesture(keyCode); - } catch (RemoteException e) { - return false; - } - } + @NonNull HighTextContrastChangeListener listener) {} /** * Sets the current state and notifies listeners, if necessary. @@ -825,314 +287,14 @@ public final class AccessibilityManager { * @param stateFlags The state flags. */ private void setStateLocked(int stateFlags) { - final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; - final boolean touchExplorationEnabled = - (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; - final boolean highTextContrastEnabled = - (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0; - - final boolean wasEnabled = mIsEnabled; - final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; - final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; - - // Ensure listeners get current state from isZzzEnabled() calls. - mIsEnabled = enabled; - mIsTouchExplorationEnabled = touchExplorationEnabled; - mIsHighTextContrastEnabled = highTextContrastEnabled; - - if (wasEnabled != enabled) { - notifyAccessibilityStateChanged(); - } - - if (wasTouchExplorationEnabled != touchExplorationEnabled) { - notifyTouchExplorationStateChanged(); - } - - if (wasHighTextContrastEnabled != highTextContrastEnabled) { - notifyHighTextContrastStateChanged(); - } } - /** - * Find an installed service with the specified {@link ComponentName}. - * - * @param componentName The name to match to the service. - * - * @return The info corresponding to the installed service, or {@code null} if no such service - * is installed. - * @hide - */ - public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName( - ComponentName componentName) { - final List<AccessibilityServiceInfo> installedServiceInfos = - getInstalledAccessibilityServiceList(); - if ((installedServiceInfos == null) || (componentName == null)) { - return null; - } - for (int i = 0; i < installedServiceInfos.size(); i++) { - if (componentName.equals(installedServiceInfos.get(i).getComponentName())) { - return installedServiceInfos.get(i); - } - } - return null; - } - - /** - * Adds an accessibility interaction connection interface for a given window. - * @param windowToken The window token to which a connection is added. - * @param connection The connection. - * - * @hide - */ public int addAccessibilityInteractionConnection(IWindow windowToken, IAccessibilityInteractionConnection connection) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return View.NO_ID; - } - userId = mUserId; - } - try { - return service.addAccessibilityInteractionConnection(windowToken, connection, userId); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); - } return View.NO_ID; } - /** - * Removed an accessibility interaction connection interface for a given window. - * @param windowToken The window token to which a connection is removed. - * - * @hide - */ public void removeAccessibilityInteractionConnection(IWindow windowToken) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.removeAccessibilityInteractionConnection(windowToken); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); - } - } - - /** - * Perform the accessibility shortcut if the caller has permission. - * - * @hide - */ - public void performAccessibilityShortcut() { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.performAccessibilityShortcut(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re); - } - } - - /** - * Notifies that the accessibility button in the system's navigation area has been clicked - * - * @hide - */ - public void notifyAccessibilityButtonClicked() { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.notifyAccessibilityButtonClicked(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while dispatching accessibility button click", re); - } - } - - /** - * Notifies that the visibility of the accessibility button in the system's navigation area - * has changed. - * - * @param shown {@code true} if the accessibility button is visible within the system - * navigation area, {@code false} otherwise - * @hide - */ - public void notifyAccessibilityButtonVisibilityChanged(boolean shown) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.notifyAccessibilityButtonVisibilityChanged(shown); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re); - } - } - - /** - * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture - * window. Intended for use by the System UI only. - * - * @param connection The connection to handle the actions. Set to {@code null} to avoid - * affecting the actions. - * - * @hide - */ - public void setPictureInPictureActionReplacingConnection( - @Nullable IAccessibilityInteractionConnection connection) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.setPictureInPictureActionReplacingConnection(connection); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error setting picture in picture action replacement", re); - } } - private IAccessibilityManager getServiceLocked() { - if (mService == null) { - tryConnectToServiceLocked(null); - } - return mService; - } - - private void tryConnectToServiceLocked(IAccessibilityManager service) { - if (service == null) { - IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); - if (iBinder == null) { - return; - } - service = IAccessibilityManager.Stub.asInterface(iBinder); - } - - try { - final long userStateAndRelevantEvents = service.addClient(mClient, mUserId); - setStateLocked(IntPair.first(userStateAndRelevantEvents)); - mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents); - mService = service; - } catch (RemoteException re) { - Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); - } - } - - /** - * Notifies the registered {@link AccessibilityStateChangeListener}s. - */ - private void notifyAccessibilityStateChanged() { - final boolean isEnabled; - final ArrayMap<AccessibilityStateChangeListener, Handler> listeners; - synchronized (mLock) { - if (mAccessibilityStateChangeListeners.isEmpty()) { - return; - } - isEnabled = mIsEnabled; - listeners = new ArrayMap<>(mAccessibilityStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final AccessibilityStateChangeListener listener = - mAccessibilityStateChangeListeners.keyAt(i); - mAccessibilityStateChangeListeners.valueAt(i) - .post(() -> listener.onAccessibilityStateChanged(isEnabled)); - } - } - - /** - * Notifies the registered {@link TouchExplorationStateChangeListener}s. - */ - private void notifyTouchExplorationStateChanged() { - final boolean isTouchExplorationEnabled; - final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners; - synchronized (mLock) { - if (mTouchExplorationStateChangeListeners.isEmpty()) { - return; - } - isTouchExplorationEnabled = mIsTouchExplorationEnabled; - listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final TouchExplorationStateChangeListener listener = - mTouchExplorationStateChangeListeners.keyAt(i); - mTouchExplorationStateChangeListeners.valueAt(i) - .post(() -> listener.onTouchExplorationStateChanged(isTouchExplorationEnabled)); - } - } - - /** - * Notifies the registered {@link HighTextContrastChangeListener}s. - */ - private void notifyHighTextContrastStateChanged() { - final boolean isHighTextContrastEnabled; - final ArrayMap<HighTextContrastChangeListener, Handler> listeners; - synchronized (mLock) { - if (mHighTextContrastStateChangeListeners.isEmpty()) { - return; - } - isHighTextContrastEnabled = mIsHighTextContrastEnabled; - listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final HighTextContrastChangeListener listener = - mHighTextContrastStateChangeListeners.keyAt(i); - mHighTextContrastStateChangeListeners.valueAt(i) - .post(() -> listener.onHighTextContrastStateChanged(isHighTextContrastEnabled)); - } - } - - /** - * Determines if the accessibility button within the system navigation area is supported. - * - * @return {@code true} if the accessibility button is supported on this device, - * {@code false} otherwise - */ - public static boolean isAccessibilityButtonSupported() { - final Resources res = Resources.getSystem(); - return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar); - } - - private final class MyCallback implements Handler.Callback { - public static final int MSG_SET_STATE = 1; - - @Override - public boolean handleMessage(Message message) { - switch (message.what) { - case MSG_SET_STATE: { - // See comment at mClient - final int state = message.arg1; - synchronized (mLock) { - setStateLocked(state); - } - } break; - } - return true; - } - } } diff --git a/android/view/inputmethod/ExtractedText.java b/android/view/inputmethod/ExtractedText.java index 0c5d9e9f..003f221d 100644 --- a/android/view/inputmethod/ExtractedText.java +++ b/android/view/inputmethod/ExtractedText.java @@ -87,6 +87,11 @@ public class ExtractedText implements Parcelable { public int flags; /** + * The hint that has been extracted. + */ + public CharSequence hint; + + /** * Used to package this object into a {@link Parcel}. * * @param dest The {@link Parcel} to be written. @@ -100,6 +105,7 @@ public class ExtractedText implements Parcelable { dest.writeInt(selectionStart); dest.writeInt(selectionEnd); dest.writeInt(this.flags); + TextUtils.writeToParcel(hint, dest, flags); } /** @@ -107,17 +113,18 @@ public class ExtractedText implements Parcelable { */ public static final Parcelable.Creator<ExtractedText> CREATOR = new Parcelable.Creator<ExtractedText>() { - public ExtractedText createFromParcel(Parcel source) { - ExtractedText res = new ExtractedText(); - res.text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - res.startOffset = source.readInt(); - res.partialStartOffset = source.readInt(); - res.partialEndOffset = source.readInt(); - res.selectionStart = source.readInt(); - res.selectionEnd = source.readInt(); - res.flags = source.readInt(); - return res; - } + public ExtractedText createFromParcel(Parcel source) { + ExtractedText res = new ExtractedText(); + res.text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.startOffset = source.readInt(); + res.partialStartOffset = source.readInt(); + res.partialEndOffset = source.readInt(); + res.selectionStart = source.readInt(); + res.selectionEnd = source.readInt(); + res.flags = source.readInt(); + res.hint = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + return res; + } public ExtractedText[] newArray(int size) { return new ExtractedText[size]; diff --git a/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/android/view/textclassifier/logging/SmartSelectionEventTracker.java index 77aea234..8d88ba60 100644 --- a/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ b/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -42,17 +42,16 @@ import java.util.UUID; //TODO: Do not allow any crashes from this class. public final class SmartSelectionEventTracker { - private static final String LOG_TAG = "SmartSelectionEventTracker"; + private static final String LOG_TAG = "SmartSelectEventTracker"; private static final boolean DEBUG_LOG_ENABLED = true; - private static final int START_EVENT_DELTA = MetricsEvent.NOTIFICATION_SINCE_CREATE_MILLIS; - private static final int PREV_EVENT_DELTA = MetricsEvent.NOTIFICATION_SINCE_UPDATE_MILLIS; - private static final int ENTITY_TYPE = MetricsEvent.NOTIFICATION_TAG; - private static final int INDEX = MetricsEvent.NOTIFICATION_SHADE_INDEX; - private static final int TAG = MetricsEvent.FIELD_CLASS_NAME; - private static final int SMART_INDICES = MetricsEvent.FIELD_GESTURE_LENGTH; - private static final int EVENT_INDICES = MetricsEvent.FIELD_CONTEXT; - private static final int SESSION_ID = MetricsEvent.FIELD_INSTANT_APP_LAUNCH_TOKEN; + private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START; + private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS; + private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX; + private static final int VERSION_TAG = MetricsEvent.FIELD_SELECTION_VERSION_TAG; + private static final int SMART_INDICES = MetricsEvent.FIELD_SELECTION_SMART_RANGE; + private static final int EVENT_INDICES = MetricsEvent.FIELD_SELECTION_RANGE; + private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID; private static final String ZERO = "0"; private static final String TEXTVIEW = "textview"; @@ -84,6 +83,7 @@ public final class SmartSelectionEventTracker { private long mSessionStartTime; private long mLastEventTime; private boolean mSmartSelectionTriggered; + private String mVersionTag; public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) { mWidgetType = widgetType; @@ -98,7 +98,8 @@ public final class SmartSelectionEventTracker { public void logEvent(@NonNull SelectionEvent event) { Preconditions.checkNotNull(event); - if (event.mEventType != SelectionEvent.EventType.SELECTION_STARTED && mSessionId == null) { + if (event.mEventType != SelectionEvent.EventType.SELECTION_STARTED && mSessionId == null + && DEBUG_LOG_ENABLED) { Log.d(LOG_TAG, "Selection session not yet started. Ignoring event"); return; } @@ -114,6 +115,7 @@ public final class SmartSelectionEventTracker { case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through case SelectionEvent.EventType.SMART_SELECTION_MULTI: mSmartSelectionTriggered = true; + mVersionTag = getVersionTag(event); mSmartIndices[0] = event.mStart; mSmartIndices[1] = event.mEnd; break; @@ -132,16 +134,15 @@ public final class SmartSelectionEventTracker { } private void writeEvent(SelectionEvent event, long now) { - final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST) + final long prevEventDelta = mLastEventTime == 0 ? 0 : now - mLastEventTime; + final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION) .setType(getLogType(event)) - .setSubtype(event.mEventType) + .setSubtype(getLogSubType(event)) .setPackageName(mContext.getPackageName()) - .setTimestamp(now) .addTaggedData(START_EVENT_DELTA, now - mSessionStartTime) - .addTaggedData(PREV_EVENT_DELTA, now - mLastEventTime) - .addTaggedData(ENTITY_TYPE, event.mEntityType) + .addTaggedData(PREV_EVENT_DELTA, prevEventDelta) .addTaggedData(INDEX, mIndex) - .addTaggedData(TAG, getTag(event)) + .addTaggedData(VERSION_TAG, mVersionTag) .addTaggedData(SMART_INDICES, getSmartDelta()) .addTaggedData(EVENT_INDICES, getEventDelta(event)) .addTaggedData(SESSION_ID, mSessionId); @@ -168,42 +169,120 @@ public final class SmartSelectionEventTracker { mSessionStartTime = 0; mLastEventTime = 0; mSmartSelectionTriggered = false; + mVersionTag = getVersionTag(null); mSessionId = null; } - private int getLogType(SelectionEvent event) { + private static int getLogType(SelectionEvent event) { switch (event.mEventType) { - case SelectionEvent.EventType.SELECTION_STARTED: // fall through - case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through - case SelectionEvent.EventType.SMART_SELECTION_MULTI: // fall through - case SelectionEvent.EventType.AUTO_SELECTION: - return MetricsEvent.TYPE_OPEN; + case SelectionEvent.ActionType.OVERTYPE: + return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE; + case SelectionEvent.ActionType.COPY: + return MetricsEvent.ACTION_TEXT_SELECTION_COPY; + case SelectionEvent.ActionType.PASTE: + return MetricsEvent.ACTION_TEXT_SELECTION_PASTE; + case SelectionEvent.ActionType.CUT: + return MetricsEvent.ACTION_TEXT_SELECTION_CUT; + case SelectionEvent.ActionType.SHARE: + return MetricsEvent.ACTION_TEXT_SELECTION_SHARE; + case SelectionEvent.ActionType.SMART_SHARE: + return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE; + case SelectionEvent.ActionType.DRAG: + return MetricsEvent.ACTION_TEXT_SELECTION_DRAG; case SelectionEvent.ActionType.ABANDON: - return MetricsEvent.TYPE_CLOSE; + return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON; + case SelectionEvent.ActionType.OTHER: + return MetricsEvent.ACTION_TEXT_SELECTION_OTHER; + case SelectionEvent.ActionType.SELECT_ALL: + return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL; + case SelectionEvent.ActionType.RESET: + return MetricsEvent.ACTION_TEXT_SELECTION_RESET; + case SelectionEvent.EventType.SELECTION_STARTED: + return MetricsEvent.ACTION_TEXT_SELECTION_START; + case SelectionEvent.EventType.SELECTION_MODIFIED: + return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY; + case SelectionEvent.EventType.SMART_SELECTION_SINGLE: + return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE; + case SelectionEvent.EventType.SMART_SELECTION_MULTI: + return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI; + case SelectionEvent.EventType.AUTO_SELECTION: + return MetricsEvent.ACTION_TEXT_SELECTION_AUTO; + default: + return MetricsEvent.VIEW_UNKNOWN; } - if (event.isActionType()) { - if (event.isTerminal() && mSmartSelectionTriggered) { - if (matchesSmartSelectionBounds(event)) { - // Smart selection accepted. - return MetricsEvent.TYPE_SUCCESS; - } else if (containsOriginalSelection(event)) { - // Smart selection rejected. - return MetricsEvent.TYPE_FAILURE; - } - // User changed the original selection entirely. - } - return MetricsEvent.TYPE_ACTION; - } else { - return MetricsEvent.TYPE_UPDATE; + } + + private static String getLogTypeString(int logType) { + switch (logType) { + case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE: + return "OVERTYPE"; + case MetricsEvent.ACTION_TEXT_SELECTION_COPY: + return "COPY"; + case MetricsEvent.ACTION_TEXT_SELECTION_PASTE: + return "PASTE"; + case MetricsEvent.ACTION_TEXT_SELECTION_CUT: + return "CUT"; + case MetricsEvent.ACTION_TEXT_SELECTION_SHARE: + return "SHARE"; + case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE: + return "SMART_SHARE"; + case MetricsEvent.ACTION_TEXT_SELECTION_DRAG: + return "DRAG"; + case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON: + return "ABANDON"; + case MetricsEvent.ACTION_TEXT_SELECTION_OTHER: + return "OTHER"; + case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL: + return "SELECT_ALL"; + case MetricsEvent.ACTION_TEXT_SELECTION_RESET: + return "RESET"; + case MetricsEvent.ACTION_TEXT_SELECTION_START: + return "SELECTION_STARTED"; + case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY: + return "SELECTION_MODIFIED"; + case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE: + return "SMART_SELECTION_SINGLE"; + case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI: + return "SMART_SELECTION_MULTI"; + case MetricsEvent.ACTION_TEXT_SELECTION_AUTO: + return "AUTO_SELECTION"; + default: + return UNKNOWN; } } - private boolean matchesSmartSelectionBounds(SelectionEvent event) { - return event.mStart == mSmartIndices[0] && event.mEnd == mSmartIndices[1]; + private static int getLogSubType(SelectionEvent event) { + switch (event.mEntityType) { + case TextClassifier.TYPE_OTHER: + return MetricsEvent.TEXT_CLASSIFIER_TYPE_OTHER; + case TextClassifier.TYPE_EMAIL: + return MetricsEvent.TEXT_CLASSIFIER_TYPE_EMAIL; + case TextClassifier.TYPE_PHONE: + return MetricsEvent.TEXT_CLASSIFIER_TYPE_PHONE; + case TextClassifier.TYPE_ADDRESS: + return MetricsEvent.TEXT_CLASSIFIER_TYPE_ADDRESS; + case TextClassifier.TYPE_URL: + return MetricsEvent.TEXT_CLASSIFIER_TYPE_URL; + default: + return MetricsEvent.TEXT_CLASSIFIER_TYPE_UNKNOWN; + } } - private boolean containsOriginalSelection(SelectionEvent event) { - return event.mStart <= mOrigStart && event.mEnd > mOrigStart; + private static String getLogSubTypeString(int logSubType) { + switch (logSubType) { + case MetricsEvent.TEXT_CLASSIFIER_TYPE_OTHER: + return TextClassifier.TYPE_OTHER; + case MetricsEvent.TEXT_CLASSIFIER_TYPE_EMAIL: + return TextClassifier.TYPE_EMAIL; + case MetricsEvent.TEXT_CLASSIFIER_TYPE_PHONE: + return TextClassifier.TYPE_PHONE; + case MetricsEvent.TEXT_CLASSIFIER_TYPE_ADDRESS: + return TextClassifier.TYPE_ADDRESS; + case MetricsEvent.TEXT_CLASSIFIER_TYPE_URL: + return TextClassifier.TYPE_URL; + default: + return TextClassifier.TYPE_UNKNOWN; + } } private int getSmartDelta() { @@ -211,8 +290,9 @@ public final class SmartSelectionEventTracker { return (clamp(mSmartIndices[0] - mOrigStart) << 16) | (clamp(mSmartIndices[1] - mOrigStart) & 0xffff); } - // If no smart selection, return start selection indices (i.e. [0, 1]) - return /* (0 << 16) | */ (1 & 0xffff); + // If the smart selection model was not run, return invalid selection indices [0,0]. This + // allows us to tell from the terminal event alone whether the model was run. + return 0; } private int getEventDelta(SelectionEvent event) { @@ -220,7 +300,7 @@ public final class SmartSelectionEventTracker { | (clamp(event.mEnd - mOrigStart) & 0xffff); } - private String getTag(SelectionEvent event) { + private String getVersionTag(@Nullable SelectionEvent event) { final String widgetType; switch (mWidgetType) { case WidgetType.TEXTVIEW: @@ -238,7 +318,9 @@ public final class SmartSelectionEventTracker { default: widgetType = UNKNOWN; } - final String version = Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG); + final String version = event == null + ? SelectionEvent.NO_VERSION_TAG + : Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG); return String.format("%s/%s", widgetType, version); } @@ -253,66 +335,17 @@ public final class SmartSelectionEventTracker { private static void debugLog(LogMaker log) { if (!DEBUG_LOG_ENABLED) return; - final String tag = Objects.toString(log.getTaggedData(TAG), "tag"); + final String tag = Objects.toString(log.getTaggedData(VERSION_TAG), "tag"); final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO)); - - final String event; - switch (log.getSubtype()) { - case SelectionEvent.ActionType.OVERTYPE: - event = "OVERTYPE"; - break; - case SelectionEvent.ActionType.COPY: - event = "COPY"; - break; - case SelectionEvent.ActionType.PASTE: - event = "PASTE"; - break; - case SelectionEvent.ActionType.CUT: - event = "CUT"; - break; - case SelectionEvent.ActionType.SHARE: - event = "SHARE"; - break; - case SelectionEvent.ActionType.SMART_SHARE: - event = "SMART_SHARE"; - break; - case SelectionEvent.ActionType.DRAG: - event = "DRAG"; - break; - case SelectionEvent.ActionType.ABANDON: - event = "ABANDON"; - break; - case SelectionEvent.ActionType.OTHER: - event = "OTHER"; - break; - case SelectionEvent.ActionType.SELECT_ALL: - event = "SELECT_ALL"; - break; - case SelectionEvent.ActionType.RESET: - event = "RESET"; - break; - case SelectionEvent.EventType.SELECTION_STARTED: - String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), ""); - sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1); - Log.d(LOG_TAG, String.format("New selection session: %s(%s)", tag, sessionId)); - event = "SELECTION_STARTED"; - break; - case SelectionEvent.EventType.SELECTION_MODIFIED: - event = "SELECTION_MODIFIED"; - break; - case SelectionEvent.EventType.SMART_SELECTION_SINGLE: - event = "SMART_SELECTION_SINGLE"; - break; - case SelectionEvent.EventType.SMART_SELECTION_MULTI: - event = "SMART_SELECTION_MULTI"; - break; - case SelectionEvent.EventType.AUTO_SELECTION: - event = "AUTO_SELECTION"; - break; - default: - event = "UNKNOWN"; + if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) { + String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), ""); + sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1); + Log.d(LOG_TAG, String.format("New selection session: %s(%s)", tag, sessionId)); } + final String type = getLogTypeString(log.getType()); + final String subType = getLogSubTypeString(log.getSubtype()); + final int smartIndices = Integer.parseInt( Objects.toString(log.getTaggedData(SMART_INDICES), ZERO)); final int smartStart = (short) ((smartIndices & 0xffff0000) >> 16); @@ -323,11 +356,8 @@ public final class SmartSelectionEventTracker { final int eventStart = (short) ((eventIndices & 0xffff0000) >> 16); final int eventEnd = (short) (eventIndices & 0xffff); - final String entity = Objects.toString( - log.getTaggedData(ENTITY_TYPE), TextClassifier.TYPE_UNKNOWN); - - Log.d(LOG_TAG, String.format("%2d: %s, context=%d,%d - old=%d,%d [%s] (%s)", - index, event, eventStart, eventEnd, smartStart, smartEnd, entity, tag)); + Log.d(LOG_TAG, String.format("%2d: %s/%s, context=%d,%d - old=%d,%d (%s)", + index, type, subType, eventStart, eventEnd, smartStart, smartEnd, tag)); } /** diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java index f368c74a..8e1f2183 100644 --- a/android/view/textservice/TextServicesManager.java +++ b/android/view/textservice/TextServicesManager.java @@ -1,213 +1,58 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2016 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 + * 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 + * 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. + * 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 android.view.textservice; -import android.annotation.SystemService; -import android.content.Context; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.ServiceManager.ServiceNotFoundException; -import android.util.Log; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; -import com.android.internal.textservice.ITextServicesManager; - import java.util.Locale; /** - * System API to the overall text services, which arbitrates interaction between applications - * and text services. - * - * The user can change the current text services in Settings. And also applications can specify - * the target text services. - * - * <h3>Architecture Overview</h3> - * - * <p>There are three primary parties involved in the text services - * framework (TSF) architecture:</p> - * - * <ul> - * <li> The <strong>text services manager</strong> as expressed by this class - * is the central point of the system that manages interaction between all - * other parts. It is expressed as the client-side API here which exists - * in each application context and communicates with a global system service - * that manages the interaction across all processes. - * <li> A <strong>text service</strong> implements a particular - * interaction model allowing the client application to retrieve information of text. - * The system binds to the current text service that is in use, causing it to be created and run. - * <li> Multiple <strong>client applications</strong> arbitrate with the text service - * manager for connections to text services. - * </ul> - * - * <h3>Text services sessions</h3> - * <ul> - * <li>The <strong>spell checker session</strong> is one of the text services. - * {@link android.view.textservice.SpellCheckerSession}</li> - * </ul> - * + * A stub class of TextServicesManager for Layout-Lib. */ -@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) public final class TextServicesManager { - private static final String TAG = TextServicesManager.class.getSimpleName(); - private static final boolean DBG = false; - - private static TextServicesManager sInstance; - - private final ITextServicesManager mService; - - private TextServicesManager() throws ServiceNotFoundException { - mService = ITextServicesManager.Stub.asInterface( - ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE)); - } + private static final TextServicesManager sInstance = new TextServicesManager(); + private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0]; /** * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist. * @hide */ public static TextServicesManager getInstance() { - synchronized (TextServicesManager.class) { - if (sInstance == null) { - try { - sInstance = new TextServicesManager(); - } catch (ServiceNotFoundException e) { - throw new IllegalStateException(e); - } - } - return sInstance; - } - } - - /** - * Returns the language component of a given locale string. - */ - private static String parseLanguageFromLocaleString(String locale) { - final int idx = locale.indexOf('_'); - if (idx < 0) { - return locale; - } else { - return locale.substring(0, idx); - } + return sInstance; } - /** - * Get a spell checker session for the specified spell checker - * @param locale the locale for the spell checker. If {@code locale} is null and - * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be - * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true, - * the locale specified in Settings will be returned only when it is same as {@code locale}. - * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is - * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be - * selected. - * @param listener a spell checker session lister for getting results from a spell checker. - * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled - * languages in settings will be returned. - * @return the spell checker session of the spell checker - */ public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale, SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) { - if (listener == null) { - throw new NullPointerException(); - } - if (!referToSpellCheckerLanguageSettings && locale == null) { - throw new IllegalArgumentException("Locale should not be null if you don't refer" - + " settings."); - } - - if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) { - return null; - } - - final SpellCheckerInfo sci; - try { - sci = mService.getCurrentSpellChecker(null); - } catch (RemoteException e) { - return null; - } - if (sci == null) { - return null; - } - SpellCheckerSubtype subtypeInUse = null; - if (referToSpellCheckerLanguageSettings) { - subtypeInUse = getCurrentSpellCheckerSubtype(true); - if (subtypeInUse == null) { - return null; - } - if (locale != null) { - final String subtypeLocale = subtypeInUse.getLocale(); - final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale); - if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) { - return null; - } - } - } else { - final String localeStr = locale.toString(); - for (int i = 0; i < sci.getSubtypeCount(); ++i) { - final SpellCheckerSubtype subtype = sci.getSubtypeAt(i); - final String tempSubtypeLocale = subtype.getLocale(); - final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale); - if (tempSubtypeLocale.equals(localeStr)) { - subtypeInUse = subtype; - break; - } else if (tempSubtypeLanguage.length() >= 2 && - locale.getLanguage().equals(tempSubtypeLanguage)) { - subtypeInUse = subtype; - } - } - } - if (subtypeInUse == null) { - return null; - } - final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener); - try { - mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(), - session.getTextServicesSessionListener(), - session.getSpellCheckerSessionListener(), bundle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - return session; + return null; } /** * @hide */ public SpellCheckerInfo[] getEnabledSpellCheckers() { - try { - final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers(); - if (DBG) { - Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null")); - } - return retval; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return EMPTY_SPELL_CHECKER_INFO; } /** * @hide */ public SpellCheckerInfo getCurrentSpellChecker() { - try { - // Passing null as a locale for ICS - return mService.getCurrentSpellChecker(null); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return null; } /** @@ -215,22 +60,13 @@ public final class TextServicesManager { */ public SpellCheckerSubtype getCurrentSpellCheckerSubtype( boolean allowImplicitlySelectedSubtype) { - try { - // Passing null as a locale until we support multiple enabled spell checker subtypes. - return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return null; } /** * @hide */ public boolean isSpellCheckerEnabled() { - try { - return mService.isSpellCheckerEnabled(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return false; } } diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java index 419b7b24..202f2046 100644 --- a/android/webkit/WebView.java +++ b/android/webkit/WebView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2008 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. @@ -16,3016 +16,223 @@ package android.webkit; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SystemApi; -import android.annotation.Widget; +import com.android.layoutlib.bridge.MockView; + import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.res.Configuration; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; import android.graphics.Picture; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.net.http.SslCertificate; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.Message; -import android.os.RemoteException; -import android.os.StrictMode; -import android.print.PrintDocumentAdapter; -import android.security.KeyChain; import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseArray; -import android.view.DragEvent; -import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewDebug; -import android.view.ViewGroup; -import android.view.ViewHierarchyEncoder; -import android.view.ViewStructure; -import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeProvider; -import android.view.autofill.AutofillValue; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.textclassifier.TextClassifier; -import android.widget.AbsoluteLayout; - -import java.io.BufferedWriter; -import java.io.File; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; -import java.util.Map; /** - * <p>A View that displays web pages. This class is the basis upon which you - * can roll your own web browser or simply display some online content within your Activity. - * It uses the WebKit rendering engine to display - * web pages and includes methods to navigate forward and backward - * through a history, zoom in and out, perform text searches and more. - * - * <p>Note that, in order for your Activity to access the Internet and load web pages - * in a WebView, you must add the {@code INTERNET} permissions to your - * Android Manifest file: - * - * <pre> - * {@code <uses-permission android:name="android.permission.INTERNET" />} - * </pre> - * - * <p>This must be a child of the <a - * href="{@docRoot}guide/topics/manifest/manifest-element.html">{@code <manifest>}</a> - * element. - * - * <p>For more information, read - * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>. - * - * <h3>Basic usage</h3> - * - * <p>By default, a WebView provides no browser-like widgets, does not - * enable JavaScript and web page errors are ignored. If your goal is only - * to display some HTML as a part of your UI, this is probably fine; - * the user won't need to interact with the web page beyond reading - * it, and the web page won't need to interact with the user. If you - * actually want a full-blown web browser, then you probably want to - * invoke the Browser application with a URL Intent rather than show it - * with a WebView. For example: - * <pre> - * Uri uri = Uri.parse("https://www.example.com"); - * Intent intent = new Intent(Intent.ACTION_VIEW, uri); - * startActivity(intent); - * </pre> - * <p>See {@link android.content.Intent} for more information. - * - * <p>To provide a WebView in your own Activity, include a {@code <WebView>} in your layout, - * or set the entire Activity window as a WebView during {@link - * android.app.Activity#onCreate(Bundle) onCreate()}: - * - * <pre class="prettyprint"> - * WebView webview = new WebView(this); - * setContentView(webview); - * </pre> - * - * <p>Then load the desired web page: - * - * <pre> - * // Simplest usage: note that an exception will NOT be thrown - * // if there is an error loading this page (see below). - * webview.loadUrl("https://example.com/"); - * - * // OR, you can also load from an HTML string: - * String summary = "<html><body>You scored <b>192</b> points.</body></html>"; - * webview.loadData(summary, "text/html", null); - * // ... although note that there are restrictions on what this HTML can do. - * // See the JavaDocs for {@link #loadData(String,String,String) loadData()} and {@link - * #loadDataWithBaseURL(String,String,String,String,String) loadDataWithBaseURL()} for more info. - * </pre> - * - * <p>A WebView has several customization points where you can add your - * own behavior. These are: - * - * <ul> - * <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass. - * This class is called when something that might impact a - * browser UI happens, for instance, progress updates and - * JavaScript alerts are sent here (see <a - * href="{@docRoot}guide/developing/debug-tasks.html#DebuggingWebPages">Debugging Tasks</a>). - * </li> - * <li>Creating and setting a {@link android.webkit.WebViewClient} subclass. - * It will be called when things happen that impact the - * rendering of the content, eg, errors or form submissions. You - * can also intercept URL loading here (via {@link - * android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView,String) - * shouldOverrideUrlLoading()}).</li> - * <li>Modifying the {@link android.webkit.WebSettings}, such as - * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean) - * setJavaScriptEnabled()}. </li> - * <li>Injecting Java objects into the WebView using the - * {@link android.webkit.WebView#addJavascriptInterface} method. This - * method allows you to inject Java objects into a page's JavaScript - * context, so that they can be accessed by JavaScript in the page.</li> - * </ul> - * - * <p>Here's a more complicated example, showing error handling, - * settings, and progress notification: - * - * <pre class="prettyprint"> - * // Let's display the progress in the activity title bar, like the - * // browser app does. - * getWindow().requestFeature(Window.FEATURE_PROGRESS); - * - * webview.getSettings().setJavaScriptEnabled(true); - * - * final Activity activity = this; - * webview.setWebChromeClient(new WebChromeClient() { - * public void onProgressChanged(WebView view, int progress) { - * // Activities and WebViews measure progress with different scales. - * // The progress meter will automatically disappear when we reach 100% - * activity.setProgress(progress * 1000); - * } - * }); - * webview.setWebViewClient(new WebViewClient() { - * public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - * Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show(); - * } - * }); - * - * webview.loadUrl("https://developer.android.com/"); - * </pre> - * - * <h3>Zoom</h3> - * - * <p>To enable the built-in zoom, set - * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} - * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}). - * - * <p>NOTE: Using zoom if either the height or width is set to - * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} may lead to undefined behavior - * and should be avoided. - * - * <h3>Cookie and window management</h3> - * - * <p>For obvious security reasons, your application has its own - * cache, cookie store etc.—it does not share the Browser - * application's data. - * - * <p>By default, requests by the HTML to open new windows are - * ignored. This is {@code true} whether they be opened by JavaScript or by - * the target attribute on a link. You can customize your - * {@link WebChromeClient} to provide your own behavior for opening multiple windows, - * and render them in whatever manner you want. - * - * <p>The standard behavior for an Activity is to be destroyed and - * recreated when the device orientation or any other configuration changes. This will cause - * the WebView to reload the current page. If you don't want that, you - * can set your Activity to handle the {@code orientation} and {@code keyboardHidden} - * changes, and then just leave the WebView alone. It'll automatically - * re-orient itself as appropriate. Read <a - * href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a> for - * more information about how to handle configuration changes during runtime. - * - * - * <h3>Building web pages to support different screen densities</h3> - * - * <p>The screen density of a device is based on the screen resolution. A screen with low density - * has fewer available pixels per inch, where a screen with high density - * has more — sometimes significantly more — pixels per inch. The density of a - * screen is important because, other things being equal, a UI element (such as a button) whose - * height and width are defined in terms of screen pixels will appear larger on the lower density - * screen and smaller on the higher density screen. - * For simplicity, Android collapses all actual screen densities into three generalized densities: - * high, medium, and low. - * <p>By default, WebView scales a web page so that it is drawn at a size that matches the default - * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen - * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels - * are bigger). - * Starting with API level {@link android.os.Build.VERSION_CODES#ECLAIR}, WebView supports DOM, CSS, - * and meta tag features to help you (as a web developer) target screens with different screen - * densities. - * <p>Here's a summary of the features you can use to handle different screen densities: - * <ul> - * <li>The {@code window.devicePixelRatio} DOM property. The value of this property specifies the - * default scaling factor used for the current device. For example, if the value of {@code - * window.devicePixelRatio} is "1.0", then the device is considered a medium density (mdpi) device - * and default scaling is not applied to the web page; if the value is "1.5", then the device is - * considered a high density device (hdpi) and the page content is scaled 1.5x; if the - * value is "0.75", then the device is considered a low density device (ldpi) and the content is - * scaled 0.75x.</li> - * <li>The {@code -webkit-device-pixel-ratio} CSS media query. Use this to specify the screen - * densities for which this style sheet is to be used. The corresponding value should be either - * "0.75", "1", or "1.5", to indicate that the styles are for devices with low density, medium - * density, or high density screens, respectively. For example: - * <pre> - * <link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio:1.5)" href="hdpi.css" /></pre> - * <p>The {@code hdpi.css} stylesheet is only used for devices with a screen pixel ration of 1.5, - * which is the high density pixel ratio. - * </li> - * </ul> - * - * <h3>HTML5 Video support</h3> - * - * <p>In order to support inline HTML5 video in your application you need to have hardware - * acceleration turned on. - * - * <h3>Full screen support</h3> - * - * <p>In order to support full screen — for video or other HTML content — you need to set a - * {@link android.webkit.WebChromeClient} and implement both - * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)} - * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is - * missing then the web contents will not be allowed to enter full screen. Optionally you can implement - * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video - * is loading. - * - * <h3>HTML5 Geolocation API support</h3> - * - * <p>For applications targeting Android N and later releases - * (API level > {@link android.os.Build.VERSION_CODES#M}) the geolocation api is only supported on - * secure origins such as https. For such applications requests to geolocation api on non-secure - * origins are automatically denied without invoking the corresponding - * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)} - * method. - * - * <h3>Layout size</h3> - * <p> - * It is recommended to set the WebView layout height to a fixed value or to - * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using - * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. - * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} - * for the height none of the WebView's parents should use a - * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in - * incorrect sizing of the views. - * - * <p>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} - * enables the following behaviors: - * <ul> - * <li>The HTML body layout height is set to a fixed value. This means that elements with a height - * relative to the HTML body may not be sized correctly. </li> - * <li>For applications targeting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the - * HTML viewport meta tag will be ignored in order to preserve backwards compatibility. </li> - * </ul> - * - * <p> - * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not - * supported. If such a width is used the WebView will attempt to use the width of the parent - * instead. - * - * <h3>Metrics</h3> - * - * <p> - * WebView may upload anonymous diagnostic data to Google when the user has consented. This data - * helps Google improve WebView. Data is collected on a per-app basis for each app which has - * instantiated a WebView. An individual app can opt out of this feature by putting the following - * tag in its manifest: - * <pre> - * <meta-data android:name="android.webkit.WebView.MetricsOptOut" - * android:value="true" /> - * </pre> - * <p> - * Data will only be uploaded for a given app if the user has consented AND the app has not opted - * out. - * - * <h3>Safe Browsing</h3> - * - * <p> - * If Safe Browsing is enabled, WebView will block malicious URLs and present a warning UI to the - * user to allow them to navigate back safely or proceed to the malicious page. - * <p> - * The recommended way for apps to enable the feature is putting the following tag in the manifest: - * <p> - * <pre> - * <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" - * android:value="true" /> - * </pre> + * Mock version of the WebView. + * Only non override public methods from the real WebView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. * */ -// Implementation notes. -// The WebView is a thin API class that delegates its public API to a backend WebViewProvider -// class instance. WebView extends {@link AbsoluteLayout} for backward compatibility reasons. -// Methods are delegated to the provider implementation: all public API methods introduced in this -// file are fully delegated, whereas public and protected methods from the View base classes are -// only delegated where a specific need exists for them to do so. -@Widget -public class WebView extends AbsoluteLayout - implements ViewTreeObserver.OnGlobalFocusChangeListener, - ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler { - - private static final String LOGTAG = "WebView"; - - // Throwing an exception for incorrect thread usage if the - // build target is JB MR2 or newer. Defaults to false, and is - // set in the WebView constructor. - private static volatile boolean sEnforceThreadChecking = false; - - /** - * Transportation object for returning WebView across thread boundaries. - */ - public class WebViewTransport { - private WebView mWebview; +public class WebView extends MockView { - /** - * Sets the WebView to the transportation object. - * - * @param webview the WebView to transport - */ - public synchronized void setWebView(WebView webview) { - mWebview = webview; - } - - /** - * Gets the WebView object. - * - * @return the transported WebView object - */ - public synchronized WebView getWebView() { - return mWebview; - } - } - - /** - * URI scheme for telephone number. - */ - public static final String SCHEME_TEL = "tel:"; /** - * URI scheme for email address. - */ - public static final String SCHEME_MAILTO = "mailto:"; - /** - * URI scheme for map address. - */ - public static final String SCHEME_GEO = "geo:0,0?q="; - - /** - * Interface to listen for find results. - */ - public interface FindListener { - /** - * Notifies the listener about progress made by a find operation. - * - * @param activeMatchOrdinal the zero-based ordinal of the currently selected match - * @param numberOfMatches how many matches have been found - * @param isDoneCounting whether the find operation has actually completed. The listener - * may be notified multiple times while the - * operation is underway, and the numberOfMatches - * value should not be considered final unless - * isDoneCounting is {@code true}. - */ - public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, - boolean isDoneCounting); - } - - /** - * Callback interface supplied to {@link #postVisualStateCallback} for receiving - * notifications about the visual state. - */ - public static abstract class VisualStateCallback { - /** - * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}. - * - * @param requestId The identifier passed to {@link #postVisualStateCallback} when this - * callback was posted. - */ - public abstract void onComplete(long requestId); - } - - /** - * Interface to listen for new pictures as they change. - * - * @deprecated This interface is now obsolete. - */ - @Deprecated - public interface PictureListener { - /** - * Used to provide notification that the WebView's picture has changed. - * See {@link WebView#capturePicture} for details of the picture. - * - * @param view the WebView that owns the picture - * @param picture the new picture. Applications targeting - * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above - * will always receive a {@code null} Picture. - * @deprecated Deprecated due to internal changes. - */ - @Deprecated - public void onNewPicture(WebView view, Picture picture); - } - - public static class HitTestResult { - /** - * Default HitTestResult, where the target is unknown. - */ - public static final int UNKNOWN_TYPE = 0; - /** - * @deprecated This type is no longer used. - */ - @Deprecated - public static final int ANCHOR_TYPE = 1; - /** - * HitTestResult for hitting a phone number. - */ - public static final int PHONE_TYPE = 2; - /** - * HitTestResult for hitting a map address. - */ - public static final int GEO_TYPE = 3; - /** - * HitTestResult for hitting an email address. - */ - public static final int EMAIL_TYPE = 4; - /** - * HitTestResult for hitting an HTML::img tag. - */ - public static final int IMAGE_TYPE = 5; - /** - * @deprecated This type is no longer used. - */ - @Deprecated - public static final int IMAGE_ANCHOR_TYPE = 6; - /** - * HitTestResult for hitting a HTML::a tag with src=http. - */ - public static final int SRC_ANCHOR_TYPE = 7; - /** - * HitTestResult for hitting a HTML::a tag with src=http + HTML::img. - */ - public static final int SRC_IMAGE_ANCHOR_TYPE = 8; - /** - * HitTestResult for hitting an edit text area. - */ - public static final int EDIT_TEXT_TYPE = 9; - - private int mType; - private String mExtra; - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public HitTestResult() { - mType = UNKNOWN_TYPE; - } - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public void setType(int type) { - mType = type; - } - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public void setExtra(String extra) { - mExtra = extra; - } - - /** - * Gets the type of the hit test result. See the XXX_TYPE constants - * defined in this class. - * - * @return the type of the hit test result - */ - public int getType() { - return mType; - } - - /** - * Gets additional type-dependant information about the result. See - * {@link WebView#getHitTestResult()} for details. May either be {@code null} - * or contain extra information about this result. - * - * @return additional type-dependant information about the result - */ - public String getExtra() { - return mExtra; - } - } - - /** - * Constructs a new WebView with a Context object. - * - * @param context a Context object used to access application assets + * Construct a new WebView with a Context object. + * @param context A Context object used to access application assets. */ public WebView(Context context) { this(context, null); } /** - * Constructs a new WebView with layout parameters. - * - * @param context a Context object used to access application assets - * @param attrs an AttributeSet passed to our parent + * Construct a new WebView with layout parameters. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. */ public WebView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.webViewStyle); } /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context a Context object used to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. + * Construct a new WebView with layout parameters and a default style. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. */ - public WebView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); + public WebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); } - - /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context a Context object used to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. - * @param defStyleRes a resource identifier of a style resource that - * supplies default values for the view, used only if - * defStyleAttr is 0 or can not be found in the theme. Can be 0 - * to not look for defaults. - */ - public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - this(context, attrs, defStyleAttr, defStyleRes, null, false); - } - - /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context a Context object used to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. - * @param privateBrowsing whether this WebView will be initialized in - * private mode - * - * @deprecated Private browsing is no longer supported directly via - * WebView and will be removed in a future release. Prefer using - * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager} - * and {@link WebStorage} for fine-grained control of privacy data. - */ - @Deprecated - public WebView(Context context, AttributeSet attrs, int defStyleAttr, - boolean privateBrowsing) { - this(context, attrs, defStyleAttr, 0, null, privateBrowsing); - } - - /** - * Constructs a new WebView with layout parameters, a default style and a set - * of custom JavaScript interfaces to be added to this WebView at initialization - * time. This guarantees that these interfaces will be available when the JS - * context is initialized. - * - * @param context a Context object used to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. - * @param javaScriptInterfaces a Map of interface names, as keys, and - * object implementing those interfaces, as - * values - * @param privateBrowsing whether this WebView will be initialized in - * private mode - * @hide This is used internally by dumprendertree, as it requires the JavaScript interfaces to - * be added synchronously, before a subsequent loadUrl call takes effect. - */ - protected WebView(Context context, AttributeSet attrs, int defStyleAttr, - Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { - this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing); - } - - /** - * @hide - */ - @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor. - protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, - Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { - super(context, attrs, defStyleAttr, defStyleRes); - - // WebView is important by default, unless app developer overrode attribute. - if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) { - setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES); - } - - if (context == null) { - throw new IllegalArgumentException("Invalid context argument"); - } - sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >= - Build.VERSION_CODES.JELLY_BEAN_MR2; - checkThread(); - - ensureProviderCreated(); - mProvider.init(javaScriptInterfaces, privateBrowsing); - // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed. - CookieSyncManager.setGetInstanceIsAllowed(); - } - - /** - * Specifies whether the horizontal scrollbar has overlay style. - * - * @deprecated This method has no effect. - * @param overlay {@code true} if horizontal scrollbar should have overlay style - */ - @Deprecated + + // START FAKE PUBLIC METHODS + public void setHorizontalScrollbarOverlay(boolean overlay) { } - /** - * Specifies whether the vertical scrollbar has overlay style. - * - * @deprecated This method has no effect. - * @param overlay {@code true} if vertical scrollbar should have overlay style - */ - @Deprecated public void setVerticalScrollbarOverlay(boolean overlay) { } - /** - * Gets whether horizontal scrollbar has overlay style. - * - * @deprecated This method is now obsolete. - * @return {@code true} - */ - @Deprecated public boolean overlayHorizontalScrollbar() { - // The old implementation defaulted to true, so return true for consistency - return true; + return false; } - /** - * Gets whether vertical scrollbar has overlay style. - * - * @deprecated This method is now obsolete. - * @return {@code false} - */ - @Deprecated public boolean overlayVerticalScrollbar() { - // The old implementation defaulted to false, so return false for consistency return false; } - /** - * Gets the visible height (in pixels) of the embedded title bar (if any). - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public int getVisibleTitleHeight() { - checkThread(); - return mProvider.getVisibleTitleHeight(); - } - - /** - * Gets the SSL certificate for the main top-level page or {@code null} if there is - * no certificate (the site is not secure). - * - * @return the SSL certificate for the main top-level page - */ - public SslCertificate getCertificate() { - checkThread(); - return mProvider.getCertificate(); - } - - /** - * Sets the SSL certificate for the main top-level page. - * - * @deprecated Calling this function has no useful effect, and will be - * ignored in future releases. - */ - @Deprecated - public void setCertificate(SslCertificate certificate) { - checkThread(); - mProvider.setCertificate(certificate); - } - - //------------------------------------------------------------------------- - // Methods called by activity - //------------------------------------------------------------------------- - - /** - * Sets a username and password pair for the specified host. This data is - * used by the WebView to autocomplete username and password fields in web - * forms. Note that this is unrelated to the credentials used for HTTP - * authentication. - * - * @param host the host that required the credentials - * @param username the username for the given host - * @param password the password for the given host - * @see WebViewDatabase#clearUsernamePassword - * @see WebViewDatabase#hasUsernamePassword - * @deprecated Saving passwords in WebView will not be supported in future versions. - */ - @Deprecated public void savePassword(String host, String username, String password) { - checkThread(); - mProvider.savePassword(host, username, password); } - /** - * Stores HTTP authentication credentials for a given host and realm to the {@link WebViewDatabase} - * instance. - * - * @param host the host to which the credentials apply - * @param realm the realm to which the credentials apply - * @param username the username - * @param password the password - * @deprecated Use {@link WebViewDatabase#setHttpAuthUsernamePassword} instead - */ - @Deprecated public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { - checkThread(); - mProvider.setHttpAuthUsernamePassword(host, realm, username, password); } - /** - * Retrieves HTTP authentication credentials for a given host and realm from the {@link - * WebViewDatabase} instance. - * @param host the host to which the credentials apply - * @param realm the realm to which the credentials apply - * @return the credentials as a String array, if found. The first element - * is the username and the second element is the password. {@code null} if - * no credentials are found. - * @deprecated Use {@link WebViewDatabase#getHttpAuthUsernamePassword} instead - */ - @Deprecated public String[] getHttpAuthUsernamePassword(String host, String realm) { - checkThread(); - return mProvider.getHttpAuthUsernamePassword(host, realm); + return null; } - /** - * Destroys the internal state of this WebView. This method should be called - * after this WebView has been removed from the view system. No other - * methods may be called on this WebView after destroy. - */ public void destroy() { - checkThread(); - mProvider.destroy(); } - /** - * Enables platform notifications of data state and proxy changes. - * Notifications are enabled by default. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated public static void enablePlatformNotifications() { - // noop } - /** - * Disables platform notifications of data state and proxy changes. - * Notifications are enabled by default. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated public static void disablePlatformNotifications() { - // noop } - /** - * Used only by internal tests to free up memory. - * - * @hide - */ - public static void freeMemoryForTests() { - getFactory().getStatics().freeMemoryForTests(); - } - - /** - * Informs WebView of the network state. This is used to set - * the JavaScript property window.navigator.isOnline and - * generates the online/offline event as specified in HTML5, sec. 5.7.7 - * - * @param networkUp a boolean indicating if network is available - */ - public void setNetworkAvailable(boolean networkUp) { - checkThread(); - mProvider.setNetworkAvailable(networkUp); - } - - /** - * Saves the state of this WebView used in - * {@link android.app.Activity#onSaveInstanceState}. Please note that this - * method no longer stores the display data for this WebView. The previous - * behavior could potentially leak files if {@link #restoreState} was never - * called. - * - * @param outState the Bundle to store this WebView's state - * @return the same copy of the back/forward list used to save the state. If - * saveState fails, the returned list will be {@code null}. - */ - public WebBackForwardList saveState(Bundle outState) { - checkThread(); - return mProvider.saveState(outState); - } - - /** - * Saves the current display data to the Bundle given. Used in conjunction - * with {@link #saveState}. - * @param b a Bundle to store the display data - * @param dest the file to store the serialized picture data. Will be - * overwritten with this WebView's picture data. - * @return {@code true} if the picture was successfully saved - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public boolean savePicture(Bundle b, final File dest) { - checkThread(); - return mProvider.savePicture(b, dest); - } - - /** - * Restores the display data that was saved in {@link #savePicture}. Used in - * conjunction with {@link #restoreState}. Note that this will not work if - * this WebView is hardware accelerated. - * - * @param b a Bundle containing the saved display data - * @param src the file where the picture data was stored - * @return {@code true} if the picture was successfully restored - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public boolean restorePicture(Bundle b, File src) { - checkThread(); - return mProvider.restorePicture(b, src); - } - - /** - * Restores the state of this WebView from the given Bundle. This method is - * intended for use in {@link android.app.Activity#onRestoreInstanceState} - * and should be called to restore the state of this WebView. If - * it is called after this WebView has had a chance to build state (load - * pages, create a back/forward list, etc.) there may be undesirable - * side-effects. Please note that this method no longer restores the - * display data for this WebView. - * - * @param inState the incoming Bundle of state - * @return the restored back/forward list or {@code null} if restoreState failed - */ - public WebBackForwardList restoreState(Bundle inState) { - checkThread(); - return mProvider.restoreState(inState); - } - - /** - * Loads the given URL with the specified additional HTTP headers. - * <p> - * Also see compatibility note on {@link #evaluateJavascript}. - * - * @param url the URL of the resource to load - * @param additionalHttpHeaders the additional headers to be used in the - * HTTP request for this URL, specified as a map from name to - * value. Note that if this map contains any of the headers - * that are set by default by this WebView, such as those - * controlling caching, accept types or the User-Agent, their - * values may be overridden by this WebView's defaults. - */ - public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { - checkThread(); - mProvider.loadUrl(url, additionalHttpHeaders); - } - - /** - * Loads the given URL. - * <p> - * Also see compatibility note on {@link #evaluateJavascript}. - * - * @param url the URL of the resource to load - */ public void loadUrl(String url) { - checkThread(); - mProvider.loadUrl(url); - } - - /** - * Loads the URL with postData using "POST" method into this WebView. If url - * is not a network URL, it will be loaded with {@link #loadUrl(String)} - * instead, ignoring the postData param. - * - * @param url the URL of the resource to load - * @param postData the data will be passed to "POST" request, which must be - * be "application/x-www-form-urlencoded" encoded. - */ - public void postUrl(String url, byte[] postData) { - checkThread(); - if (URLUtil.isNetworkUrl(url)) { - mProvider.postUrl(url, postData); - } else { - mProvider.loadUrl(url); - } } - /** - * Loads the given data into this WebView using a 'data' scheme URL. - * <p> - * Note that JavaScript's same origin policy means that script running in a - * page loaded using this method will be unable to access content loaded - * using any scheme other than 'data', including 'http(s)'. To avoid this - * restriction, use {@link - * #loadDataWithBaseURL(String,String,String,String,String) - * loadDataWithBaseURL()} with an appropriate base URL. - * <p> - * The encoding parameter specifies whether the data is base64 or URL - * encoded. If the data is base64 encoded, the value of the encoding - * parameter must be 'base64'. For all other values of the parameter, - * including {@code null}, it is assumed that the data uses ASCII encoding for - * octets inside the range of safe URL characters and use the standard %xx - * hex encoding of URLs for octets outside that range. For example, '#', - * '%', '\', '?' should be replaced by %23, %25, %27, %3f respectively. - * <p> - * The 'data' scheme URL formed by this method uses the default US-ASCII - * charset. If you need need to set a different charset, you should form a - * 'data' scheme URL which explicitly specifies a charset parameter in the - * mediatype portion of the URL and call {@link #loadUrl(String)} instead. - * Note that the charset obtained from the mediatype portion of a data URL - * always overrides that specified in the HTML or XML document itself. - * - * @param data a String of data in the given encoding - * @param mimeType the MIME type of the data, e.g. 'text/html' - * @param encoding the encoding of the data - */ public void loadData(String data, String mimeType, String encoding) { - checkThread(); - mProvider.loadData(data, mimeType, encoding); } - /** - * Loads the given data into this WebView, using baseUrl as the base URL for - * the content. The base URL is used both to resolve relative URLs and when - * applying JavaScript's same origin policy. The historyUrl is used for the - * history entry. - * <p> - * Note that content specified in this way can access local device files - * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than - * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'. - * <p> - * If the base URL uses the data scheme, this method is equivalent to - * calling {@link #loadData(String,String,String) loadData()} and the - * historyUrl is ignored, and the data will be treated as part of a data: URL. - * If the base URL uses any other scheme, then the data will be loaded into - * the WebView as a plain string (i.e. not part of a data URL) and any URL-encoded - * entities in the string will not be decoded. - * <p> - * Note that the baseUrl is sent in the 'Referer' HTTP header when - * requesting subresources (images, etc.) of the page loaded using this method. - * - * @param baseUrl the URL to use as the page's base URL. If {@code null} defaults to - * 'about:blank'. - * @param data a String of data in the given encoding - * @param mimeType the MIMEType of the data, e.g. 'text/html'. If {@code null}, - * defaults to 'text/html'. - * @param encoding the encoding of the data - * @param historyUrl the URL to use as the history entry. If {@code null} defaults - * to 'about:blank'. If non-null, this must be a valid URL. - */ public void loadDataWithBaseURL(String baseUrl, String data, - String mimeType, String encoding, String historyUrl) { - checkThread(); - mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + String mimeType, String encoding, String failUrl) { } - /** - * Asynchronously evaluates JavaScript in the context of the currently displayed page. - * If non-null, |resultCallback| will be invoked with any result returned from that - * execution. This method must be called on the UI thread and the callback will - * be made on the UI thread. - * <p> - * Compatibility note. Applications targeting {@link android.os.Build.VERSION_CODES#N} or - * later, JavaScript state from an empty WebView is no longer persisted across navigations like - * {@link #loadUrl(String)}. For example, global variables and functions defined before calling - * {@link #loadUrl(String)} will not exist in the loaded page. Applications should use - * {@link #addJavascriptInterface} instead to persist JavaScript objects across navigations. - * - * @param script the JavaScript to execute. - * @param resultCallback A callback to be invoked when the script execution - * completes with the result of the execution (if any). - * May be {@code null} if no notification of the result is required. - */ - public void evaluateJavascript(String script, ValueCallback<String> resultCallback) { - checkThread(); - mProvider.evaluateJavaScript(script, resultCallback); - } - - /** - * Saves the current view as a web archive. - * - * @param filename the filename where the archive should be placed - */ - public void saveWebArchive(String filename) { - checkThread(); - mProvider.saveWebArchive(filename); - } - - /** - * Saves the current view as a web archive. - * - * @param basename the filename where the archive should be placed - * @param autoname if {@code false}, takes basename to be a file. If {@code true}, basename - * is assumed to be a directory in which a filename will be - * chosen according to the URL of the current page. - * @param callback called after the web archive has been saved. The - * parameter for onReceiveValue will either be the filename - * under which the file was saved, or {@code null} if saving the - * file failed. - */ - public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) { - checkThread(); - mProvider.saveWebArchive(basename, autoname, callback); - } - - /** - * Stops the current load. - */ public void stopLoading() { - checkThread(); - mProvider.stopLoading(); } - /** - * Reloads the current URL. - */ public void reload() { - checkThread(); - mProvider.reload(); } - /** - * Gets whether this WebView has a back history item. - * - * @return {@code true} iff this WebView has a back history item - */ public boolean canGoBack() { - checkThread(); - return mProvider.canGoBack(); + return false; } - /** - * Goes back in the history of this WebView. - */ public void goBack() { - checkThread(); - mProvider.goBack(); } - /** - * Gets whether this WebView has a forward history item. - * - * @return {@code true} iff this WebView has a forward history item - */ public boolean canGoForward() { - checkThread(); - return mProvider.canGoForward(); + return false; } - /** - * Goes forward in the history of this WebView. - */ public void goForward() { - checkThread(); - mProvider.goForward(); } - /** - * Gets whether the page can go back or forward the given - * number of steps. - * - * @param steps the negative or positive number of steps to move the - * history - */ public boolean canGoBackOrForward(int steps) { - checkThread(); - return mProvider.canGoBackOrForward(steps); + return false; } - /** - * Goes to the history item that is the number of steps away from - * the current item. Steps is negative if backward and positive - * if forward. - * - * @param steps the number of steps to take back or forward in the back - * forward list - */ public void goBackOrForward(int steps) { - checkThread(); - mProvider.goBackOrForward(steps); - } - - /** - * Gets whether private browsing is enabled in this WebView. - */ - public boolean isPrivateBrowsingEnabled() { - checkThread(); - return mProvider.isPrivateBrowsingEnabled(); } - /** - * Scrolls the contents of this WebView up by half the view size. - * - * @param top {@code true} to jump to the top of the page - * @return {@code true} if the page was scrolled - */ public boolean pageUp(boolean top) { - checkThread(); - return mProvider.pageUp(top); + return false; } - - /** - * Scrolls the contents of this WebView down by half the page size. - * - * @param bottom {@code true} to jump to bottom of page - * @return {@code true} if the page was scrolled - */ + public boolean pageDown(boolean bottom) { - checkThread(); - return mProvider.pageDown(bottom); - } - - /** - * Posts a {@link VisualStateCallback}, which will be called when - * the current state of the WebView is ready to be drawn. - * - * <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not - * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The - * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of - * the DOM at the current time are ready to be drawn the next time the {@link WebView} - * draws. - * - * <p>The next draw after the callback completes is guaranteed to reflect all the updates to the - * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may also - * contain updates applied after the callback was posted. - * - * <p>The state of the DOM covered by this API includes the following: - * <ul> - * <li>primitive HTML elements (div, img, span, etc..)</li> - * <li>images</li> - * <li>CSS animations</li> - * <li>WebGL</li> - * <li>canvas</li> - * </ul> - * It does not include the state of: - * <ul> - * <li>the video tag</li> - * </ul> - * - * <p>To guarantee that the {@link WebView} will successfully render the first frame - * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions - * must be met: - * <ul> - * <li>If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then - * the {@link WebView} must be attached to the view hierarchy.</li> - * <li>If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE} - * then the {@link WebView} must be attached to the view hierarchy and must be made - * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.</li> - * <li>If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the - * {@link WebView} must be attached to the view hierarchy and its - * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed - * values and must be made {@link View#VISIBLE VISIBLE} from the - * {@link VisualStateCallback#onComplete} method.</li> - * </ul> - * - * <p>When using this API it is also recommended to enable pre-rasterization if the {@link - * WebView} is off screen to avoid flickering. See {@link WebSettings#setOffscreenPreRaster} for - * more details and do consider its caveats. - * - * @param requestId An id that will be returned in the callback to allow callers to match - * requests with callbacks. - * @param callback The callback to be invoked. - */ - public void postVisualStateCallback(long requestId, VisualStateCallback callback) { - checkThread(); - mProvider.insertVisualStateCallback(requestId, callback); + return false; } - /** - * Clears this WebView so that onDraw() will draw nothing but white background, - * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY. - * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state - * and release page resources (including any running JavaScript). - */ - @Deprecated public void clearView() { - checkThread(); - mProvider.clearView(); } - - /** - * Gets a new picture that captures the current contents of this WebView. - * The picture is of the entire document being displayed, and is not - * limited to the area currently displayed by this WebView. Also, the - * picture is a static copy and is unaffected by later changes to the - * content being displayed. - * <p> - * Note that due to internal changes, for API levels between - * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and - * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} inclusive, the - * picture does not include fixed position elements or scrollable divs. - * <p> - * Note that from {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} the returned picture - * should only be drawn into bitmap-backed Canvas - using any other type of Canvas will involve - * additional conversion at a cost in memory and performance. Also the - * {@link android.graphics.Picture#createFromStream} and - * {@link android.graphics.Picture#writeToStream} methods are not supported on the - * returned object. - * - * @deprecated Use {@link #onDraw} to obtain a bitmap snapshot of the WebView, or - * {@link #saveWebArchive} to save the content to a file. - * - * @return a picture that captures the current contents of this WebView - */ - @Deprecated + public Picture capturePicture() { - checkThread(); - return mProvider.capturePicture(); - } - - /** - * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user - * to provide a print document name. - */ - @Deprecated - public PrintDocumentAdapter createPrintDocumentAdapter() { - checkThread(); - return mProvider.createPrintDocumentAdapter("default"); + return null; } - /** - * Creates a PrintDocumentAdapter that provides the content of this WebView for printing. - * - * The adapter works by converting the WebView contents to a PDF stream. The WebView cannot - * be drawn during the conversion process - any such draws are undefined. It is recommended - * to use a dedicated off screen WebView for the printing. If necessary, an application may - * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance - * wrapped around the object returned and observing the onStart and onFinish methods. See - * {@link android.print.PrintDocumentAdapter} for more information. - * - * @param documentName The user-facing name of the printed document. See - * {@link android.print.PrintDocumentInfo} - */ - public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) { - checkThread(); - return mProvider.createPrintDocumentAdapter(documentName); - } - - /** - * Gets the current scale of this WebView. - * - * @return the current scale - * - * @deprecated This method is prone to inaccuracy due to race conditions - * between the web rendering and UI threads; prefer - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - @ViewDebug.ExportedProperty(category = "webview") public float getScale() { - checkThread(); - return mProvider.getScale(); + return 0; } - /** - * Sets the initial scale for this WebView. 0 means default. - * The behavior for the default scale depends on the state of - * {@link WebSettings#getUseWideViewPort()} and - * {@link WebSettings#getLoadWithOverviewMode()}. - * If the content fits into the WebView control by width, then - * the zoom is set to 100%. For wide content, the behavior - * depends on the state of {@link WebSettings#getLoadWithOverviewMode()}. - * If its value is {@code true}, the content will be zoomed out to be fit - * by width into the WebView control, otherwise not. - * - * If initial scale is greater than 0, WebView starts with this value - * as initial scale. - * Please note that unlike the scale properties in the viewport meta tag, - * this method doesn't take the screen density into account. - * - * @param scaleInPercent the initial scale in percent - */ public void setInitialScale(int scaleInPercent) { - checkThread(); - mProvider.setInitialScale(scaleInPercent); } - /** - * Invokes the graphical zoom picker widget for this WebView. This will - * result in the zoom widget appearing on the screen to control the zoom - * level of this WebView. - */ public void invokeZoomPicker() { - checkThread(); - mProvider.invokeZoomPicker(); - } - - /** - * Gets a HitTestResult based on the current cursor node. If a HTML::a - * tag is found and the anchor has a non-JavaScript URL, the HitTestResult - * type is set to SRC_ANCHOR_TYPE and the URL is set in the "extra" field. - * If the anchor does not have a URL or if it is a JavaScript URL, the type - * will be UNKNOWN_TYPE and the URL has to be retrieved through - * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is - * found, the HitTestResult type is set to IMAGE_TYPE and the URL is set in - * the "extra" field. A type of - * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a URL that has an image as - * a child node. If a phone number is found, the HitTestResult type is set - * to PHONE_TYPE and the phone number is set in the "extra" field of - * HitTestResult. If a map address is found, the HitTestResult type is set - * to GEO_TYPE and the address is set in the "extra" field of HitTestResult. - * If an email address is found, the HitTestResult type is set to EMAIL_TYPE - * and the email is set in the "extra" field of HitTestResult. Otherwise, - * HitTestResult type is set to UNKNOWN_TYPE. - */ - public HitTestResult getHitTestResult() { - checkThread(); - return mProvider.getHitTestResult(); } - /** - * Requests the anchor or image element URL at the last tapped point. - * If hrefMsg is {@code null}, this method returns immediately and does not - * dispatch hrefMsg to its target. If the tapped point hits an image, - * an anchor, or an image in an anchor, the message associates - * strings in named keys in its data. The value paired with the key - * may be an empty string. - * - * @param hrefMsg the message to be dispatched with the result of the - * request. The message data contains three keys. "url" - * returns the anchor's href attribute. "title" returns the - * anchor's text. "src" returns the image's src attribute. - */ public void requestFocusNodeHref(Message hrefMsg) { - checkThread(); - mProvider.requestFocusNodeHref(hrefMsg); } - /** - * Requests the URL of the image last touched by the user. msg will be sent - * to its target with a String representing the URL as its object. - * - * @param msg the message to be dispatched with the result of the request - * as the data member with "url" as key. The result can be {@code null}. - */ public void requestImageRef(Message msg) { - checkThread(); - mProvider.requestImageRef(msg); } - /** - * Gets the URL for the current page. This is not always the same as the URL - * passed to WebViewClient.onPageStarted because although the load for - * that URL has begun, the current page may not have changed. - * - * @return the URL for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") public String getUrl() { - checkThread(); - return mProvider.getUrl(); + return null; } - /** - * Gets the original URL for the current page. This is not always the same - * as the URL passed to WebViewClient.onPageStarted because although the - * load for that URL has begun, the current page may not have changed. - * Also, there may have been redirects resulting in a different URL to that - * originally requested. - * - * @return the URL that was originally requested for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") - public String getOriginalUrl() { - checkThread(); - return mProvider.getOriginalUrl(); - } - - /** - * Gets the title for the current page. This is the title of the current page - * until WebViewClient.onReceivedTitle is called. - * - * @return the title for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") public String getTitle() { - checkThread(); - return mProvider.getTitle(); + return null; } - /** - * Gets the favicon for the current page. This is the favicon of the current - * page until WebViewClient.onReceivedIcon is called. - * - * @return the favicon for the current page - */ public Bitmap getFavicon() { - checkThread(); - return mProvider.getFavicon(); + return null; } - /** - * Gets the touch icon URL for the apple-touch-icon <link> element, or - * a URL on this site's server pointing to the standard location of a - * touch icon. - * - * @hide - */ - public String getTouchIconUrl() { - return mProvider.getTouchIconUrl(); - } - - /** - * Gets the progress for the current page. - * - * @return the progress for the current page between 0 and 100 - */ public int getProgress() { - checkThread(); - return mProvider.getProgress(); + return 0; } - - /** - * Gets the height of the HTML content. - * - * @return the height of the HTML content - */ - @ViewDebug.ExportedProperty(category = "webview") + public int getContentHeight() { - checkThread(); - return mProvider.getContentHeight(); - } - - /** - * Gets the width of the HTML content. - * - * @return the width of the HTML content - * @hide - */ - @ViewDebug.ExportedProperty(category = "webview") - public int getContentWidth() { - return mProvider.getContentWidth(); + return 0; } - /** - * Pauses all layout, parsing, and JavaScript timers for all WebViews. This - * is a global requests, not restricted to just this WebView. This can be - * useful if the application has been paused. - */ public void pauseTimers() { - checkThread(); - mProvider.pauseTimers(); } - /** - * Resumes all layout, parsing, and JavaScript timers for all WebViews. - * This will resume dispatching all timers. - */ public void resumeTimers() { - checkThread(); - mProvider.resumeTimers(); - } - - /** - * Does a best-effort attempt to pause any processing that can be paused - * safely, such as animations and geolocation. Note that this call - * does not pause JavaScript. To pause JavaScript globally, use - * {@link #pauseTimers}. - * - * To resume WebView, call {@link #onResume}. - */ - public void onPause() { - checkThread(); - mProvider.onPause(); - } - - /** - * Resumes a WebView after a previous call to {@link #onPause}. - */ - public void onResume() { - checkThread(); - mProvider.onResume(); - } - - /** - * Gets whether this WebView is paused, meaning onPause() was called. - * Calling onResume() sets the paused state back to {@code false}. - * - * @hide - */ - public boolean isPaused() { - return mProvider.isPaused(); - } - - /** - * Informs this WebView that memory is low so that it can free any available - * memory. - * @deprecated Memory caches are automatically dropped when no longer needed, and in response - * to system memory pressure. - */ - @Deprecated - public void freeMemory() { - checkThread(); - mProvider.freeMemory(); } - /** - * Clears the resource cache. Note that the cache is per-application, so - * this will clear the cache for all WebViews used. - * - * @param includeDiskFiles if {@code false}, only the RAM cache is cleared - */ - public void clearCache(boolean includeDiskFiles) { - checkThread(); - mProvider.clearCache(includeDiskFiles); + public void clearCache() { } - /** - * Removes the autocomplete popup from the currently focused form field, if - * present. Note this only affects the display of the autocomplete popup, - * it does not remove any saved form data from this WebView's store. To do - * that, use {@link WebViewDatabase#clearFormData}. - */ public void clearFormData() { - checkThread(); - mProvider.clearFormData(); } - /** - * Tells this WebView to clear its internal back/forward list. - */ public void clearHistory() { - checkThread(); - mProvider.clearHistory(); } - /** - * Clears the SSL preferences table stored in response to proceeding with - * SSL certificate errors. - */ public void clearSslPreferences() { - checkThread(); - mProvider.clearSslPreferences(); - } - - /** - * Clears the client certificate preferences stored in response - * to proceeding/cancelling client cert requests. Note that WebView - * automatically clears these preferences when it receives a - * {@link KeyChain#ACTION_STORAGE_CHANGED} intent. The preferences are - * shared by all the WebViews that are created by the embedder application. - * - * @param onCleared A runnable to be invoked when client certs are cleared. - * The embedder can pass {@code null} if not interested in the - * callback. The runnable will be called in UI thread. - */ - public static void clearClientCertPreferences(Runnable onCleared) { - getFactory().getStatics().clearClientCertPreferences(onCleared); - } - - /** - * Starts Safe Browsing initialization. - * <p> - * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is - * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those - * devices {@code callback} will receive {@code false}. - * <p> - * This does not enable the Safe Browsing feature itself, and should only be called if Safe - * Browsing is enabled by the manifest tag or {@link WebSettings#setSafeBrowsingEnabled}. This - * prepares resources used for Safe Browsing. - * <p> - * This should be called with the Application Context (and will always use the Application - * context to do its work regardless). - * - * @param context Application Context. - * @param callback will be called on the UI thread with {@code true} if initialization is - * successful, {@code false} otherwise. - */ - public static void startSafeBrowsing(Context context, ValueCallback<Boolean> callback) { - getFactory().getStatics().initSafeBrowsing(context, callback); - } - - /** - * Sets the list of domains that are exempt from SafeBrowsing checks. The list is - * global for all the WebViews. - * <p> - * Each rule should take one of these: - * <table> - * <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr> - * <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr> - * <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr> - * <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr> - * <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr> - * </table> - * <p> - * All other rules, including wildcards, are invalid. - * - * @param urls the list of URLs - * @param callback will be called with {@code true} if URLs are successfully added to the - * whitelist. It will be called with {@code false} if any URLs are malformed. The callback will - * be run on the UI thread - */ - public static void setSafeBrowsingWhitelist(@NonNull List<String> urls, - @Nullable ValueCallback<Boolean> callback) { - getFactory().getStatics().setSafeBrowsingWhitelist(urls, callback); - } - - /** - * Returns a URL pointing to the privacy policy for Safe Browsing reporting. - * - * @return the url pointing to a privacy policy document which can be displayed to users. - */ - @NonNull - public static Uri getSafeBrowsingPrivacyPolicyUrl() { - return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl(); - } - - /** - * Gets the WebBackForwardList for this WebView. This contains the - * back/forward list for use in querying each item in the history stack. - * This is a copy of the private WebBackForwardList so it contains only a - * snapshot of the current state. Multiple calls to this method may return - * different objects. The object returned from this method will not be - * updated to reflect any new state. - */ - public WebBackForwardList copyBackForwardList() { - checkThread(); - return mProvider.copyBackForwardList(); - - } - - /** - * Registers the listener to be notified as find-on-page operations - * progress. This will replace the current listener. - * - * @param listener an implementation of {@link FindListener} - */ - public void setFindListener(FindListener listener) { - checkThread(); - setupFindListenerIfNeeded(); - mFindListener.mUserFindListener = listener; - } - - /** - * Highlights and scrolls to the next match found by - * {@link #findAllAsync}, wrapping around page boundaries as necessary. - * Notifies any registered {@link FindListener}. If {@link #findAllAsync(String)} - * has not been called yet, or if {@link #clearMatches} has been called since the - * last find operation, this function does nothing. - * - * @param forward the direction to search - * @see #setFindListener - */ - public void findNext(boolean forward) { - checkThread(); - mProvider.findNext(forward); - } - - /** - * Finds all instances of find on the page and highlights them. - * Notifies any registered {@link FindListener}. - * - * @param find the string to find - * @return the number of occurrences of the String "find" that were found - * @deprecated {@link #findAllAsync} is preferred. - * @see #setFindListener - */ - @Deprecated - public int findAll(String find) { - checkThread(); - StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync"); - return mProvider.findAll(find); - } - - /** - * Finds all instances of find on the page and highlights them, - * asynchronously. Notifies any registered {@link FindListener}. - * Successive calls to this will cancel any pending searches. - * - * @param find the string to find. - * @see #setFindListener - */ - public void findAllAsync(String find) { - checkThread(); - mProvider.findAllAsync(find); - } - - /** - * Starts an ActionMode for finding text in this WebView. Only works if this - * WebView is attached to the view system. - * - * @param text if non-null, will be the initial text to search for. - * Otherwise, the last String searched for in this WebView will - * be used to start. - * @param showIme if {@code true}, show the IME, assuming the user will begin typing. - * If {@code false} and text is non-null, perform a find all. - * @return {@code true} if the find dialog is shown, {@code false} otherwise - * @deprecated This method does not work reliably on all Android versions; - * implementing a custom find dialog using WebView.findAllAsync() - * provides a more robust solution. - */ - @Deprecated - public boolean showFindDialog(String text, boolean showIme) { - checkThread(); - return mProvider.showFindDialog(text, showIme); } - /** - * Gets the first substring consisting of the address of a physical - * location. Currently, only addresses in the United States are detected, - * and consist of: - * <ul> - * <li>a house number</li> - * <li>a street name</li> - * <li>a street type (Road, Circle, etc), either spelled out or - * abbreviated</li> - * <li>a city name</li> - * <li>a state or territory, either spelled out or two-letter abbr</li> - * <li>an optional 5 digit or 9 digit zip code</li> - * </ul> - * All names must be correctly capitalized, and the zip code, if present, - * must be valid for the state. The street type must be a standard USPS - * spelling or abbreviation. The state or territory must also be spelled - * or abbreviated using USPS standards. The house number may not exceed - * five digits. - * - * @param addr the string to search for addresses - * @return the address, or if no address is found, {@code null} - */ public static String findAddress(String addr) { - // TODO: Rewrite this in Java so it is not needed to start up chromium - // Could also be deprecated - return getFactory().getStatics().findAddress(addr); - } - - /** - * For apps targeting the L release, WebView has a new default behavior that reduces - * memory footprint and increases performance by intelligently choosing - * the portion of the HTML document that needs to be drawn. These - * optimizations are transparent to the developers. However, under certain - * circumstances, an App developer may want to disable them: - * <ol> - * <li>When an app uses {@link #onDraw} to do own drawing and accesses portions - * of the page that is way outside the visible portion of the page.</li> - * <li>When an app uses {@link #capturePicture} to capture a very large HTML document. - * Note that capturePicture is a deprecated API.</li> - * </ol> - * Enabling drawing the entire HTML document has a significant performance - * cost. This method should be called before any WebViews are created. - */ - public static void enableSlowWholeDocumentDraw() { - getFactory().getStatics().enableSlowWholeDocumentDraw(); + return null; } - /** - * Clears the highlighting surrounding text matches created by - * {@link #findAllAsync}. - */ - public void clearMatches() { - checkThread(); - mProvider.clearMatches(); - } - - /** - * Queries the document to see if it contains any image references. The - * message object will be dispatched with arg1 being set to 1 if images - * were found and 0 if the document does not reference any images. - * - * @param response the message that will be dispatched with the result - */ public void documentHasImages(Message response) { - checkThread(); - mProvider.documentHasImages(response); } - /** - * Sets the WebViewClient that will receive various notifications and - * requests. This will replace the current handler. - * - * @param client an implementation of WebViewClient - * @see #getWebViewClient - */ public void setWebViewClient(WebViewClient client) { - checkThread(); - mProvider.setWebViewClient(client); - } - - /** - * Gets the WebViewClient. - * - * @return the WebViewClient, or a default client if not yet set - * @see #setWebViewClient - */ - public WebViewClient getWebViewClient() { - checkThread(); - return mProvider.getWebViewClient(); } - /** - * Registers the interface to be used when content can not be handled by - * the rendering engine, and should be downloaded instead. This will replace - * the current handler. - * - * @param listener an implementation of DownloadListener - */ public void setDownloadListener(DownloadListener listener) { - checkThread(); - mProvider.setDownloadListener(listener); } - /** - * Sets the chrome handler. This is an implementation of WebChromeClient for - * use in handling JavaScript dialogs, favicons, titles, and the progress. - * This will replace the current handler. - * - * @param client an implementation of WebChromeClient - * @see #getWebChromeClient - */ public void setWebChromeClient(WebChromeClient client) { - checkThread(); - mProvider.setWebChromeClient(client); } - /** - * Gets the chrome handler. - * - * @return the WebChromeClient, or {@code null} if not yet set - * @see #setWebChromeClient - */ - public WebChromeClient getWebChromeClient() { - checkThread(); - return mProvider.getWebChromeClient(); - } - - /** - * Sets the Picture listener. This is an interface used to receive - * notifications of a new Picture. - * - * @param listener an implementation of WebView.PictureListener - * @deprecated This method is now obsolete. - */ - @Deprecated - public void setPictureListener(PictureListener listener) { - checkThread(); - mProvider.setPictureListener(listener); - } - - /** - * Injects the supplied Java object into this WebView. The object is - * injected into the JavaScript context of the main frame, using the - * supplied name. This allows the Java object's methods to be - * accessed from JavaScript. For applications targeted to API - * level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - * and above, only public methods that are annotated with - * {@link android.webkit.JavascriptInterface} can be accessed from JavaScript. - * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below, - * all public methods (including the inherited ones) can be accessed, see the - * important security note below for implications. - * <p> Note that injected objects will not appear in JavaScript until the page is next - * (re)loaded. JavaScript should be enabled before injecting the object. For example: - * <pre> - * class JsObject { - * {@literal @}JavascriptInterface - * public String toString() { return "injectedObject"; } - * } - * webview.getSettings().setJavaScriptEnabled(true); - * webView.addJavascriptInterface(new JsObject(), "injectedObject"); - * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null); - * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre> - * <p> - * <strong>IMPORTANT:</strong> - * <ul> - * <li> This method can be used to allow JavaScript to control the host - * application. This is a powerful feature, but also presents a security - * risk for apps targeting {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or earlier. - * Apps that target a version later than {@link android.os.Build.VERSION_CODES#JELLY_BEAN} - * are still vulnerable if the app runs on a device running Android earlier than 4.2. - * The most secure way to use this method is to target {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - * and to ensure the method is called only when running on Android 4.2 or later. - * With these older versions, JavaScript could use reflection to access an - * injected object's public fields. Use of this method in a WebView - * containing untrusted content could allow an attacker to manipulate the - * host application in unintended ways, executing Java code with the - * permissions of the host application. Use extreme care when using this - * method in a WebView which could contain untrusted content.</li> - * <li> JavaScript interacts with Java object on a private, background - * thread of this WebView. Care is therefore required to maintain thread - * safety. - * </li> - * <li> The Java object's fields are not accessible.</li> - * <li> For applications targeted to API level {@link android.os.Build.VERSION_CODES#LOLLIPOP} - * and above, methods of injected Java objects are enumerable from - * JavaScript.</li> - * </ul> - * - * @param object the Java object to inject into this WebView's JavaScript - * context. {@code null} values are ignored. - * @param name the name used to expose the object in JavaScript - */ - public void addJavascriptInterface(Object object, String name) { - checkThread(); - mProvider.addJavascriptInterface(object, name); - } - - /** - * Removes a previously injected Java object from this WebView. Note that - * the removal will not be reflected in JavaScript until the page is next - * (re)loaded. See {@link #addJavascriptInterface}. - * - * @param name the name used to expose the object in JavaScript - */ - public void removeJavascriptInterface(String name) { - checkThread(); - mProvider.removeJavascriptInterface(name); - } - - /** - * Creates a message channel to communicate with JS and returns the message - * ports that represent the endpoints of this message channel. The HTML5 message - * channel functionality is described - * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here - * </a> - * - * <p>The returned message channels are entangled and already in started state. - * - * @return the two message ports that form the message channel. - */ - public WebMessagePort[] createWebMessageChannel() { - checkThread(); - return mProvider.createWebMessageChannel(); - } - - /** - * Post a message to main frame. The embedded application can restrict the - * messages to a certain target origin. See - * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages"> - * HTML5 spec</a> for how target origin can be used. - * <p> - * A target origin can be set as a wildcard ("*"). However this is not recommended. - * See the page above for security issues. - * - * @param message the WebMessage - * @param targetOrigin the target origin. - */ - public void postWebMessage(WebMessage message, Uri targetOrigin) { - checkThread(); - mProvider.postMessageToMainFrame(message, targetOrigin); - } - - /** - * Gets the WebSettings object used to control the settings for this - * WebView. - * - * @return a WebSettings object that can be used to control this WebView's - * settings - */ - public WebSettings getSettings() { - checkThread(); - return mProvider.getSettings(); - } - - /** - * Enables debugging of web contents (HTML / CSS / JavaScript) - * loaded into any WebViews of this application. This flag can be enabled - * in order to facilitate debugging of web layouts and JavaScript - * code running inside WebViews. Please refer to WebView documentation - * for the debugging guide. - * - * The default is {@code false}. - * - * @param enabled whether to enable web contents debugging - */ - public static void setWebContentsDebuggingEnabled(boolean enabled) { - getFactory().getStatics().setWebContentsDebuggingEnabled(enabled); - } - - /** - * Gets the list of currently loaded plugins. - * - * @return the list of currently loaded plugins - * @deprecated This was used for Gears, which has been deprecated. - * @hide - */ - @Deprecated - public static synchronized PluginList getPluginList() { - return new PluginList(); - } - - /** - * @deprecated This was used for Gears, which has been deprecated. - * @hide - */ - @Deprecated - public void refreshPlugins(boolean reloadOpenPages) { - checkThread(); - } - - /** - * Puts this WebView into text selection mode. Do not rely on this - * functionality; it will be deprecated in the future. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public void emulateShiftHeld() { - checkThread(); - } - - /** - * @deprecated WebView no longer needs to implement - * ViewGroup.OnHierarchyChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onChildViewAdded(View parent, View child) {} - - /** - * @deprecated WebView no longer needs to implement - * ViewGroup.OnHierarchyChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onChildViewRemoved(View p, View child) {} - - /** - * @deprecated WebView should not have implemented - * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - } - - /** - * @deprecated Only the default case, {@code true}, will be supported in a future version. - */ - @Deprecated - public void setMapTrackballToArrowKeys(boolean setMap) { - checkThread(); - mProvider.setMapTrackballToArrowKeys(setMap); + public void addJavascriptInterface(Object obj, String interfaceName) { } - - public void flingScroll(int vx, int vy) { - checkThread(); - mProvider.flingScroll(vx, vy); - } - - /** - * Gets the zoom controls for this WebView, as a separate View. The caller - * is responsible for inserting this View into the layout hierarchy. - * <p/> - * API level {@link android.os.Build.VERSION_CODES#CUPCAKE} introduced - * built-in zoom mechanisms for the WebView, as opposed to these separate - * zoom controls. The built-in mechanisms are preferred and can be enabled - * using {@link WebSettings#setBuiltInZoomControls}. - * - * @deprecated the built-in zoom mechanisms are preferred - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} - */ - @Deprecated public View getZoomControls() { - checkThread(); - return mProvider.getZoomControls(); + return null; } - /** - * Gets whether this WebView can be zoomed in. - * - * @return {@code true} if this WebView can be zoomed in - * - * @deprecated This method is prone to inaccuracy due to race conditions - * between the web rendering and UI threads; prefer - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - public boolean canZoomIn() { - checkThread(); - return mProvider.canZoomIn(); - } - - /** - * Gets whether this WebView can be zoomed out. - * - * @return {@code true} if this WebView can be zoomed out - * - * @deprecated This method is prone to inaccuracy due to race conditions - * between the web rendering and UI threads; prefer - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - public boolean canZoomOut() { - checkThread(); - return mProvider.canZoomOut(); - } - - /** - * Performs a zoom operation in this WebView. - * - * @param zoomFactor the zoom factor to apply. The zoom factor will be clamped to the WebView's - * zoom limits. This value must be in the range 0.01 to 100.0 inclusive. - */ - public void zoomBy(float zoomFactor) { - checkThread(); - if (zoomFactor < 0.01) - throw new IllegalArgumentException("zoomFactor must be greater than 0.01."); - if (zoomFactor > 100.0) - throw new IllegalArgumentException("zoomFactor must be less than 100."); - mProvider.zoomBy(zoomFactor); - } - - /** - * Performs zoom in in this WebView. - * - * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes - */ public boolean zoomIn() { - checkThread(); - return mProvider.zoomIn(); + return false; } - /** - * Performs zoom out in this WebView. - * - * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes - */ public boolean zoomOut() { - checkThread(); - return mProvider.zoomOut(); - } - - /** - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public void debugDump() { - checkThread(); - } - - /** - * See {@link ViewDebug.HierarchyHandler#dumpViewHierarchyWithProperties(BufferedWriter, int)} - * @hide - */ - @Override - public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) { - mProvider.dumpViewHierarchyWithProperties(out, level); - } - - /** - * See {@link ViewDebug.HierarchyHandler#findHierarchyView(String, int)} - * @hide - */ - @Override - public View findHierarchyView(String className, int hashCode) { - return mProvider.findHierarchyView(className, hashCode); - } - - /** @hide */ - @IntDef({ - RENDERER_PRIORITY_WAIVED, - RENDERER_PRIORITY_BOUND, - RENDERER_PRIORITY_IMPORTANT - }) - @Retention(RetentionPolicy.SOURCE) - public @interface RendererPriority {} - - /** - * The renderer associated with this WebView is bound with - * {@link Context#BIND_WAIVE_PRIORITY}. At this priority level - * {@link WebView} renderers will be strong targets for out of memory - * killing. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_WAIVED = 0; - /** - * The renderer associated with this WebView is bound with - * the default priority for services. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_BOUND = 1; - /** - * The renderer associated with this WebView is bound with - * {@link Context#BIND_IMPORTANT}. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_IMPORTANT = 2; - - /** - * Set the renderer priority policy for this {@link WebView}. The - * priority policy will be used to determine whether an out of - * process renderer should be considered to be a target for OOM - * killing. - * - * Because a renderer can be associated with more than one - * WebView, the final priority it is computed as the maximum of - * any attached WebViews. When a WebView is destroyed it will - * cease to be considerered when calculating the renderer - * priority. Once no WebViews remain associated with the renderer, - * the priority of the renderer will be reduced to - * {@link #RENDERER_PRIORITY_WAIVED}. - * - * The default policy is to set the priority to - * {@link #RENDERER_PRIORITY_IMPORTANT} regardless of visibility, - * and this should not be changed unless the caller also handles - * renderer crashes with - * {@link WebViewClient#onRenderProcessGone}. Any other setting - * will result in WebView renderers being killed by the system - * more aggressively than the application. - * - * @param rendererRequestedPriority the minimum priority at which - * this WebView desires the renderer process to be bound. - * @param waivedWhenNotVisible if {@code true}, this flag specifies that - * when this WebView is not visible, it will be treated as - * if it had requested a priority of - * {@link #RENDERER_PRIORITY_WAIVED}. - */ - public void setRendererPriorityPolicy( - @RendererPriority int rendererRequestedPriority, - boolean waivedWhenNotVisible) { - mProvider.setRendererPriorityPolicy(rendererRequestedPriority, waivedWhenNotVisible); - } - - /** - * Get the requested renderer priority for this WebView. - * - * @return the requested renderer priority policy. - */ - @RendererPriority - public int getRendererRequestedPriority() { - return mProvider.getRendererRequestedPriority(); - } - - /** - * Return whether this WebView requests a priority of - * {@link #RENDERER_PRIORITY_WAIVED} when not visible. - * - * @return whether this WebView requests a priority of - * {@link #RENDERER_PRIORITY_WAIVED} when not visible. - */ - public boolean getRendererPriorityWaivedWhenNotVisible() { - return mProvider.getRendererPriorityWaivedWhenNotVisible(); - } - - /** - * Sets the {@link TextClassifier} for this WebView. - */ - public void setTextClassifier(@Nullable TextClassifier textClassifier) { - mProvider.setTextClassifier(textClassifier); - } - - /** - * Returns the {@link TextClassifier} used by this WebView. - * If no TextClassifier has been set, this WebView uses the default set by the system. - */ - @NonNull - public TextClassifier getTextClassifier() { - return mProvider.getTextClassifier(); - } - - //------------------------------------------------------------------------- - // Interface for WebView providers - //------------------------------------------------------------------------- - - /** - * Gets the WebViewProvider. Used by providers to obtain the underlying - * implementation, e.g. when the application responds to - * WebViewClient.onCreateWindow() request. - * - * @hide WebViewProvider is not public API. - */ - @SystemApi - public WebViewProvider getWebViewProvider() { - return mProvider; - } - - /** - * Callback interface, allows the provider implementation to access non-public methods - * and fields, and make super-class calls in this WebView instance. - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public class PrivateAccess { - // ---- Access to super-class methods ---- - public int super_getScrollBarStyle() { - return WebView.super.getScrollBarStyle(); - } - - public void super_scrollTo(int scrollX, int scrollY) { - WebView.super.scrollTo(scrollX, scrollY); - } - - public void super_computeScroll() { - WebView.super.computeScroll(); - } - - public boolean super_onHoverEvent(MotionEvent event) { - return WebView.super.onHoverEvent(event); - } - - public boolean super_performAccessibilityAction(int action, Bundle arguments) { - return WebView.super.performAccessibilityActionInternal(action, arguments); - } - - public boolean super_performLongClick() { - return WebView.super.performLongClick(); - } - - public boolean super_setFrame(int left, int top, int right, int bottom) { - return WebView.super.setFrame(left, top, right, bottom); - } - - public boolean super_dispatchKeyEvent(KeyEvent event) { - return WebView.super.dispatchKeyEvent(event); - } - - public boolean super_onGenericMotionEvent(MotionEvent event) { - return WebView.super.onGenericMotionEvent(event); - } - - public boolean super_requestFocus(int direction, Rect previouslyFocusedRect) { - return WebView.super.requestFocus(direction, previouslyFocusedRect); - } - - public void super_setLayoutParams(ViewGroup.LayoutParams params) { - WebView.super.setLayoutParams(params); - } - - public void super_startActivityForResult(Intent intent, int requestCode) { - WebView.super.startActivityForResult(intent, requestCode); - } - - // ---- Access to non-public methods ---- - public void overScrollBy(int deltaX, int deltaY, - int scrollX, int scrollY, - int scrollRangeX, int scrollRangeY, - int maxOverScrollX, int maxOverScrollY, - boolean isTouchEvent) { - WebView.this.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, - maxOverScrollX, maxOverScrollY, isTouchEvent); - } - - public void awakenScrollBars(int duration) { - WebView.this.awakenScrollBars(duration); - } - - public void awakenScrollBars(int duration, boolean invalidate) { - WebView.this.awakenScrollBars(duration, invalidate); - } - - public float getVerticalScrollFactor() { - return WebView.this.getVerticalScrollFactor(); - } - - public float getHorizontalScrollFactor() { - return WebView.this.getHorizontalScrollFactor(); - } - - public void setMeasuredDimension(int measuredWidth, int measuredHeight) { - WebView.this.setMeasuredDimension(measuredWidth, measuredHeight); - } - - public void onScrollChanged(int l, int t, int oldl, int oldt) { - WebView.this.onScrollChanged(l, t, oldl, oldt); - } - - public int getHorizontalScrollbarHeight() { - return WebView.this.getHorizontalScrollbarHeight(); - } - - public void super_onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, - int l, int t, int r, int b) { - WebView.super.onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); - } - - // ---- Access to (non-public) fields ---- - /** Raw setter for the scroll X value, without invoking onScrollChanged handlers etc. */ - public void setScrollXRaw(int scrollX) { - WebView.this.mScrollX = scrollX; - } - - /** Raw setter for the scroll Y value, without invoking onScrollChanged handlers etc. */ - public void setScrollYRaw(int scrollY) { - WebView.this.mScrollY = scrollY; - } - - } - - //------------------------------------------------------------------------- - // Package-private internal stuff - //------------------------------------------------------------------------- - - // Only used by android.webkit.FindActionModeCallback. - void setFindDialogFindListener(FindListener listener) { - checkThread(); - setupFindListenerIfNeeded(); - mFindListener.mFindDialogFindListener = listener; - } - - // Only used by android.webkit.FindActionModeCallback. - void notifyFindDialogDismissed() { - checkThread(); - mProvider.notifyFindDialogDismissed(); - } - - //------------------------------------------------------------------------- - // Private internal stuff - //------------------------------------------------------------------------- - - private WebViewProvider mProvider; - - /** - * In addition to the FindListener that the user may set via the WebView.setFindListener - * API, FindActionModeCallback will register it's own FindListener. We keep them separate - * via this class so that the two FindListeners can potentially exist at once. - */ - private class FindListenerDistributor implements FindListener { - private FindListener mFindDialogFindListener; - private FindListener mUserFindListener; - - @Override - public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, - boolean isDoneCounting) { - if (mFindDialogFindListener != null) { - mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, - isDoneCounting); - } - - if (mUserFindListener != null) { - mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, - isDoneCounting); - } - } - } - private FindListenerDistributor mFindListener; - - private void setupFindListenerIfNeeded() { - if (mFindListener == null) { - mFindListener = new FindListenerDistributor(); - mProvider.setFindListener(mFindListener); - } - } - - private void ensureProviderCreated() { - checkThread(); - if (mProvider == null) { - // As this can get called during the base class constructor chain, pass the minimum - // number of dependencies here; the rest are deferred to init(). - mProvider = getFactory().createWebView(this, new PrivateAccess()); - } - } - - private static WebViewFactoryProvider getFactory() { - return WebViewFactory.getProvider(); - } - - private final Looper mWebViewThread = Looper.myLooper(); - - private void checkThread() { - // Ignore mWebViewThread == null because this can be called during in the super class - // constructor, before this class's own constructor has even started. - if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) { - Throwable throwable = new Throwable( - "A WebView method was called on thread '" + - Thread.currentThread().getName() + "'. " + - "All WebView methods must be called on the same thread. " + - "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() + - ", FYI main Looper is " + Looper.getMainLooper() + ")"); - Log.w(LOGTAG, Log.getStackTraceString(throwable)); - StrictMode.onWebViewMethodCalledOnWrongThread(throwable); - - if (sEnforceThreadChecking) { - throw new RuntimeException(throwable); - } - } - } - - //------------------------------------------------------------------------- - // Override View methods - //------------------------------------------------------------------------- - - // TODO: Add a test that enumerates all methods in ViewDelegte & ScrollDelegate, and ensures - // there's a corresponding override (or better, caller) for each of them in here. - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mProvider.getViewDelegate().onAttachedToWindow(); - } - - /** @hide */ - @Override - protected void onDetachedFromWindowInternal() { - mProvider.getViewDelegate().onDetachedFromWindow(); - super.onDetachedFromWindowInternal(); - } - - /** @hide */ - @Override - public void onMovedToDisplay(int displayId, Configuration config) { - mProvider.getViewDelegate().onMovedToDisplay(displayId, config); - } - - @Override - public void setLayoutParams(ViewGroup.LayoutParams params) { - mProvider.getViewDelegate().setLayoutParams(params); - } - - @Override - public void setOverScrollMode(int mode) { - super.setOverScrollMode(mode); - // This method may be called in the constructor chain, before the WebView provider is - // created. - ensureProviderCreated(); - mProvider.getViewDelegate().setOverScrollMode(mode); - } - - @Override - public void setScrollBarStyle(int style) { - mProvider.getViewDelegate().setScrollBarStyle(style); - super.setScrollBarStyle(style); - } - - @Override - protected int computeHorizontalScrollRange() { - return mProvider.getScrollDelegate().computeHorizontalScrollRange(); - } - - @Override - protected int computeHorizontalScrollOffset() { - return mProvider.getScrollDelegate().computeHorizontalScrollOffset(); - } - - @Override - protected int computeVerticalScrollRange() { - return mProvider.getScrollDelegate().computeVerticalScrollRange(); - } - - @Override - protected int computeVerticalScrollOffset() { - return mProvider.getScrollDelegate().computeVerticalScrollOffset(); - } - - @Override - protected int computeVerticalScrollExtent() { - return mProvider.getScrollDelegate().computeVerticalScrollExtent(); - } - - @Override - public void computeScroll() { - mProvider.getScrollDelegate().computeScroll(); - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - return mProvider.getViewDelegate().onHoverEvent(event); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return mProvider.getViewDelegate().onTouchEvent(event); - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) { - return mProvider.getViewDelegate().onGenericMotionEvent(event); - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - return mProvider.getViewDelegate().onTrackballEvent(event); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyUp(keyCode, event); - } - - @Override - public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { - return mProvider.getViewDelegate().onKeyMultiple(keyCode, repeatCount, event); - } - - /* - TODO: These are not currently implemented in WebViewClassic, but it seems inconsistent not - to be delegating them too. - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyPreIme(keyCode, event); - } - @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyLongPress(keyCode, event); - } - @Override - public boolean onKeyShortcut(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyShortcut(keyCode, event); - } - */ - - @Override - public AccessibilityNodeProvider getAccessibilityNodeProvider() { - AccessibilityNodeProvider provider = - mProvider.getViewDelegate().getAccessibilityNodeProvider(); - return provider == null ? super.getAccessibilityNodeProvider() : provider; - } - - @Deprecated - @Override - public boolean shouldDelayChildPressedState() { - return mProvider.getViewDelegate().shouldDelayChildPressedState(); - } - - @Override - public CharSequence getAccessibilityClassName() { - return WebView.class.getName(); - } - - @Override - public void onProvideVirtualStructure(ViewStructure structure) { - mProvider.getViewDelegate().onProvideVirtualStructure(structure); - } - - /** - * {@inheritDoc} - * - * <p>The {@link ViewStructure} traditionally represents a {@link View}, while for web pages - * it represent HTML nodes. Hence, it's necessary to "map" the HTML properties in a way that is - * understood by the {@link android.service.autofill.AutofillService} implementations: - * - * <ol> - * <li>Only the HTML nodes inside a {@code FORM} are generated. - * <li>The source of the HTML is set using {@link ViewStructure#setWebDomain(String)} in the - * node representing the WebView. - * <li>If a web page has multiple {@code FORM}s, only the data for the current form is - * represented—if the user taps a field from another form, then the current autofill - * context is canceled (by calling {@link android.view.autofill.AutofillManager#cancel()} and - * a new context is created for that {@code FORM}. - * <li>Similarly, if the page has {@code IFRAME} nodes, they are not initially represented in - * the view structure until the user taps a field from a {@code FORM} inside the - * {@code IFRAME}, in which case it would be treated the same way as multiple forms described - * above, except that the {@link ViewStructure#setWebDomain(String) web domain} of the - * {@code FORM} contains the {@code src} attribute from the {@code IFRAME} node. - * <li>If the Android SDK provides a similar View, then should be set with the - * fully-qualified class name of such view. - * <li>The W3C autofill field ({@code autocomplete} tag attribute) maps to - * {@link ViewStructure#setAutofillHints(String[])}. - * <li>The {@code type} attribute of {@code INPUT} tags maps to - * {@link ViewStructure#setInputType(int)}. - * <li>The {@code value} attribute of {@code INPUT} tags maps to - * {@link ViewStructure#setText(CharSequence)}. - * <li>If the view is editalbe, the {@link ViewStructure#setAutofillType(int)} and - * {@link ViewStructure#setAutofillValue(AutofillValue)} must be set. - * <li>The {@code placeholder} attribute maps to {@link ViewStructure#setHint(CharSequence)}. - * <li>Other HTML attributes can be represented through - * {@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}. - * </ol> - * - * <p>It should also call {@code structure.setDataIsSensitive(false)} for fields whose value - * were not dynamically changed (for example, through Javascript). - * - * <p>Example1: an HTML form with 2 fields for username and password. - * - * <pre class="prettyprint"> - * <input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"> - * <input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"> - * </pre> - * - * <p>Would map to: - * - * <pre class="prettyprint"> - * int index = structure.addChildCount(2); - * ViewStructure username = structure.newChild(index); - * username.setAutofillId(structure.getAutofillId(), 1); // id 1 - first child - * username.setClassName("input"); - * username.setInputType("android.widget.EditText"); - * username.setAutofillHints("username"); - * username.setHtmlInfo(username.newHtmlInfoBuilder("input") - * .addAttribute("type", "text") - * .addAttribute("name", "username") - * .addAttribute("id", "user") - * .build()); - * username.setHint("Email or username"); - * username.setAutofillType(View.AUTOFILL_TYPE_TEXT); - * username.setAutofillValue(AutofillValue.forText("Type your username")); - * username.setText("Type your username"); - * // Value of the field is not sensitive because it was not dynamically changed: - * username.setDataIsSensitive(false); - * - * ViewStructure password = structure.newChild(index + 1); - * username.setAutofillId(structure, 2); // id 2 - second child - * password.setInputType("android.widget.EditText"); - * password.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); - * password.setAutofillHints("current-password"); - * password.setHtmlInfo(password.newHtmlInfoBuilder("input") - * .addAttribute("type", "password") - * .addAttribute("name", "password") - * .addAttribute("id", "pass") - * .build()); - * password.setHint("Password"); - * password.setAutofillType(View.AUTOFILL_TYPE_TEXT); - * </pre> - * - * <p>Example2: an IFRAME tag. - * - * <pre class="prettyprint"> - * <iframe src="https://example.com/login"/> - * </pre> - * - * <p>Would map to: - * - * <pre class="prettyprint"> - * int index = structure.addChildCount(1); - * ViewStructure iframe = structure.newChildFor(index); - * iframe.setAutofillId(structure.getAutofillId(), 1); - * iframe.setHtmlInfo(iframe.newHtmlInfoBuilder("iframe") - * .addAttribute("src", "https://example.com/login") - * .build()); - * </pre> - */ - @Override - public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { - mProvider.getViewDelegate().onProvideAutofillVirtualStructure(structure, flags); - } - - @Override - public void autofill(SparseArray<AutofillValue>values) { - mProvider.getViewDelegate().autofill(values); - } - - /** @hide */ - @Override - public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfoInternal(info); - mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info); - } - - /** @hide */ - @Override - public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { - super.onInitializeAccessibilityEventInternal(event); - mProvider.getViewDelegate().onInitializeAccessibilityEvent(event); - } - - /** @hide */ - @Override - public boolean performAccessibilityActionInternal(int action, Bundle arguments) { - return mProvider.getViewDelegate().performAccessibilityAction(action, arguments); - } - - /** @hide */ - @Override - protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, - int l, int t, int r, int b) { - mProvider.getViewDelegate().onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); - } - - @Override - protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { - mProvider.getViewDelegate().onOverScrolled(scrollX, scrollY, clampedX, clampedY); - } - - @Override - protected void onWindowVisibilityChanged(int visibility) { - super.onWindowVisibilityChanged(visibility); - mProvider.getViewDelegate().onWindowVisibilityChanged(visibility); - } - - @Override - protected void onDraw(Canvas canvas) { - mProvider.getViewDelegate().onDraw(canvas); - } - - @Override - public boolean performLongClick() { - return mProvider.getViewDelegate().performLongClick(); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - mProvider.getViewDelegate().onConfigurationChanged(newConfig); - } - - /** - * Creates a new InputConnection for an InputMethod to interact with the WebView. - * This is similar to {@link View#onCreateInputConnection} but note that WebView - * calls InputConnection methods on a thread other than the UI thread. - * If these methods are overridden, then the overriding methods should respect - * thread restrictions when calling View methods or accessing data. - */ - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - return mProvider.getViewDelegate().onCreateInputConnection(outAttrs); - } - - @Override - public boolean onDragEvent(DragEvent event) { - return mProvider.getViewDelegate().onDragEvent(event); - } - - @Override - protected void onVisibilityChanged(View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - // This method may be called in the constructor chain, before the WebView provider is - // created. - ensureProviderCreated(); - mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility); - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - mProvider.getViewDelegate().onWindowFocusChanged(hasWindowFocus); - super.onWindowFocusChanged(hasWindowFocus); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - mProvider.getViewDelegate().onFocusChanged(focused, direction, previouslyFocusedRect); - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - /** @hide */ - @Override - protected boolean setFrame(int left, int top, int right, int bottom) { - return mProvider.getViewDelegate().setFrame(left, top, right, bottom); - } - - @Override - protected void onSizeChanged(int w, int h, int ow, int oh) { - super.onSizeChanged(w, h, ow, oh); - mProvider.getViewDelegate().onSizeChanged(w, h, ow, oh); - } - - @Override - protected void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - mProvider.getViewDelegate().onScrollChanged(l, t, oldl, oldt); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return mProvider.getViewDelegate().dispatchKeyEvent(event); - } - - @Override - public boolean requestFocus(int direction, Rect previouslyFocusedRect) { - return mProvider.getViewDelegate().requestFocus(direction, previouslyFocusedRect); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mProvider.getViewDelegate().onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { - return mProvider.getViewDelegate().requestChildRectangleOnScreen(child, rect, immediate); - } - - @Override - public void setBackgroundColor(int color) { - mProvider.getViewDelegate().setBackgroundColor(color); - } - - @Override - public void setLayerType(int layerType, Paint paint) { - super.setLayerType(layerType, paint); - mProvider.getViewDelegate().setLayerType(layerType, paint); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - mProvider.getViewDelegate().preDispatchDraw(canvas); - super.dispatchDraw(canvas); - } - - @Override - public void onStartTemporaryDetach() { - super.onStartTemporaryDetach(); - mProvider.getViewDelegate().onStartTemporaryDetach(); - } - - @Override - public void onFinishTemporaryDetach() { - super.onFinishTemporaryDetach(); - mProvider.getViewDelegate().onFinishTemporaryDetach(); - } - - @Override - public Handler getHandler() { - return mProvider.getViewDelegate().getHandler(super.getHandler()); - } - - @Override - public View findFocus() { - return mProvider.getViewDelegate().findFocus(super.findFocus()); - } - - /** - * If WebView has already been loaded into the current process this method will return the - * package that was used to load it. Otherwise, the package that would be used if the WebView - * was loaded right now will be returned; this does not cause WebView to be loaded, so this - * information may become outdated at any time. - * The WebView package changes either when the current WebView package is updated, disabled, or - * uninstalled. It can also be changed through a Developer Setting. - * If the WebView package changes, any app process that has loaded WebView will be killed. The - * next time the app starts and loads WebView it will use the new WebView package instead. - * @return the current WebView package, or {@code null} if there is none. - */ - public static PackageInfo getCurrentWebViewPackage() { - PackageInfo webviewPackage = WebViewFactory.getLoadedPackageInfo(); - if (webviewPackage != null) { - return webviewPackage; - } - - try { - return WebViewFactory.getUpdateService().getCurrentWebViewPackage(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * Receive the result from a previous call to {@link #startActivityForResult(Intent, int)}. - * - * @param requestCode The integer request code originally supplied to - * startActivityForResult(), allowing you to identify who this - * result came from. - * @param resultCode The integer result code returned by the child activity - * through its setResult(). - * @param data An Intent, which can return result data to the caller - * (various data can be attached to Intent "extras"). - * @hide - */ - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - mProvider.getViewDelegate().onActivityResult(requestCode, resultCode, data); - } - - /** @hide */ - @Override - protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { - super.encodeProperties(encoder); - - checkThread(); - encoder.addProperty("webview:contentHeight", mProvider.getContentHeight()); - encoder.addProperty("webview:contentWidth", mProvider.getContentWidth()); - encoder.addProperty("webview:scale", mProvider.getScale()); - encoder.addProperty("webview:title", mProvider.getTitle()); - encoder.addProperty("webview:url", mProvider.getUrl()); - encoder.addProperty("webview:originalUrl", mProvider.getOriginalUrl()); + return false; } } diff --git a/android/widget/DatePickerCalendarDelegate.java b/android/widget/DatePickerCalendarDelegate.java index 60b47572..e40023d0 100644 --- a/android/widget/DatePickerCalendarDelegate.java +++ b/android/widget/DatePickerCalendarDelegate.java @@ -22,11 +22,10 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.icu.text.DateFormat; import android.icu.text.DisplayContext; -import android.icu.text.SimpleDateFormat; import android.icu.util.Calendar; import android.os.Parcelable; -import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.StateSet; import android.view.HapticFeedbackConstants; @@ -62,8 +61,8 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { private static final int[] ATTRS_DISABLED_ALPHA = new int[] { com.android.internal.R.attr.disabledAlpha}; - private SimpleDateFormat mYearFormat; - private SimpleDateFormat mMonthDayFormat; + private DateFormat mYearFormat; + private DateFormat mMonthDayFormat; // Top-level container. private ViewGroup mContainer; @@ -273,19 +272,16 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { /** * Listener called when the user clicks on a header item. */ - private final OnClickListener mOnHeaderClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - tryVibrate(); + private final OnClickListener mOnHeaderClickListener = v -> { + tryVibrate(); - switch (v.getId()) { - case R.id.date_picker_header_year: - setCurrentView(VIEW_YEAR); - break; - case R.id.date_picker_header_date: - setCurrentView(VIEW_MONTH_DAY); - break; - } + switch (v.getId()) { + case R.id.date_picker_header_year: + setCurrentView(VIEW_YEAR); + break; + case R.id.date_picker_header_date: + setCurrentView(VIEW_MONTH_DAY); + break; } }; @@ -299,10 +295,9 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { } // Update the date formatter. - final String datePattern = DateFormat.getBestDateTimePattern(locale, "EMMMd"); - mMonthDayFormat = new SimpleDateFormat(datePattern, locale); + mMonthDayFormat = DateFormat.getInstanceForSkeleton("EMMMd", locale); mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE); - mYearFormat = new SimpleDateFormat("y", locale); + mYearFormat = DateFormat.getInstanceForSkeleton("y", locale); // Update the header text. onCurrentDateChanged(false); @@ -344,14 +339,11 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { case VIEW_YEAR: final int year = mCurrentDate.get(Calendar.YEAR); mYearPickerView.setYear(year); - mYearPickerView.post(new Runnable() { - @Override - public void run() { - mYearPickerView.requestFocus(); - final View selected = mYearPickerView.getSelectedView(); - if (selected != null) { - selected.requestFocus(); - } + mYearPickerView.post(() -> { + mYearPickerView.requestFocus(); + final View selected = mYearPickerView.getSelectedView(); + if (selected != null) { + selected.requestFocus(); } }); diff --git a/android/widget/Editor.java b/android/widget/Editor.java index d23dfe46..0f617242 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -41,6 +41,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.metrics.LogMaker; import android.os.Bundle; import android.os.LocaleList; import android.os.Parcel; @@ -1585,49 +1586,49 @@ public class Editor { outText.startOffset = 0; outText.selectionStart = mTextView.getSelectionStart(); outText.selectionEnd = mTextView.getSelectionEnd(); + outText.hint = mTextView.getHint(); return true; } boolean reportExtractedText() { final Editor.InputMethodState ims = mInputMethodState; - if (ims != null) { - final boolean contentChanged = ims.mContentChanged; - if (contentChanged || ims.mSelectionModeChanged) { - ims.mContentChanged = false; - ims.mSelectionModeChanged = false; - final ExtractedTextRequest req = ims.mExtractedTextRequest; - if (req != null) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - if (TextView.DEBUG_EXTRACT) { - Log.v(TextView.LOG_TAG, "Retrieving extracted start=" - + ims.mChangedStart - + " end=" + ims.mChangedEnd - + " delta=" + ims.mChangedDelta); - } - if (ims.mChangedStart < 0 && !contentChanged) { - ims.mChangedStart = EXTRACT_NOTHING; - } - if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, - ims.mChangedDelta, ims.mExtractedText)) { - if (TextView.DEBUG_EXTRACT) { - Log.v(TextView.LOG_TAG, - "Reporting extracted start=" - + ims.mExtractedText.partialStartOffset - + " end=" + ims.mExtractedText.partialEndOffset - + ": " + ims.mExtractedText.text); - } - - imm.updateExtractedText(mTextView, req.token, ims.mExtractedText); - ims.mChangedStart = EXTRACT_UNKNOWN; - ims.mChangedEnd = EXTRACT_UNKNOWN; - ims.mChangedDelta = 0; - ims.mContentChanged = false; - return true; - } - } - } - } + if (ims == null) { + return false; + } + ims.mSelectionModeChanged = false; + final ExtractedTextRequest req = ims.mExtractedTextRequest; + if (req == null) { + return false; + } + final InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm == null) { + return false; + } + if (TextView.DEBUG_EXTRACT) { + Log.v(TextView.LOG_TAG, "Retrieving extracted start=" + + ims.mChangedStart + + " end=" + ims.mChangedEnd + + " delta=" + ims.mChangedDelta); + } + if (ims.mChangedStart < 0 && !ims.mContentChanged) { + ims.mChangedStart = EXTRACT_NOTHING; + } + if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, + ims.mChangedDelta, ims.mExtractedText)) { + if (TextView.DEBUG_EXTRACT) { + Log.v(TextView.LOG_TAG, + "Reporting extracted start=" + + ims.mExtractedText.partialStartOffset + + " end=" + ims.mExtractedText.partialEndOffset + + ": " + ims.mExtractedText.text); + } + + imm.updateExtractedText(mTextView, req.token, ims.mExtractedText); + ims.mChangedStart = EXTRACT_UNKNOWN; + ims.mChangedEnd = EXTRACT_UNKNOWN; + ims.mChangedDelta = 0; + ims.mContentChanged = false; + return true; } return false; } @@ -3932,6 +3933,10 @@ public class Editor { textClassification.getLabel()) .setIcon(textClassification.getIcon()) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + mMetricsLogger.write( + new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST) + .setType(MetricsEvent.TYPE_OPEN) + .setSubtype(textClassification.getLogType())); } } @@ -3973,6 +3978,9 @@ public class Editor { .onClick(mTextView); } } + mMetricsLogger.action( + MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST, + textClassification.getLogType()); stopTextActionMode(); return true; } diff --git a/android/widget/TextView.java b/android/widget/TextView.java index 4b6c4d35..efcc3a2f 100644 --- a/android/widget/TextView.java +++ b/android/widget/TextView.java @@ -5547,6 +5547,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @android.view.RemotableViewMethod public final void setHint(CharSequence hint) { + setHintInternal(hint); + + if (isInputMethodTarget()) { + mEditor.reportExtractedText(); + } + } + + private void setHintInternal(CharSequence hint) { mHint = TextUtils.stringOrSpannedString(hint); if (mLayout != null) { @@ -7644,6 +7652,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { MetaKeyKeyListener.stopSelecting(this, sp); } + + setHintInternal(text.hint); } /** @@ -8433,7 +8443,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mMaxMode != LINES) { desired = Math.min(desired, mMaximum); - } else if (cap && linecount > mMaximum && layout instanceof DynamicLayout) { + } else if (cap && linecount > mMaximum && (layout instanceof DynamicLayout + || layout instanceof BoringLayout)) { desired = layout.getLineTop(mMaximum); if (dr != null) { |