/*
* Copyright 2015. Appsi Mobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.appsimobile.appsii.appwidget;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.os.TransactionTooLargeException;
import android.support.annotation.NonNull;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.WeakHashMap;
/**
* An extended host, that allows registering listeners for starting and stopping
* the widgets. It also returns a special host-view needed to correctly dispatch
* the events when using a recycler-view that wants to scroll the app-widgets,
* before responding to the scroll by itself.
* <p/>
* The startListening implementation is cherry-picked from the AOSP launcher3 app.
*/
public class AppsiiAppWidgetHost extends AppWidgetHost {
final WeakHashMap<HostStatusListener, Void> mListeners = new WeakHashMap<>();
private final ArrayList<Runnable> mProviderChangeListeners = new ArrayList<Runnable>();
boolean mStartedListening;
public AppsiiAppWidgetHost(Context context, int hostId) {
super(context, hostId);
}
@Override
public void startListening() {
try {
super.startListening();
mStartedListening = true;
notifyStartListening();
} catch (Exception e) {
if (e.getCause() instanceof TransactionTooLargeException) {
// We're willing to let this slide. The exception is being caused by the list of
// RemoteViews which is being passed back. The startListening relationship will
// have been established by this point, and we will end up populating the
// widgets upon bind anyway. See issue 14255011 for more context.
} else {
throw new RuntimeException(e);
}
}
}
private void notifyStartListening() {
for (HostStatusListener l : mListeners.keySet()) {
if (l != null) {
l.onStartedListening();
}
}
}
@Override
public void stopListening() {
super.stopListening();
mStartedListening = false;
notifyStopListening();
clearViews();
notifyViewsCleared();
}
private void notifyStopListening() {
for (HostStatusListener l : mListeners.keySet()) {
if (l != null) {
l.onStoppedListening();
}
}
}
private void notifyViewsCleared() {
for (HostStatusListener l : mListeners.keySet()) {
if (l != null) {
l.onViewsCleared();
}
}
}
@Override
protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
AppWidgetProviderInfo appWidget) {
return new AppsiAppWidgetHostView(context);
}
protected void onProvidersChanged() {
// Once we get the message that widget packages are updated, we need to rebind items
// in AppsCustomize accordingly.
// mLauncher.bindPackagesUpdated(LauncherModel.getSortedWidgetsAndShortcuts(mLauncher));
for (Runnable callback : mProviderChangeListeners) {
callback.run();
}
}
public boolean isListening() {
return mStartedListening;
}
public void addProviderChangeListener(Runnable callback) {
mProviderChangeListeners.add(callback);
}
public void removeProviderChangeListener(Runnable callback) {
mProviderChangeListeners.remove(callback);
}
public void registerHostStatusListener(HostStatusListener l) {
mListeners.put(l, null);
}
public void unregisterHostStatusListener(HostStatusListener l) {
mListeners.remove(l);
}
public interface HostStatusListener {
void onStartedListening();
void onStoppedListening();
void onViewsCleared();
}
public interface CapturedEventQueue {
boolean dispatchTouchEvent(MotionEvent e);
void release();
}
/**
* A very special implementation of the AppWidgetHostView that allows Appsii
* to re-route it's motion-event input. Because the recycler-view somehow
* posts the events to the children even if it's been captured I needed some
* way to post modified events to the view, and have it ignore the normal
* events dispatched by the system.
*/
public static class AppsiAppWidgetHostView extends AppWidgetHostView {
boolean mEventTouchQueueCaptured;
AppWidgetTouchEventQueue mAppWidgetTouchEventQueue;
public AppsiAppWidgetHostView(Context context) {
super(context);
}
/**
* This is called when a component (HomeController) wants to override the
* event queue. This will make the view ignore normal touch events and
* respond only to our own stream of events.
*/
public CapturedEventQueue captureEventQueue() {
mEventTouchQueueCaptured = true;
if (mAppWidgetTouchEventQueue == null) {
mAppWidgetTouchEventQueue = new AppWidgetTouchEventQueue();
}
return mAppWidgetTouchEventQueue;
}
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
// When the event queue is captured, return false and
// do not dispatch
return !mEventTouchQueueCaptured && super.dispatchTouchEvent(ev);
}
/**
* Used to dispatch events when the queue has been captured.
*/
boolean innerDispatchTouchEvent(MotionEvent e) {
// When the event queue is captured, dispatch them to
// the component tree.
return mEventTouchQueueCaptured && super.dispatchTouchEvent(e);
}
class AppWidgetTouchEventQueue implements CapturedEventQueue {
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
return innerDispatchTouchEvent(e);
}
@Override
public void release() {
mEventTouchQueueCaptured = false;
}
}
}
}